您好,我正在尝试从 Spring Security 会话身份验证和授权迁移到通过 JWT 进行身份验证。我有一个关于我遇到的具体情况的问题。我感兴趣的是使用 cookie 来避免将令牌存储在本地存储中,而不是使用 Authorization 标头进行身份验证。然而,由于缺少“承载令牌”,我的集成测试一直失败。我想知道是否有其他人遇到过类似的情况,他们需要将 JWT 令牌作为 cookie 发送,而不是使用授权标头。如果是这样,您是如何解决错误消息
look below
的?任何见解或解决方案将不胜感激。谢谢你。
错误
main] .s.r.w.a.BearerTokenAuthenticationFilter : Did not process request since did not find bearer token
集成测试
@Test
@Order(3)
void login() throws Exception {
MvcResult login = this.MOCK_MVC
.perform(post("******")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(new LoginDTO(ADMIN_EMAIL, ADMIN_PASSWORD).convertToJSON().toString())
)
.andExpect(status().isOk())
.andReturn();
Cookie cookie = login.getResponse().getCookie(COOKIE_NAME);
// Test route
this.MOCK_MVC
.perform(get("****").cookie(cookie))
.andExpect(status().isOk());
}
登录方法
/**
* Note Transactional annotation is used because Entity class has properties with fetch type LAZY
* @param dto consist of principal(username or email) and password.
* @param req of type HttpServletRequest
* @param res of type HttpServletResponse
* @throws AuthenticationException is thrown when credentials do not exist or bad credentials
* @return ResponseEntity of type HttpStatus
* */
@Transactional
public ResponseEntity<?> login(LoginDTO dto, HttpServletRequest req, HttpServletResponse res) {
Authentication authentication = this.authManager.authenticate(
UsernamePasswordAuthenticationToken.unauthenticated(dto.getPrincipal(), dto.getPassword())
);
// Jwt Token
String token = this.jwtTokenService.generateToken(authentication);
// Add Jwt Cookie to Header
Cookie jwtCookie = new Cookie(COOKIENAME, token);
jwtCookie.setDomain(DOMAIN);
jwtCookie.setPath(COOKIE_PATH);
jwtCookie.setSecure(COOKIE_SECURE);
jwtCookie.setHttpOnly(HTTPONLY);
jwtCookie.setMaxAge(COOKIEMAXAGE);
// Add custom cookie to response
res.addCookie(jwtCookie);
// Second cookie where UI can access to validate if user is logged in
Cookie cookie = new Cookie(LOGGEDSESSION, UUID.randomUUID().toString());
cookie.setDomain(DOMAIN);
cookie.setPath(COOKIE_PATH);
cookie.setSecure(COOKIE_SECURE);
cookie.setHttpOnly(false);
cookie.setMaxAge(COOKIEMAXAGE);
// Add custom cookie to response
res.addCookie(cookie);
return new ResponseEntity<>(OK);
}
过滤链
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> {
auth.requestMatchers(publicRoutes()).permitAll();
auth.anyRequest().authenticated();
})
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))
.exceptionHandling((ex) -> ex.authenticationEntryPoint(this.authEntryPoint))
// .addFilterBefore(new JwtFilter(), BearerTokenAuthenticationFilter.class)
.logout(out -> out
.logoutUrl("****")
.deleteCookies(COOKIE_NAME, LOGGEDSESSION)
.logoutSuccessHandler((request, response, authentication) ->
SecurityContextHolder.clearContext()
)
)
.build();
}
最后,我想提请您注意前面提到的SecurityFilterChain,您会注意到我已经注释掉了addFilterBefore 方法。最初,我的方法是通过提取包含 JWT 令牌的所需 cookie 并将其添加到请求标头来处理每个传入请求。当 cookie 存在时,此方法效果很好,但当 cookie 为空时,例如在用户登录过程中,则效果不佳。注意
HeaderMapRequestWrapper
实现类似于 link
@Component @Slf4j
public class JwtFilter extends OncePerRequestFilter {
@Value(value = "${server.servlet.session.cookie.name}")
private String COOKIENAME;
@Override
protected void doFilterInternal(
@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain filterChain
) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
log.info("Cookies Array " + Arrays.toString(cookies)); // Null on login requests
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(request);
if (cookies != null) {
Optional<String> cookie = Arrays.stream(cookies)
.map(Cookie::getName)
.filter(name -> name.equals(COOKIENAME))
.findFirst();
cookie.ifPresent(s -> requestWrapper.addHeader(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(s)));
}
filterChain.doFilter(requestWrapper, response);
}
}
我能够通过查看 spring docs 解决这个问题。由于默认情况下,资源服务器会在授权标头中查找不记名令牌,而在我的例子中,jwt 是一个 cookie,因此我必须定义
BearerTokenResolver
的自定义实现。
@Bean
public BearerTokenResolver bearerTokenResolver(JwtDecoder decoder, JwtTokenService service) {
return new BearerResolver(JSESSIONID, decoder, service);
}
private record BearerResolver(
String JSESSIONID,
JwtDecoder decoder,
JwtTokenService service
) implements BearerTokenResolver {
@Override
public String resolve(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
// ternary operator
return cookies == null ? null : Arrays
.stream(cookies)
.filter(cookie -> cookie.getName().equals(JSESSIONID))
.filter(this.service::_isTokenNoneExpired)
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}