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

Spring Security 6.2.1会话登录403错误及UserDetails空指针问题

Spring Security 6.2.1 无表单登录问题修复方案

核心问题拆解

  • 403 Forbidden:hasAnyRole权限校验逻辑错误,Spring Security的hasAnyRole会自动给传入的角色名添加ROLE_前缀,若数据库存储的是ROLE_USER,直接写hasAnyRole("ROLE_USER")会实际校验ROLE_ROLE_USER,导致权限不匹配。
  • @AuthenticationPrincipal注入null:登录流程未正确将Authentication对象存入SecurityContext并绑定到会话,后续请求无法从会话中读取认证信息。

具体修复步骤

1. 修正权限校验规则

根据需求选择两种正确写法之一:

  • 用hasAnyRole:仅传入角色名(去掉前缀)
  • 用hasAuthority:传入完整的权限字符串(保留前缀)

示例配置代码:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 根据实际场景决定是否关闭CSRF
            .authorizeHttpRequests(auth -> auth
                // 写法1:hasAnyRole 自动补全ROLE_前缀
                .requestMatchers("/modify/update").hasAnyRole("USER")
                // 写法2:hasAuthority 直接使用完整权限字符串
                // .requestMatchers("/modify/update").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 会话式登录需保留会话
            );
        return http.build();
    }
}

2. 完善无表单登录的认证流程

自定义登录接口必须手动完成Authentication对象的创建与存储,确保会话绑定认证信息:

@RestController
public class LoginController {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private SecurityContextRepository securityContextRepository;

    @PostMapping("/login")
    public ResponseEntity<Void> login(@RequestBody LoginRequest request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        // 1. 加载用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        // 2. 校验密码
        if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        // 3. 创建认证对象
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities()
        );
        // 4. 存入SecurityContext
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authToken);
        SecurityContextHolder.setContext(context);
        // 5. 保存到会话,确保后续请求能读取
        securityContextRepository.saveContext(context, servletRequest, servletResponse);
        return ResponseEntity.ok().build();
    }
}

3. 验证会话与SecurityContext绑定

  • 确认登录响应头包含Set-Cookie: JSESSIONID=xxx; Path=/; HttpOnly
  • 后续请求携带该JSESSIONID Cookie
  • 调试时可在控制器中打印认证信息:
@PostMapping("/modify/update")
public ResponseEntity<String> update(HttpServletRequest request) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    System.out.println("当前认证用户:" + auth.getName());
    // 业务逻辑
    return ResponseEntity.ok("操作成功");
}

4. 正确使用@AuthenticationPrincipal

确保注入类型与UserDetails实现类匹配,若为自定义用户类可直接注入:

// 注入UserDetails
@PostMapping("/modify/update")
public ResponseEntity<String> update(@AuthenticationPrincipal UserDetails userDetails) {
    if (userDetails == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
    return ResponseEntity.ok("更新成功,操作人:" + userDetails.getUsername());
}

// 注入自定义User类(需实现UserDetails)
@PostMapping("/modify/update")
public ResponseEntity<String> update(@AuthenticationPrincipal CustomUser customUser) {
    return ResponseEntity.ok("更新成功,操作人:" + customUser.getNickname());
}

内容的提问来源于stack exchange,提问作者김민식

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 00:30:54