内容简介:设计REST API时,必须考虑如何保护REST API,在基于Spring的应用程序中,Spring Security是一种出色的身份验证和授权解决方案,它提供了几种保护REST API的选项。最简单的方法是使用HTTP Basic,当你启动基于Spring Boot的应用程序时,默认情况下会激活它,这有利于开发,可在开发阶段经常使用,但不建议在生产环境中使用。Spring Session(使用Spring Security)提供了一个简单的策略来创建和验证基于头的令牌(会话ID),它可以用于保护RES
设计REST API时,必须考虑如何保护REST API,在基于Spring的应用程序中,Spring Security是一种出色的身份验证和授权解决方案,它提供了几种保护REST API的选项。
最简单的方法是使用HTTP Basic,当你启动基于Spring Boot的应用程序时,默认情况下会激活它,这有利于开发,可在开发阶段经常使用,但不建议在生产环境中使用。
Spring Session(使用Spring Security)提供了一个简单的策略来创建和验证基于头的令牌(会话ID),它可以用于保护RESTful API。
除此之外,Spring Security OAuth(Spring Security下的子项目)提供OAuth授权的完整解决方案,包括OAuth2协议中定义的所有角色的实现,例如授权服务器,资源服务器,OAuth2客户端等,Spring Cloud在其子项目Spring Cloud Security中给OAuth2客户端增加了单点登录功能,在基于Spring Security OAuth的解决方案中,访问令牌的内容可以是签名的JWT令牌或不透明值,我们必须遵循标准OAuth2授权流程来获取访问令牌。
对于那些没有计划将自己API暴露给第三方应用程序的资源完全拥有者来说,基于JWT令牌的简单授权更简单合理(我们不需要管理第三方客户端应用程序的凭据)。
Spring Security本身并没有提供这样的选项,幸运的是,通过将我们的自定义过滤器混合到Spring Security Filter Chain中来实现它并不困难。在这篇文章中,我们将创建这样一个自定义JWT身份验证解决方案。
在此示例应用程序中,可以将基于自定义JWT令牌的身份验证流程指定为以下步骤:
1. 从身份验证端点获取基于JWT的令牌,例如/auth/signin。
2. 从身份验证结果中提取令牌。
3. 将HTTP标头Authorization值设置为Bearer jwt_token。
4. 然后发送一个访问受保护资源的请求。
5. 如果请求的资源受到保护,Spring Security将使用我们的自定义Filter来验证JWT令牌,并构建一个Authentication对象,把它放入SecurityContextHolder以完成身份验证流程。
6. 如果JWT令牌有效,它将把请求的资源返回给客户端。
生成项目框架
创建新Spring Boot项目的最快方法是使用Spring Initializr生成基本代码。
打开浏览器,转到http://start.spring.io,在Dependencies字段中,选择Web,Security,JPA,Lombok,然后单击Generate按钮或按ALT + ENTER键以生成项目框架代码。
等待一段时间下载生成的代码,完成后,将zip文件解压缩到本地系统。
打开你喜欢的IDE,例如Intellij IDEA,NetBeans IDE,然后导入它。
创建示例REST API
在此应用程序中,我们将公开车辆资源的REST API。
/vehicles POST {name:'title'}
/vehicles/{id} GET 200, {id:'1', name:'title'}
/vehicles/{id} PUT {name:'title'}
/vehicles/{id} DELETE
创建JPA实体Vehicle。
@Entity @Table(name="vehicles") @Data @Builder @AllArgsConstructor @NoArgsConstructor public class Vehicle implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id ; @Column private String name; }
创建JPA存储库:
public interface VehicleRepository extends JpaRepository<Vehicle, Long> { }
创建一个Spring MVC basec Controller来公开REST API。
@RestController @RequestMapping("/v1/vehicles") public class VehicleController { private VehicleRepository vehicles; public VehicleController(VehicleRepository vehicles) { this.vehicles = vehicles; } @GetMapping("") public ResponseEntity all() { return ok(this.vehicles.findAll()); } @PostMapping("") public ResponseEntity save(@RequestBody VehicleForm form, HttpServletRequest request) { Vehicle saved = this.vehicles.save(Vehicle.builder().name(form.getName()).build()); return created( ServletUriComponentsBuilder .fromContextPath(request) .path("/v1/vehicles/{id}") .buildAndExpand(saved.getId()) .toUri()) .build(); } @GetMapping("/{id}") public ResponseEntity get(@PathVariable("id") Long id) { return ok(this.vehicles.findById(id).orElseThrow(() -> new VehicleNotFoundException())); } @PutMapping("/{id}") public ResponseEntity update(@PathVariable("id") Long id, @RequestBody VehicleForm form) { Vehicle existed = this.vehicles.findById(id).orElseThrow(() -> new VehicleNotFoundException()); existed.setName(form.getName()); this.vehicles.save(existed); return noContent().build(); } @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable("id") Long id) { Vehicle existed = this.vehicles.findById(id).orElseThrow(() -> new VehicleNotFoundException()); this.vehicles.delete(existed); return noContent().build(); } }
这很简单而且不用动脑。我们定义了VehicleNotFoundException,如果相关id车辆未找到将抛出这个错误。
创建一个简单的异常处理程序来处理自定义异常。
@RestControllerAdvice @Slf4j public class RestExceptionHandler { @ExceptionHandler(value = {VehicleNotFoundException.class}) public ResponseEntity vehicleNotFound(VehicleNotFoundException ex, WebRequest request) { log.debug("handling VehicleNotFoundException..."); return notFound().build(); } }
创建一个CommandLineRunnerbean以在应用程序启动阶段初始化一些车辆数据。
@Component @Slf4j public class DataInitializer implements CommandLineRunner { @Autowired VehicleRepository vehicles; @Override public void run(String... args) throws Exception { log.debug("initializing vehicles data..."); Arrays.asList("moto", "car").forEach(v -> this.vehicles.saveAndFlush(Vehicle.builder().name(v).build())); log.debug("printing all vehicles..."); this.vehicles.findAll().forEach(v -> log.debug(" Vehicle :" + v.toString())); } }
通过在终端中执行命令行mvn spring-boot:run运行,或直接在IDE中运行类来运行应用程序。
打开终端,用于curl测试API:
>curl http://localhost:8080/v1/vehicles [ { "id" : 1, "name" : "moto" }, { "id" : 2, "name" : "car" } ]
Spring Data Rest能直接通过Repository接口公开API。
@RepositoryRestResource在现有VehicleRepository界面上添加注释。
@RepositoryRestResource(path = "vehicles", collectionResourceRel = "vehicles", itemResourceRel = "vehicle") public interface VehicleRepository extends JpaRepository<Vehicle, Long> { }
重新启动应用程序并尝试访问http://localhost:8080/vehicles
curl -X GET http://localhost:8080/vehicles { "_embedded" : { "vehicles" : [ { "name" : "moto", "_links" : { "self" : { "href" : "http://localhost:8080/vehicles/1" }, "vehicle" : { "href" : "http://localhost:8080/vehicles/1" } } }, { "name" : "car", "_links" : { "self" : { "href" : "http://localhost:8080/vehicles/2" }, "vehicle" : { "href" : "http://localhost:8080/vehicles/2" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/vehicles{?page,size,sort}", "templated" : true }, "profile" : { "href" : "http://localhost:8080/profile/vehicles" } }, "page" : { "size" : 20, "totalElements" : 2, "totalPages" : 1, "number" : 0 } }
这里利用Spring HATEOAS项目来暴露更丰富的REST API,这些API属于Richardson Mature Model Level 3(自我文档)。
保护REST API
现在我们将创建一个基于JWT令牌的自定义身份验证过滤器来验证JWT令牌。
JwtTokenFilter为JWT令牌验证创建过滤器名称。
public class JwtTokenFilter extends GenericFilterBean { private JwtTokenProvider jwtTokenProvider; public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { String token = jwtTokenProvider.resolveToken((HttpServletRequest) req); if (token != null && jwtTokenProvider.validateToken(token)) { Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null; SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(req, res); } }
它使用JwtTokenProvider处理JWT,例如生成JWT令牌,解析JWT声明。
@Component public class JwtTokenProvider { @Value("${security.jwt.token.secret-key:secret}") private String secretKey = "secret"; @Value("${security.jwt.token.expire-length:3600000}") private long validityInMilliseconds = 3600000; // 1h @Autowired private UserDetailsService userDetailsService; @PostConstruct protected void init() { secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } public String createToken(String username, List<String> roles) { Claims claims = Jwts.claims().setSubject(username); claims.put("roles", roles); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder()// .setClaims(claims)// .setIssuedAt(now)// .setExpiration(validity)// .signWith(SignatureAlgorithm.HS256, secretKey)// .compact(); } public Authentication getAuthentication(String token) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } public String getUsername(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } public String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7, bearerToken.length()); } return null; } public boolean validateToken(String token) { try { Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); if (claims.getBody().getExpiration().before(new Date())) { return false; } return true; } catch (JwtException | IllegalArgumentException e) { throw new InvalidJwtAuthenticationException("Expired or invalid JWT token"); } } }
创建一个独立的Configurer类来进行设置JwtTokenFilter。
public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private JwtTokenProvider jwtTokenProvider; public JwtConfigurer(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } @Override public void configure(HttpSecurity http) throws Exception { JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }
在我们的应用程序作用域中应用此配置器SecurityConfig。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtTokenProvider jwtTokenProvider; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { //@formatter:off http .httpBasic().disable() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/auth/signin").permitAll() .antMatchers(HttpMethod.GET, "/vehicles/**").permitAll() .antMatchers(HttpMethod.DELETE, "/vehicles/**").hasRole("ADMIN") .antMatchers(HttpMethod.GET, "/v1/vehicles/**").permitAll() .anyRequest().authenticated() .and() .apply(new JwtConfigurer(jwtTokenProvider)); //@formatter:on } }
要启用Spring Security,我们必须在运行时提供自定义UserDetailsService这个bean:
@Component public class CustomUserDetailsService implements UserDetailsService { private UserRepository users; public CustomUserDetailsService(UserRepository users) { this.users = users; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return this.users.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found")); } }
该CustomUserDetailsService试图以用户名为查询参数从数据库中获取用户数据。
User是一个标准的JPA实体,为了简化工作,它还实现了Spring Security特定的UserDetails接口。
@Entity @Table(name="users") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.AUTO) Long id; @NotEmpty private String username; @NotEmpty private String password; @ElementCollection(fetch = FetchType.EAGER) @Builder.Default private List<String> roles = new ArrayList<>(); @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.roles.stream().map(SimpleGrantedAuthority::new).collect(toList()); } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
创建为User实体创建一个Repository接口:
public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); }
创建一个控制器来验证用户:
@RestController @RequestMapping("/auth") public class AuthController { @Autowired AuthenticationManager authenticationManager; @Autowired JwtTokenProvider jwtTokenProvider; @Autowired UserRepository users; @PostMapping("/signin") public ResponseEntity signin(@RequestBody AuthenticationRequest data) { try { String username = data.getUsername(); authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword())); String token = jwtTokenProvider.createToken(username, this.users.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("Username " + username + "not found")).getRoles()); Map<Object, Object> model = new HashMap<>(); model.put("username", username); model.put("token", token); return ok(model); } catch (AuthenticationException e) { throw new BadCredentialsException("Invalid username/password supplied"); } } }
创建端点以获取当前用户信息。
@RestController() public class UserinfoController { @GetMapping("/me") public ResponseEntity currentUser(@AuthenticationPrincipal UserDetails userDetails){ Map<Object, Object> model = new HashMap<>(); model.put("username", userDetails.getUsername()); model.put("roles", userDetails.getAuthorities() .stream() .map(a -> ((GrantedAuthority) a).getAuthority()) .collect(toList()) ); return ok(model); } }
当前用户通过身份验证后,@AuthenticationPrincipal将绑定到当前主体。
在我们的初始化类中添加两个用于测试目的的用户。
@Component @Slf4j public class DataInitializer implements CommandLineRunner { //... @Autowired UserRepository users; @Autowired PasswordEncoder passwordEncoder; @Override public void run(String... args) throws Exception { //... this.users.save(User.builder() .username("user") .password(this.passwordEncoder.encode("password")) .roles(Arrays.asList( "ROLE_USER")) .build() ); this.users.save(User.builder() .username("admin") .password(this.passwordEncoder.encode("password")) .roles(Arrays.asList("ROLE_USER", "ROLE_ADMIN")) .build() ); log.debug("printing all users..."); this.users.findAll().forEach(v -> log.debug(" User :" + v.toString())); } }
现在用于curl尝试此身份验证流程。
通过user/password登录:
curl -X POST http://localhost:8080/auth/signin -H "Content-Type:application/json" -d "{\"username\":\"user\", \"password\":\"password\"}" { "username" : "user", "token" : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImlhdCI6MTUyNDY0OTI4OSwiZXhwIjoxNTI0NjUyODg5fQ.Lj1w6vPJNdJbcY6cAhO3DbkgCAqpG7lzztzUeKMyNyE" }
将token值放入HTTP标头Authorization,将其值设置为Bearer token,然后访问当前用户信息。
curl -X GET http://localhost:8080/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImlhdCI6MTUyNDY0OTI4OSwiZXhwIjoxNTI0NjUyODg5fQ.Lj1w6vPJNdJbcY6cAhO3DbkgCAqpG7lzztzUeKMyNyE" { "roles" : [ "ROLE_USER" ], "username" : "user" }
github中的源代码 ,它还包括使用JUnit,Spring Boot Test,RestAssured等的测试代码。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 并发编程 -- 线程池源码实战
- 别人家的 InfluxDB 实战 + 源码剖析
- 新书上市 -《Elasticsearch 源码解析与优化实战》
- ItemDecoration深入解析与实战(一)——源码分析
- Spring Boot系列实战文章合集(附源码)
- 小程序源码反编译实战笔记2018-05-31
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Making Things See
Greg Borenstein / Make / 2012-2-3 / USD 39.99
Welcome to the Vision Revolution. With Microsoft's Kinect leading the way, you can now use 3D computer vision technology to build digital 3D models of people and objects that you can manipulate with g......一起来看看 《Making Things See》 这本书的介绍吧!