Spring Security:在SecurityContext中找不到认证对象。

5

以下的配置(filterChain)在SpringBoot-2.7.5中运行良好,但是在我尝试在SpringBoot-3.0.0-RC1中测试它时,它不起作用并显示以下消息。如果要迁移到Spring-Boot-3.0.0,是否需要更改任何内容?谢谢。

{ "timestamp": 1667794247614, "status": 401, "error": "Unauthorized", "message": "SecurityContext中未找到Authentication对象", "path": "/api/admin/1" }

 @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .exceptionHandling().authenticationEntryPoint(jwtAuthenticationProvider).and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeRequests()
        .antMatchers("/**").permitAll()            
        // private endpoints
        .anyRequest().authenticated();

    http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

以下是jwtTokenFilter:
@Component
public class **JwtTokenFilter** extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private JPAUserDetailService jpaUserDetailService;

    

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // Get authorization header and validate
        final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        
        if (isEmpty(header) || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        // Get jwt token and validate
        final String token = header.split(" ")[1].trim();
        
        if (!jwtTokenUtil.validate(token)) {
                                    
            chain.doFilter(request, response);
            
            return;
        }

        // Get user identity and set it on the spring security context
        UserDetails userDetails = jpaUserDetailService.loadUserByUsername(jwtTokenUtil.getUsername(token));

        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, (userDetails == null ? null : userDetails.getAuthorities()));

        
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(authentication);
                
        chain.doFilter(request, response);
    }

}
3个回答

3
在Spring Security 6中,默认行为是SecurityContextHolderFilter仅从SecurityContextRepository读取SecurityContext并将其填充到SecurityContextHolder中。现在,如果用户希望SecurityContext在请求之间保持不变,则必须显式使用SecurityContextRepository保存SecurityContext。这消除了歧义,并通过仅在必要时要求写入SecurityContextRepository(即HttpSession)来提高性能。
SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);

查看https://docs.spring.io/spring-security/reference/5.8/migration.html#_explicit_save_securitycontextrepository

如果上述方法不行,请尝试返回到5.x默认设置:

http
    .securityContext((securityContext) -> 
            .requireExplicitSave(false)
    )

嗨,我按照上面的更改了一些代码,但仍然无法正常工作。您能否教我在上述代码中漏掉了什么?谢谢。 - EricMacau
我已经更新了答案,并提供了一种返回旧默认设置的方法。 - Marcus Hert da Coregio
我已经将代码更改为您的最新更新代码,但仍然无法正常工作,请问您能否提供完整的Spring Security 6配置?谢谢。 - EricMacau

0

我按照以下方式更改了一些代码,但仍然不起作用。

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    // Get authorization header and validate
    final String header = request.getHeader(HttpHeaders.AUTHORIZATION);

    if (isEmpty(header) || !header.startsWith("Bearer ")) {
        chain.doFilter(request, response);
        return;
    }

    // Get jwt token and validate
    final String token = header.split(" ")[1].trim();

    if (!jwtTokenUtil.validate(token)) {

        chain.doFilter(request, response);

        return;
    }

    // Get user identity and set it on the spring security context
    UserDetails userDetails = jpaUserDetailService.loadUserByUsername(jwtTokenUtil.getUsername(token));

    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, (userDetails == null ? null : userDetails.getAuthorities()));


    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));


    SecurityContextImpl scix = new SecurityContextImpl(authentication);

    SecurityContextHolder.setContext(scix);



    RequestAttributeSecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();

    securityContextRepository.saveContext(scix, request, response);


    chain.doFilter(request, response);
}

还有以下的配置:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .exceptionHandling().authenticationEntryPoint(jwtAuthenticationProvider)
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeHttpRequests((requests) - > requests
            .requestMatchers("/swagger-ui", "/rest-api-docs").permitAll()
            .anyRequest().authenticated()
        )

        .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
        .securityContext((securityContext) - >
            securityContext.requireExplicitSave(false)
        );
    return http.build();
}

0

我想出了这个问题的解决方案。

正如你在Spring Security 6.x中所看到的:

SecurityContextHolderFilter 负责使用 SecurityContextRepository 在请求之间加载 SecurityContext。

并且:

带有 SecurityContext 的 SecurityContextHolder 还会将 SecurityContext 保存到 SecurityContextRepository 中,如果需要在请求之间保留。

因此,我将 SecurityContextHolderRepository 添加到 SecurityFilterChain 中:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig{

    @Resource(name = "jwtService")
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(encoder());
        return authenticationProvider;
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(){
        return new JwtAuthenticationFilter();
    }
    @Bean
    public SecurityContextRepository securityContextRepository(){
        return new NullSecurityContextRepository(); // I use Null Repository since I don't need it for anything except store information in UserDetails
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.cors().and()
                .csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers("/api/auth/n/**").permitAll()
                .requestMatchers("/api/auth/r/testing").permitAll() // The API need JWT for authentication
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .securityContext((securityContext) -> securityContext.securityContextRepository(securityContextRepository())) // Add Security Context Holder Repository
                .authenticationProvider(authenticationProvider());

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // Add it before UserName and Password AuthenticationFilter
        return http.build();
    }
}

然后在我的自定义Jwt过滤器中,我将SecurityContext保存到NullContextRepository中,问题得到解决:
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final Logger LOG = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private JwtUserDetailsService userDetailsService;

    @Resource
    private SecurityContextRepository securityContextRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwt = getJwtFromRequest(request);

        if(!StringUtils.isEmpty(jwt)){
            jwtTokenUtil.validateToken(jwt);
            String userName = jwtTokenUtil.getUserNameFromToken(jwt);
            UserDetails userDetails = userDetailsService.loadUserByUsername(userName);

            if (userDetails != null && userDetails instanceof CustomUserDetails) {
                Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                SecurityContext securityContext = SecurityContextHolder.getContext();
                securityContext.setAuthentication(authentication);
                SecurityContextHolder.setContext(securityContext);

                securityContextRepository.saveContext(securityContext, request, response); // ADD this 
                LOG.info("Authentication has user name : " + ((UserDetails)authentication.getPrincipal()).getUsername());
            }
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request){
        String bearerToken = request.getHeader("Authorization");
        if(bearerToken == null){
            return null;
        }
        if(bearerToken.startsWith("Bearer ")){
            return bearerToken.substring(7);
        }
        return null;
    }
}

这是结果:

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接