Spring Security是一个非常流行的安全框架,用于在Spring应用程序中实现身份验证和授权。JWT是一种用于在不同的系统之间安全传输信息的方式。在使用Spring Security和JWT时,退出登录是常见的操作之一,但处理起来可能会遇到一些问题。下面我会详细讲解在Spring Security中使用JWT退出登录时可能遇到的坑,包括原因和解决方案。
问题
当使用Spring Security和JWT时,退出登录是比较简单的,只需从请求头中获取JWT令牌,并将其放入黑名单中即可。这样,当用户下次再尝试用这个令牌进行访问时,就会发现其已经过期了,从而被拒绝访问。
但实际上,一些Spring Security配置可能会导致问题。具体地说,这些配置将JWT令牌的校验交给了Spring Security,而不是自己进行校验。这时候,一些令牌校验可能会被忽略,从而导致用户无法退出登录。具体可能的原因和解决方案如下。
问题一:无法退出登录
在一些情况下,由于Spring Security对JWT令牌的处理,我们可能无法正常退出登录。例如,下列示例中,我们使用Spring Boot 2.1.1版本的starter-security和starter-jwt依赖,并把JWT令牌放入请求头中:
@RestController
@RequestMapping("/api/sample")
public class SampleController {
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponseDto> login(@RequestBody JwtAuthenticationRequestDto requestDto) {
// Authenticate user and return JWT token
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletResponse response) {
// Get JWT token from request header
String token = getToken(request);
// Invalidate JWT token
tokenService.invalidate(token);
return ResponseEntity.ok(null);
}
private String getToken(HttpServletRequest request) {
final String requestHeader = request.getHeader("Authorization");
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
return requestHeader.substring(7);
}
return null;
}
}
当我们尝试调用POST /api/sample/logout接口时,我们会发现之后再使用同样的JWT令牌进行访问时,依然可以正常访问。这是因为,虽然我们已经将JWT令牌放入了黑名单中,但Spring Security会在黑名单中进行校验时忽略它。
问题二:过度校验
在一些情况下,Spring Security可能会对JWT令牌进行过度校验,从而导致无法退出登录。例如,下列示例中,我们使用了一个过度复杂的JWT解析器来进行令牌校验:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/sample/login").permitAll()
.anyRequest().authenticated();
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
}
虽然这个解析器的作用是验证token是否合法,但在退出登录时,我们需要把token放到黑名单中,而不是对token进行额外的校验。
解决方案
解决上述问题的主要方法是修改Spring Security的配置,以便在退出登录时正确地处理JWT令牌。
解决方案一:添加过滤器
在Spring Security中,可以通过添加一个过滤器来关闭原生的JWT令牌校验,从而避免退出登录时的问题。示例代码如下:
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Get token from request header
String token = getToken(request);
if (JwtTokenUtil.isRefreshToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
// Check if token exists in a blacklist
if (tokenService.isTokenBlacklisted(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
filterChain.doFilter(request, response);
}
private String getToken(HttpServletRequest request) {
final String requestHeader = request.getHeader("Authorization");
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
return requestHeader.substring(7);
}
return null;
}
@Autowired
private TokenService tokenService;
}
这里使用了一个自定义的JwtTokenFilter过滤器,其中doFilterInternal方法首先会获取请求头中的JWT令牌,然后校验它是否存在于黑名单中。如果不存在,则通过该请求,否则返回401未授权错误。
自Spring Boot 2.3版本开始,可以通过jwt解析器中设置自定义验签算法,例如:
converter.setVerifier(JWT.require(Algorithm.HMAC256("my-secret"))
.withIssuer("auth0")
.build());
这样就可以根据需求设置验签算法。
解决方案二:更改JwtAccessTokenConverter
另一种解决方法是更改JwtAccessTokenConverter,使其在退出登录时不再进行过度校验。示例代码如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/sample/login").permitAll()
.anyRequest().authenticated();
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifier(new CustomJwtAccessTokenConverter(publicKey));
return converter;
}
public static class CustomJwtAccessTokenConverter extends DefaultAccessTokenConverter {
private final JwtVerifier verifier;
public CustomJwtAccessTokenConverter(String publicKey) {
this.verifier = JWT.require(Algorithm.HMAC256(publicKey))
.withIssuer("my-application")
.build();
}
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
try {
// Parse JWT token
DecodedJWT jwt = verifier.verify(map.get("access_token").toString());
// Extract authentication data from token
AuthenticationCustomData data = new AuthenticationCustomData();
data.setUserId(jwt.getClaim("userId").asString());
data.setAuthorities((List<String>) map.get("authorities"));
UsernamePasswordAuthenticationToken userAuthentication = new UsernamePasswordAuthenticationToken(
jwt.getSubject(),
null,
AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", data.getAuthorities())));
return new OAuth2Authentication(
new OAuth2Request(null, "my-application", userAuthentication.getAuthorities(), true, null, null, null, data.getScopes(), null),
userAuthentication);
} catch (JWTVerificationException e) {
throw new InvalidTokenException(e.getMessage());
}
}
}
}
在这个解决方案中,我们自定义了CustomJwtAccessTokenConverter类,并重写了其中的extractAuthentication方法,使其只解密token,而不进行过度校验。在上述示例中,我们使用了一个自定义的AuthenticationCustomData类,它包含了用户ID、授权信息等信息。我们还在JwtAccessTokenConverter中添加了公钥,并使用它来验证JWT令牌的签名。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Spring Security中用JWT退出登录时遇到的坑 - Python技术站