Spring Boot 安全性 - 使用 Cookie 中的令牌而不是授权标头

问题描述 投票:0回答:1

您好,我正在尝试从 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-boot spring-security spring-oauth2
1个回答
5
投票

我能够通过查看 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);
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.