首页 > Java > java教程 > 正文

Spring Boot JWT 角色权限控制:解决 401 未授权问题

DDD
发布: 2025-12-04 17:33:07
原创
941人浏览过

spring boot jwt 角色权限控制:解决 401 未授权问题

本教程旨在解决 Spring Boot 中使用 JWT 进行角色权限控制时遇到的 401 未授权错误。文章将深入探讨 Spring Security、JWT 认证与授权的关键组件,包括安全配置、JWT 过滤器、用户详情服务以及认证流程。核心内容聚焦于排查并解决因用户权限数据缺失或配置不当导致的授权失败问题,并提供详细的代码示例和调试建议。

在 Spring Boot 应用中集成 JWT (JSON Web Token) 实现无状态的认证和基于角色的授权是常见的实践。然而,在配置 hasAuthority() 或 hasRole() 来保护 API 端点时,开发者常会遇到 401 未授权错误,即使 JWT token 能够正确生成。本文将详细解析这一问题,并提供一套完整的解决方案和最佳实践。

1. Spring Boot JWT 认证与授权核心组件

要实现基于 JWT 的角色权限控制,Spring Security 需要以下几个核心组件协同工作:

  • WebSecurityConfigurerAdapter (或 SecurityFilterChain): 配置 Spring Security 的整体行为,包括禁用 CSRF、CORS,设置会话管理策略为无状态,以及定义授权规则和添加自定义过滤器。
  • JWT 认证过滤器 (OncePerRequestFilter): 拦截所有受保护的请求,从请求头中提取 JWT token,验证其有效性,并根据 token 中的信息构建认证对象 (Authentication),将其设置到 Spring Security 上下文 (SecurityContextHolder) 中。
  • UserDetailsService: 负责根据用户名加载用户详情 (UserDetails),其中包含用户的密码和最重要的——用户的权限集合 (GrantedAuthority)。
  • AuthenticationManager: 处理用户登录请求,验证用户凭据。
  • JWT 工具: 负责 JWT token 的生成、解析和验证。

2. 安全配置 (WebSecurityConfigurerAdapter)

这是 Spring Security 的入口点,定义了哪些路径需要认证、哪些路径需要特定权限。

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationEntryPoint unauthorizedHandler;
    private final JwtRequestFilter jwtRequestFilter;

    public SecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, JwtRequestFilter jwtRequestFilter) {
        this.unauthorizedHandler = unauthorizedHandler;
        this.jwtRequestFilter = jwtRequestFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() // 禁用CSRF
            .cors().disable() // 禁用CORS (根据实际需求配置)
            .authorizeRequests()
                // 允许所有用户访问登录接口
                .antMatchers("/authenticate", "/register").permitAll()
                // 根据角色分配权限
                .antMatchers("/user/**", "/document/**", "/appointment/**", "/activity/**")
                    .hasAuthority(UserRole.ADMIN.name())
                .antMatchers("/user/**", "/activity/**", "/appointment/", "/document/")
                    .hasAnyAuthority(UserRole.SUPPORTEXECUTIVE.name(), UserRole.FIELDEXECUTIVE.name())
                // 其他所有请求都需要认证
                .anyRequest().authenticated()
            .and()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) // 处理未认证请求
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置为无状态会话
            .and()
            // 在UsernamePasswordAuthenticationFilter之前添加JWT过滤器
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
登录后复制

关键点:

AIBox 一站式AI创作平台
AIBox 一站式AI创作平台

AIBox365一站式AI创作平台,支持ChatGPT、GPT4、Claue3、Gemini、Midjourney等国内外大模型

AIBox 一站式AI创作平台 224
查看详情 AIBox 一站式AI创作平台
  • http.csrf().disable().cors().disable(): 对于无状态的 RESTful API,通常会禁用 CSRF。CORS 根据前端部署情况决定是否禁用或配置。
  • antMatchers().permitAll(): 允许未经认证的请求访问某些公共资源,如登录接口。
  • antMatchers().hasAuthority(UserRole.ADMIN.name()): 这是核心的授权规则。它要求访问匹配路径的用户必须拥有 ADMIN 权限。
  • sessionCreationPolicy(SessionCreationPolicy.STATELESS): 明确指示 Spring Security 不创建或使用 HTTP 会话,这对于 JWT 是必需的。
  • addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class): 将自定义的 JWT 过滤器添加到 Spring Security 过滤器链中,确保在标准的基于表单的认证过滤器之前执行。

3. JWT 认证过滤器 (JwtRequestFilter)

此过滤器负责解析和验证传入请求中的 JWT token,并将认证信息设置到 Spring Security 上下文。

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtUtil jwtUtil; // 假设有一个JWT工具类

    public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7); // 提取token
            try {
                username = jwtUtil.extractUsername(jwt); // 从token中提取用户名
            } catch (Exception e) {
                logger.error("Error extracting username from token: " + e.getMessage());
                // 可以添加更详细的异常处理,例如设置HTTP状态码
            }
        }

        // 如果用户名不为空且当前安全上下文中没有认证信息
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // 验证token是否有效
            if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { // 验证token和用户名
                // 构建认证对象
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()); // 注意这里传入userDetails,而不是仅仅username
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 将认证对象设置到安全上下文
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}
登录后复制

关键点:

  • userDetails.getAuthorities(): 这是最重要的一步。UsernamePasswordAuthenticationToken 的第三个参数必须是用户的权限集合 (Collection extends GrantedAuthority>)。Spring Security 的 hasAuthority() 方法会检查这个集合中是否存在所需的权限。
  • SecurityContextHolder.getContext().setAuthentication(authenticationToken): 将认证信息放入安全上下文,后续的授权检查才能生效。

4. 用户详情服务 (UserDetailsService)

UserDetailsService 是 Spring Security 加载用户认证和授权信息的核心接口。它需要返回一个 UserDetails 对象,其中包含了用户的用户名、密码以及权限。

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository; // 假设有一个用户仓库

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库或其他存储中加载用户信息
        com.example.demo.model.User user = userRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + username));

        // 核心:构建用户的权限集合
        // 假设User实体中有一个getRoles()方法返回角色列表
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        user.getRoles().forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getName())));

        // 返回Spring Security的User对象,其中包含用户名、密码和权限
        return new User(user.getEmail(), user.getPassword(), authorities);
    }
}
登录后复制

关键点:

  • user.getRoles() 或 user.getAuthorities(): 这是解决 401 问题的关键所在。UserDetailsService 必须从数据库中正确地加载用户的角色或权限信息,并将其转换为 GrantedAuthority 对象的集合。
  • SimpleGrantedAuthority: 这是一个 GrantedAuthority 的简单实现,通常用于表示角色名或权限字符串。
  • return new User(...): 返回的 User 对象(Spring Security 提供的 UserDetails 实现)必须包含正确的权限列表。如果此列表为空或不包含所需的权限,那么 hasAuthority() 检查将失败。

5. 用户登录 (AuthController)

用户通过提供凭据进行登录,成功后生成 JWT token。

import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil; // 假设有一个JWT工具类

    public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/authenticate")
    public ResponseEntity<UserResponse> loginUser(@RequestBody UserRequest request) throws Exception {
        try {
            // 使用AuthenticationManager验证用户凭据
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(request.getUserEmail(), request.getPassword()));

            // 凭据验证成功,生成JWT token
            String token = jwtUtil.generateToken(request.getUserEmail());
            System.out.println("Generated Token: " + token);
            return ResponseEntity.ok(new UserResponse(token));
        } catch (DisabledException e) {
            throw new Exception("USER_DISABLED", e);
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }
    }
}
登录后复制

关键点:

  • authenticationManager.authenticate(): 这是 Spring Security 进行认证的标准方式。它会调用 UserDetailsService 来加载用户,并使用配置的密码编码器来验证密码。
  • jwtUtil.generateToken(): 认证成功后,生成 JWT token 并返回给客户端。客户端在后续请求中携带此 token。

6. 解决 401 未授权问题的核心:权限数据

当您在 SecurityConfig 中使用 hasAuthority(Role) 保护端点时遇到 401 错误,而 permitAll() 却能正常工作,这几乎总是意味着以下问题:

最主要的原因:UserDetailsService 返回的 UserDetails 对象中,getAuthorities() 方法返回的权限集合不包含 hasAuthority() 所需的权限。

排查步骤:

  1. 检查数据库中的权限数据:

    • 确保您的用户表或关联的角色表中确实存储了用户的角色信息。例如,如果 UserRole.ADMIN.name() 是 "ADMIN",那么数据库中该用户的角色字段应该包含 "ADMIN"。
    • 检查大小写敏感性。Spring Security 默认是大小写敏感的,ADMIN 和 admin 是不同的权限。
  2. 调试 CustomUserDetailsService.loadUserByUsername() 方法:

    • 在 loadUserByUsername 方法中,打印或调试 user.getRoles() 以及最终构建的 authorities 集合。
    • 确认 authorities 集合中包含了您期望的 SimpleGrantedAuthority("ADMIN") 或其他角色。
  3. 调试 JwtRequestFilter.doFilterInternal() 方法:

    • 在 doFilterInternal 方法中,当 UserDetails usr = userDetailsService.loadUserByUsername(username); 执行后,打印 usr.getAuthorities()。
    • 确认这里返回的 UserDetails 对象的权限集合与 SecurityConfig 中 hasAuthority() 所需的权限完全匹配。
    • 如果 usr.getAuthorities() 为空或不包含正确权限,那么即使 token 有效,授权检查也会失败。

示例:假设用户表结构

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL
);

CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE user_roles (
    user_id BIGINT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (role_id) REFERENCES roles(id)
);

-- 示例数据
INSERT INTO users (email, password) VALUES ('admin@example.com', '$2a$10$YourEncodedAdminPassword');
INSERT INTO roles (name) VALUES ('ADMIN'), ('SUPPORTEXECUTIVE'), ('FIELDEXECUTIVE');
INSERT INTO user_roles (user_id, role_id) VALUES (1, 1); -- 给admin@example.com分配ADMIN角色
登录后复制

确保您的 UserRepository 和 User 实体能够正确地加载 User 及其关联的 Role。

7. 注意事项与最佳实践

  • 密码编码器: 始终使用 BCryptPasswordEncoder 或其他强大的密码编码器来存储用户密码。
  • 错误处理: 为 JwtAuthenticationEntryPoint 提供清晰的错误响应,以便客户端能够理解认证失败的原因。
  • JWT 密钥管理: 生产环境中,JWT 的密钥应该安全存储,并定期轮换。
  • 权限命名: 保持权限命名的一致性,推荐使用大写字符串,例如 ROLE_ADMIN 或 ADMIN。如果使用 hasRole(),Spring Security 会自动添加 ROLE_ 前缀,而 hasAuthority() 则直接匹配。
  • 日志记录: 在过滤器和 UserDetailsService 中添加详细的日志,有助于在开发和生产环境中快速定位问题。

总结

解决 Spring Boot JWT 角色权限控制中 401 未授权问题的关键在于确保 UserDetailsService 能够正确地从数据源加载用户的权限信息,并将其封装到 UserDetails 对象中。这些权限随后会被 JwtRequestFilter 用于构建 Authentication 对象,最终由 Spring Security 的授权管理器进行匹配。通过仔细检查数据库中的权限数据、调试 UserDetailsService 和 JwtRequestFilter 的权限加载过程,您将能够有效地诊断并解决此类授权问题。

以上就是Spring Boot JWT 角色权限控制:解决 401 未授权问题的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号