You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot认证后出现Anonymous Role问题排查与解决求助

Spring Boot + Angular认证后仍识别为匿名角色问题排查

问题描述

Spring Boot + Angular项目中,管理员账号完成登录认证后,系统仍将用户识别为匿名角色,访问受保护接口时返回Full authentication is required to access this resource错误,尝试多种方案未解决。

相关代码与日志

WebSecurityConfig代码

public class WebSecurityConfig {
    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;

    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());

        return authProvider;
    }

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

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

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

        http.authenticationProvider(authenticationProvider());

        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Auth Controller代码

@CrossOrigin(origins = "http://localhost:4200", maxAge = 3600)
@RestController
@RequestMapping("/api2/auth")
public class AuthController {
        @Autowired
        AuthenticationManager authenticationManager;
        @Autowired
        UserRepository userRepository;
        @Autowired
        RoleRepository roleRepository;
        @Autowired
        PasswordEncoder encoder;
        @Autowired
        JwtUtils jwtUtils;

        @PostMapping("/signin")
        public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
            Authentication authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
            SecurityContextHolder.getContext().setAuthentication(authentication);

            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);
            List<String> roles = userDetails.getAuthorities().stream()
                    .map(item -> item.getAuthority())
                    .collect(Collectors.toList());
            return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
                    .body(new UserInfoResponse(userDetails.getId(),
                            userDetails.getUsername(),
                            userDetails.getPassword(),
                            roles));
        }

        @PostMapping("/signup")
        public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
            if (userRepository.existsByUsername(signUpRequest.getUsername())) {
                return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
            }

            // Create new user's account
            User user = new User(signUpRequest.getUsername(),
                    encoder.encode(signUpRequest.getPassword()));
            Set<String> strRoles =  signUpRequest.getRole();
            Set<Role> roles = new HashSet<>();
            if (strRoles == null) {
                Role userRole = roleRepository.findByName(ERole.ROLE_USER)
                        .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
                roles.add(userRole);
            } else {
                strRoles.forEach(role -> {
                    if ("admin".equals(role)) {
                        Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
                                .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
                        roles.add(adminRole);

                    } else if ("user".equals(role)) {
                        Role userRole = roleRepository.findByName(ERole.ROLE_USER)
                                .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
                        roles.add(userRole);
                    }
                });
            }
            user.setRoles(roles);
            userRepository.save(user);
            return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
        }

        @PostMapping("/signout")
        public ResponseEntity<?> logoutUser() {
            ResponseCookie cookie = jwtUtils.getCleanJwtCookie();
            return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString())
                    .body(new MessageResponse("You've been signed out!"));
        }
    }

请求追踪日志

ERROR 2540 ---  c.s.e.security.jwt.AuthEntryPointJwt     : Unauthorized error: Full authentication is required to access this resource
TRACE 2540 ---  o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]
DEBUG 2540 ---  s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
TRACE 2540 --- o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter...]
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/13)
TRACE 2540 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : Invoking SecurityContextPersistenceFilter (3/13)
DEBUG 2540 --- s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
TRACE 2540 --- o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/13)
TRACE 2540 --- o.s.security.web.FilterChainProxy        : Invoking CorsFilter (5/13)
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking LogoutFilter (6/13)
TRACE 2540 ---  o.s.s.w.a.logout.LogoutFilter            : Did not match request to Or [Ant [pattern='/logout', GET], Ant [pattern='/logout', POST], Ant [pattern='/logout', PUT], Ant [pattern='/logout', DELETE]]
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking AuthTokenFilter (7/13)
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking RequestCacheAwareFilter (8/13)
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderAwareRequestFilter (9/13)
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking AnonymousAuthenticationFilter (10/13)
TRACE 2540 ---[ o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
TRACE 2540 ---  o.s.security.web.FilterChainProxy        : Invoking SessionManagementFilter (11/13)
TRACE 2540 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : Invoking ExceptionTranslationFilter (12/13)
TRACE 2540 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : Invoking FilterSecurityInterceptor (13/13)
TRACE 2540 --- [nio-8080-exec-2] edFilterInvocationSecurityMetadataSource : Did not match request to Ant [pattern='/api2/auth/**'] - [permitAll] (1/2)
TRACE 2540 ---o.s.s.w.a.i.FilterSecurityInterceptor    : Did not re-authenticate AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] before authorizing
TRACE 2540 ---  o.s.s.w.a.expression.WebExpressionVoter  : Voted to deny authorization
TRACE 2540 ---  o.s.s.w.a.i.FilterSecurityInterceptor    : Failed to authorize filter invocation [GET /product/slider] with attributes [authenticated] using AffirmativeBased [DecisionVoters=[org.springframework.security.web.access.expression.WebExpressionVoter@1dcad16f], AllowIfAllAbstainDecisions=false]
TRACE 2540 ---  o.s.s.w.a.ExceptionTranslationFilter     : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied

补充日志

ERROR 8184 ---  c.s.e.security.jwt.AuthEntryPointJwt     : Unauthorized error: Full authentication is required to access this resource >
TRACE 8184 --- o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure] >
DEBUG 8184 --- s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

项目类结构

项目类结构

问题分析

从日志可见,AuthTokenFilter已被调用,但后续AnonymousAuthenticationFilter仍设置了匿名身份到SecurityContextHolder,说明AuthTokenFilter未成功解析JWT并设置认证信息。核心原因包括:

  1. AuthTokenFilter缺失从Cookie提取、验证JWT并设置Authentication的逻辑;
  2. CORS配置未允许前端携带Cookie,导致后端无法获取认证信息;
  3. JWT Cookie的路径、域名等参数配置错误,前端请求未携带该Cookie;
  4. UserDetailsServiceImpl未正确返回包含角色的用户信息。

解决办法

1. 完善AuthTokenFilter认证逻辑

实现从Cookie提取JWT、验证并设置Authentication的逻辑:

public class AuthTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(jwt);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }

        filterChain.doFilter(request, response);
    }

    private String parseJwt(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                // 与JwtUtils中Cookie名称一致
                if ("jwtToken".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

2. 配置支持Cookie的CORS策略

在WebSecurityConfig中添加自定义CORS配置:

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    configuration.setExposedHeaders(Arrays.asList(HttpHeaders.SET_COOKIE));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf().disable()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeRequests().antMatchers("/api2/auth/**").permitAll()
            .anyRequest().authenticated();

    http.authenticationProvider(authenticationProvider());

    http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

Angular端请求需设置withCredentials: true:

this.http.get('/product/slider', { withCredentials: true }).subscribe(...);

3. 修正JwtUtils的Cookie生成逻辑

确保Cookie参数配置正确:

public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
    String jwt = generateTokenFromUsername(userPrincipal.getUsername());
    ResponseCookie cookie = ResponseCookie.from("jwtToken", jwt)
            .path("/")
            .maxAge(24 * 60 * 60)
            .httpOnly(true)
            .secure(false) // 生产环境改为true
            .sameSite(SameSiteMode.LAX)
            .build();
    return cookie;
}

4. 检查UserDetailsServiceImpl实现

确保正确返回包含角色的用户信息:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

        return UserDetailsImpl.build(user);
    }
}

UserDetailsImpl的构建方法:

public static UserDetailsImpl build(User user) {
    List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority(role.getName().name()))
            .collect(Collectors.toList());

    return new UserDetailsImpl(
            user.getId(),
            user.getUsername(),
            user.getPassword(),
            authorities);
}

验证步骤

  1. 登录后检查浏览器是否存在jwtTokenCookie;
  2. 访问接口时确认请求携带该Cookie;
  3. 调试AuthTokenFilter,确认JWT被正确解析并设置Authentication。

内容的提问来源于stack exchange,提问作者Amirreza Andishmand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 14:10:20