原始地址:https://dev.to/d_tomov/jwt-bearer-authentication-authorization-with-spring-security-5-in-a-spring-boot-app-2cfe
这几天,我一直在尝试编译一个关于如何在Spring Boot应用程序中实现JWT Bearer安全性的合理简单的示例。这是REST网络服务安全性的标准。我找到了一些不错的例子,但都不能满足我的兴趣。我试图尽可能收集信息,并将其作为示例清晰地呈现出来。
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
io.jsonwebtoken
jjwt-api
0.11.1
io.jsonwebtoken
jjwt-impl
0.11.1
runtime
io.jsonwebtoken
jjwt-jackson
0.11.1
runtime
我们将应用程序作为普通Spring Boot应用程序启动。添加Spring Web用于标准REST API和Spring Security用于安全部分-下载并解压缩。
我们还需要添加io.jsonwebtoken的JWT依赖项。注意,JWT的两个依赖项是从maven中央复制的,在编译阶段不需要,只需要在应用程序运行时。编译所需的唯一依赖项是jjwt-api。
对于示例,令牌操作被分离到一个称为TokenService的接口中:
public interface TokenService {
String generateToken(User user);
UserPrincipal parseToken(String token);
}
User是应用程序中的实体,如下所示:
public class User {
private Integer id;
private String username;
private String password;
private boolean isAdmin;
}
UserPrincipal是将在Spring的安全上下文中的Principal对象。Principal是当前登录应用程序的用户。稍后我们将看到如何在Spring Security Context中设置它。
UserPrincipal如下所示:
public class UserPrincipal {
private Integer id;
private String username;
private boolean isAdmin;
}
我们需要实现两个方法,一个用于令牌生成,一个用于令牌解析。(JWT_SECRET是一个String)
@Override
public String generateToken(User user) {
Instant expirationTime = Instant.now().plus(1, ChronoUnit.HOURS);
Date expirationDate = Date.from(expirationTime);
Key key = Keys.hmacShaKeyFor(JWT_SECRET.getBytes());
String compactTokenString = Jwts.builder()
.claim("id", user.getId())
.claim("sub", user.getUsername())
.claim("admin", user.isAdmin())
.setExpiration(expirationDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
return "Bearer " + compactTokenString;
}
我们使用Jwts.builder()构建我们的令牌,并将其构建为紧凑的令牌。在示例中,我们使用它来设置id,sub和admin的声明,但是您可以添加任何声明。声明是将添加到JWT的正文中的信息。有一些标准声明,如sub(主题),iss(发行者),…,您可以在此处查看。我们在前面附加了带有空格的“Bearer”作为前缀,以指定身份验证方案为Bearer类型。名称“Bearer身份验证”可以理解为“给予该令牌持有者访问权限”。在此处您将需要做一些决策:
/**
* @param token - 从“Bearer”前缀中剥离的紧凑令牌
*/
@Override
public UserPrincipal parseToken(String token) {
byte[] secretBytes = JWT_SECRET.getBytes();
Jws jwsClaims = Jwts.parserBuilder()
.setSigningKey(secretBytes)
.build()
.parseClaimsJws(token);
String username = jwsClaims.getBody()
.getSubject();
Integer userId = jwsClaims.getBody()
.get("id", Integer.class);
boolean isAdmin = jwsClaims.getBody().get("admin", Boolean.class);
return new UserPrincipal(userId, username, isAdmin);
}
在解析令牌时,您需要与生成JWT时使用的相同的密钥。根据您选择的秘密策略或业务逻辑,您可能需要在此处进行一些验证。使用**Jwts.parserBuilder()**将令牌解析为Jws对象,您可以在其中获取您在令牌中放置的任何声明。您知道它们存在,因为JWT是不可变的,如果有人伪造了一个令牌,解析将失败并引发无效签名异常。
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TokenService tokenService;
public JwtAuthenticationFilter(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws IOException, ServletException {
String authorizationHeader = httpServletRequest.getHeader("Authorization");
if (authorizationHeaderIsInvalid(authorizationHeader)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
UsernamePasswordAuthenticationToken token = createToken(authorizationHeader);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private boolean authorizationHeaderIsInvalid(String authorizationHeader) {
return authorizationHeader == null
|| !authorizationHeader.startsWith("Bearer ");
}
private UsernamePasswordAuthenticationToken createToken(String authorizationHeader) {
String token = authorizationHeader.replace("Bearer ", "");
UserPrincipal userPrincipal = tokenService.parseToken(token);
List authorities = new ArrayList<>();
if (userPrincipal.isAdmin()) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
return new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities);
}
}
在该示例中,我们扩展了OncePerRequestFilter,这是为了保证每个请求调度一次执行的。我们首先简单地检查“Authorization”头是否存在(通常用于传递Bearer令牌)。然后将令牌去掉其“Bearer”前缀,然后将令牌解析返回的UserPrincipal传递给UsernamePasswordAuthenticationToken,它将用作Spring Security上下文中的我们的身份验证/授权。应该使用SecurityContextHolder的setAuthentication方法将这个UsernamePasswordAuthenticationToken设置在SecurityContext中,以便以后使用。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private TokenService tokenService;
@Autowired
public SecurityConfig(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.mvcMatchers("/users", "/users/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(tokenService),
UsernamePasswordAuthenticationFilter.class);
}
}
@EnableGlobalMethodSecurity(prePostEnabled = true)用于启用@PreAuthorize注释,以在控制器方法调用之前检查权限/角色。该类扩展了WebSecurityConfigurerAdapter,这是用于安全配置的基础(适配器)类。这里禁用CSRF-在Web服务中不需要防止跨站请求伪造保护,它通常在浏览器应用程序上下文中使用。进入重点我们配置端点安全性。(顺序很重要,要小心)- 首先,对于由mvcMatchers()匹配的端点允许所有请求- 通常是注册和登录端点。- 然后我们配置除允许的端点之外的每个请求进行身份验证。这意味着Spring将在安全上下文中查找一些形式的身份验证-在我们的例子中是UsernamePasswordAuthenticationToken,如果不存在则返回403 FORBIDDEN。最后,我们添加了过滤器,并将其顺序设置为在UsernamePasswordAuthenticationFilter之前。因为我们没有扩展有序过滤器并且在过滤器类上没有注解@Order,所以需要设置顺序。
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/hello/admin")
@PreAuthorize("hasRole('ADMIN')")
public String helloAdmin() {
return "hello admin";
}
@GetMapping("/hello/user")
public String helloUser() {
UserPrincipal userPrincipal =
(UserPrincipal) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
return "hello " + userPrincipal.getUsername();
}
}
我们有三个示例端点: