Spring_Security_JWT权限认证完整指南.md 65 KB

Spring Security + JWT 权限认证完整指南

目录


1. 系统架构概述

1.1 技术栈

  • Spring Boot 2.2.7
  • Spring Security 5.x
  • JWT (JSON Web Token)
  • Redis - 存储Token
  • MySQL 8.0 - 存储用户、角色、权限数据
  • MyBatis-Plus - 数据库操作

1.2 权限控制级别

本系统实现了三级权限控制

  1. 接口级别 - URL路径访问控制
  2. 方法级别 - 使用 @PreAuthorize 注解
  3. 按钮级别 - 前端根据权限标识显示/隐藏按钮

1.3 认证流程架构图

┌─────────────┐      ┌──────────────┐      ┌────────────────┐
│   客户端    │─────>│  登录接口    │─────>│ Authentication │
│             │      │ /auth/login  │      │    Manager     │
└─────────────┘      └──────────────┘      └────────────────┘
                            ↓                       ↓
                            ↓              ┌────────────────┐
                            ↓              │ UserDetails    │
                            ↓              │    Service     │
                            ↓              └────────────────┘
                            ↓                       ↓
                     ┌──────────────┐              ↓
                     │  生成JWT     │      ┌────────────────┐
                     │  Token       │      │  查询用户      │
                     └──────────────┘      │  角色+权限     │
                            ↓              └────────────────┘
                            ↓                       ↓
                     ┌──────────────┐              ↓
                     │ 存储Token到  │<─────────────┘
                     │   Redis      │
                     └──────────────┘
                            ↓
                     ┌──────────────┐
                     │ 返回Token给  │
                     │   客户端     │
                     └──────────────┘

2. 数据库表设计

2.1 RBAC权限模型

本系统采用经典的 RBAC (Role-Based Access Control) 模型:

  • 用户表 (sys_user)
  • 角色表 (sys_role)
  • 菜单/权限表 (sys_menu)
  • 用户-角色关联表 (sys_user_role)
  • 角色-菜单关联表 (sys_role_menu)

2.2 完整SQL脚本

-- ====================================
-- 用户表
-- ====================================
CREATE TABLE `sys_user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` VARCHAR(50) NOT NULL COMMENT '用户名',
  `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
  `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
  `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
  `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
  `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
  `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
  `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

-- ====================================
-- 角色表
-- ====================================
CREATE TABLE `sys_role` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
  `role_key` VARCHAR(50) NOT NULL COMMENT '角色权限字符串',
  `role_sort` INT(4) NOT NULL DEFAULT 0 COMMENT '显示顺序',
  `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
  `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表';

-- ====================================
-- 菜单/权限表(支持2级菜单 + 按钮权限)
-- ====================================
CREATE TABLE `sys_menu` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称',
  `parent_id` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '父菜单ID,0表示顶级菜单',
  `menu_type` CHAR(1) NOT NULL COMMENT '菜单类型:M-目录,C-菜单,F-按钮',
  `path` VARCHAR(200) DEFAULT NULL COMMENT '路由地址',
  `component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径',
  `perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如:system:user:add)',
  `icon` VARCHAR(100) DEFAULT NULL COMMENT '菜单图标',
  `order_num` INT(4) NOT NULL DEFAULT 0 COMMENT '显示顺序',
  `visible` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可见:0-隐藏,1-显示',
  `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
  `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表';

-- ====================================
-- 用户-角色关联表
-- ====================================
CREATE TABLE `sys_user_role` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
  `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_role` (`user_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- ====================================
-- 角色-菜单关联表
-- ====================================
CREATE TABLE `sys_role_menu` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
  `menu_id` BIGINT(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';

-- ====================================
-- 初始化数据
-- ====================================
-- 插入超级管理员用户(密码: admin123,已BCrypt加密)
INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `status`)
VALUES (1, 'admin', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZySmDu', '超级管理员', 1);

-- 插入普通用户(密码: user123,已BCrypt加密)
INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `status`)
VALUES (2, 'user', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZySmDu', '普通用户', 1);

-- 插入角色
INSERT INTO `sys_role` (`id`, `role_name`, `role_key`, `role_sort`)
VALUES (1, '超级管理员', 'admin', 1);

INSERT INTO `sys_role` (`id`, `role_name`, `role_key`, `role_sort`)
VALUES (2, '普通用户', 'user', 2);

-- 插入一级菜单(目录)
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `icon`, `order_num`, `perms`)
VALUES (1, '系统管理', 0, 'M', '/system', 'system', 1, NULL);

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `icon`, `order_num`, `perms`)
VALUES (2, '用户中心', 0, 'M', '/user', 'user', 2, NULL);

-- 插入二级菜单
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (101, '用户管理', 1, 'C', '/system/user', 'system/user/index', 1, 'system:user:list');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (102, '角色管理', 1, 'C', '/system/role', 'system/role/index', 2, 'system:role:list');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (103, '菜单管理', 1, 'C', '/system/menu', 'system/menu/index', 3, 'system:menu:list');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (201, '个人信息', 2, 'C', '/user/profile', 'user/profile/index', 1, 'user:profile:view');

-- 插入按钮权限(F类型 - 按钮级别权限控制)
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1011, '用户新增', 101, 'F', 1, 'system:user:add');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1012, '用户修改', 101, 'F', 2, 'system:user:edit');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1013, '用户删除', 101, 'F', 3, 'system:user:remove');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1021, '角色新增', 102, 'F', 1, 'system:role:add');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1022, '角色修改', 102, 'F', 2, 'system:role:edit');

INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1023, '角色删除', 102, 'F', 3, 'system:role:remove');

-- 用户-角色关联
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (2, 2);

-- 角色-菜单关联(超级管理员拥有所有权限)
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES
(1, 1), (1, 2), (1, 101), (1, 102), (1, 103), (1, 201),
(1, 1011), (1, 1012), (1, 1013), (1, 1021), (1, 1022), (1, 1023);

-- 普通用户只有查看权限
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES (2, 2), (2, 201);

2.3 权限标识命名规范

权限标识(perms字段)采用 模块:功能:操作 三段式命名:

权限标识 说明 示例
system:user:list 用户列表查看 查询用户列表接口
system:user:add 用户新增 新增用户按钮
system:user:edit 用户编辑 编辑用户按钮
system:user:remove 用户删除 删除用户按钮
system:user:export 用户导出 导出Excel按钮

3. Spring Security认证流程

3.1 完整认证流程图

用户登录流程:
┌──────────────────────────────────────────────────────────────────┐
│                         1. 用户发起登录请求                       │
│                    POST /auth/login                               │
│                    { username, password }                         │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                    2. Controller接收请求                          │
│              SysUserController.login()                            │
│         创建 UsernamePasswordAuthenticationToken                  │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                 3. AuthenticationManager 认证                     │
│         调用 authenticate(authenticationToken)                    │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│              4. DaoAuthenticationProvider 处理                    │
│    - 调用 UserDetailsService.loadUserByUsername()                │
│    - 获取用户详情(包含密码、角色、权限)                        │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│           5. UserDetailsServiceImpl.loadUserByUsername()          │
│    ┌──────────────────────────────────────┐                      │
│    │ a. 查询用户基本信息                  │                      │
│    │    getUserWithRolesAndPermissions()   │                      │
│    └──────────────────┬───────────────────┘                      │
│                       ↓                                           │
│    ┌──────────────────────────────────────┐                      │
│    │ b. 查询用户权限列表                  │                      │
│    │    getPermissionsByUserId()           │                      │
│    └──────────────────┬───────────────────┘                      │
│                       ↓                                           │
│    ┌──────────────────────────────────────┐                      │
│    │ c. 构建 LoginUser 对象              │                      │
│    │    包含:用户信息 + 权限列表         │                      │
│    └──────────────────────────────────────┘                      │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                      6. 密码校验                                  │
│    PasswordEncoder.matches(rawPassword, encodedPassword)          │
│    ✓ 匹配成功 → 继续                                             │
│    ✗ 匹配失败 → 抛出 BadCredentialsException                     │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                   7. 认证成功,生成Token                          │
│    ┌──────────────────────────────────────┐                      │
│    │ a. 生成 JWT Token                    │                      │
│    │    JwtUtil.generateToken()            │                      │
│    │    包含:userId, username, 过期时间   │                      │
│    └──────────────────┬───────────────────┘                      │
│                       ↓                                           │
│    ┌──────────────────────────────────────┐                      │
│    │ b. 将Token存储到Redis                │                      │
│    │    key: token:userId                  │                      │
│    │    value: token                       │                      │
│    │    expire: 24小时                     │                      │
│    └──────────────────────────────────────┘                      │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                    8. 返回Token给客户端                           │
│    { success: true, token: "xxx", userInfo: {...} }              │
└──────────────────────────────────────────────────────────────────┘


后续请求认证流程:
┌──────────────────────────────────────────────────────────────────┐
│                   1. 客户端发起业务请求                           │
│            Header: Authorization: Bearer {token}                  │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                2. JwtAuthenticationFilter 拦截                    │
│    ┌──────────────────────────────────────┐                      │
│    │ a. 从请求头获取Token                 │                      │
│    │    Authorization: Bearer {token}      │                      │
│    └──────────────────┬───────────────────┘                      │
│                       ↓                                           │
│    ┌──────────────────────────────────────┐                      │
│    │ b. 验证Token有效性                   │                      │
│    │    JwtUtil.validateToken()            │                      │
│    └──────────────────┬───────────────────┘                      │
│                       ↓                                           │
│    ┌──────────────────────────────────────┐                      │
│    │ c. 从Redis获取LoginUser              │                      │
│    │    TokenService.getLoginUser()        │                      │
│    └──────────────────┬───────────────────┘                      │
│                       ↓                                           │
│    ┌──────────────────────────────────────┐                      │
│    │ d. 将LoginUser设置到SecurityContext  │                      │
│    │    包含用户信息和权限列表             │                      │
│    └──────────────────────────────────────┘                      │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                  3. FilterSecurityInterceptor                     │
│             检查用户是否有访问当前URL的权限                       │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                    4. 方法级别权限验证                            │
│              @PreAuthorize("hasAuthority('xxx')")                │
│              检查用户是否有执行该方法的权限                       │
└────────────────────────┬─────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────────────┐
│                  5. 执行业务逻辑,返回结果                        │
└──────────────────────────────────────────────────────────────────┘

3.2 核心过滤器链顺序

Spring Security的过滤器链按以下顺序执行:

1. SecurityContextPersistenceFilter      - 创建SecurityContext
2. HeaderWriterFilter                    - 添加安全响应头
3. LogoutFilter                          - 处理登出请求
4. JwtAuthenticationFilter              - JWT认证过滤器(自定义)
5. RequestCacheAwareFilter               - 恢复被中断的请求
6. SecurityContextHolderAwareRequestFilter - 包装请求对象
7. AnonymousAuthenticationFilter         - 匿名认证
8. SessionManagementFilter               - Session管理
9. ExceptionTranslationFilter            - 异常转换
10. FilterSecurityInterceptor            - 权限验证

4. 核心组件详解

4.1 SecurityConfig(安全配置类)

作用:Spring Security的核心配置类

关键配置项

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 1. 配置密码编码器(BCrypt)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 2. 暴露认证管理器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 3. 配置认证方式
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }

    // 4. 配置HTTP安全策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()  // 禁用CSRF(使用JWT不需要)
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
            .and()
            .authorizeRequests()
                .antMatchers("/auth/login", "/auth/logout").permitAll() // 公开接口
                .anyRequest().authenticated(); // 其他接口需要认证

        // 添加JWT过滤器
        http.addFilterBefore(jwtAuthenticationFilter,
                           UsernamePasswordAuthenticationFilter.class);
    }
}

重要说明

  • permitAll() vs anonymous()
    • permitAll():允许所有人访问(包括已认证和未认证用户)
    • anonymous():只允许匿名用户访问(已认证用户会被拒绝)
    • 登录接口必须使用 permitAll()

4.2 UserDetailsServiceImpl(用户详情服务)

作用:加载用户信息、角色和权限

实现要点

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        // 1. 查询用户(包含角色信息)
        SysUser user = userService.getUserWithRolesAndPermissions(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 2. 查询用户权限列表
        List<String> permissions = userService.getPermissionsByUserId(user.getId());

        // 3. 构建LoginUser对象(实现UserDetails接口)
        return new LoginUser(user, permissions);
    }
}

SQL查询示例

<!-- 查询用户及其角色 -->
<select id="selectUserWithRolesAndPermissions" resultMap="UserWithRolesMap">
    SELECT
        u.id, u.username, u.password, u.nickname, u.status,
        r.id AS role_id, r.role_name, r.role_key
    FROM sys_user u
    LEFT JOIN sys_user_role ur ON u.id = ur.user_id
    LEFT JOIN sys_role r ON ur.role_id = r.id
    WHERE u.username = #{username}
      AND u.del_flag = 0
      AND (r.del_flag = 0 OR r.id IS NULL)
</select>

<!-- 查询用户权限 -->
<select id="selectPermissionsByUserId" resultType="java.lang.String">
    SELECT DISTINCT m.perms
    FROM sys_user_role ur
    LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
    LEFT JOIN sys_menu m ON rm.menu_id = m.id
    WHERE ur.user_id = #{userId}
      AND m.perms IS NOT NULL
      AND m.perms != ''
      AND m.status = 1
      AND m.del_flag = 0
</select>

4.3 LoginUser(用户认证信息)

作用:封装用户详情和权限,实现UserDetails接口

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser user;               // 用户信息
    private List<String> permissions;   // 权限列表

    // 构造方法
    public LoginUser(SysUser user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    // 将权限字符串转换为GrantedAuthority
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 账户是否启用(检查status字段)
    @Override
    public boolean isEnabled() {
        return user.getStatus() == 1;
    }

    // 其他方法返回true
    @Override
    public boolean isAccountNonExpired() { return true; }

    @Override
    public boolean isAccountNonLocked() { return true; }

    @Override
    public boolean isCredentialsNonExpired() { return true; }
}

4.4 JwtAuthenticationFilter(JWT认证过滤器)

作用:拦截所有请求,验证JWT Token

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenService tokenService;

    @Autowired
    private JwtUtil jwtUtil;

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

        // 1. 从请求头获取token
        String token = getTokenFromRequest(request);

        // 2. 验证token
        if (token != null && jwtUtil.validateToken(token)) {

            // 3. 从Redis获取用户信息
            LoginUser loginUser = tokenService.getLoginUser(token);

            if (loginUser != null) {
                // 4. 刷新token有效期
                tokenService.verifyToken(loginUser);

                // 5. 创建认证对象
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        loginUser, null, loginUser.getAuthorities());

                // 6. 设置到SecurityContext
                SecurityContextHolder.getContext()
                    .setAuthentication(authentication);
            }
        }

        // 7. 继续过滤器链
        chain.doFilter(request, response);
    }

    // 从请求头提取token
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

4.5 JwtUtil(JWT工具类)

作用:生成和解析JWT Token

@Component
public class JwtUtil {

    // 密钥(生产环境应从配置文件读取)
    private String secret = "your-secret-key-must-be-very-long";

    // 过期时间(24小时)
    private long expiration = 86400000;

    // 生成Token
    public String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // 从Token中获取用户ID
    public Long getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("userId", Long.class);
    }

    // 从Token中获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("username", String.class);
    }

    // 验证Token
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 解析Token
    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }
}

4.6 TokenService(Token服务)

作用:管理Token的存储和刷新

@Service
public class TokenService {

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private JwtUtil jwtUtil;

    // Token在Redis中的前缀
    private static final String LOGIN_TOKEN_KEY = "login_tokens:";

    // 刷新时间(20分钟)
    private static final long REFRESH_TIME = 20 * 60 * 1000;

    // 过期时间(24小时)
    private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;

    /**
     * 创建Token
     */
    public String createToken(LoginUser loginUser) {
        String uuid = UUID.randomUUID().toString();
        loginUser.setToken(uuid);
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + EXPIRE_TIME);

        // 生成JWT
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", loginUser.getUser().getId());
        claims.put("username", loginUser.getUsername());
        String token = jwtUtil.generateToken(claims);

        // 存储到Redis
        String tokenKey = getTokenKey(token);
        redisCache.setCacheObject(tokenKey, loginUser, EXPIRE_TIME, TimeUnit.MILLISECONDS);

        return token;
    }

    /**
     * 获取用户信息
     */
    public LoginUser getLoginUser(String token) {
        String tokenKey = getTokenKey(token);
        return redisCache.getCacheObject(tokenKey);
    }

    /**
     * 刷新Token有效期
     */
    public void verifyToken(LoginUser loginUser) {
        long currentTime = System.currentTimeMillis();
        long expireTime = loginUser.getExpireTime();

        // 距离过期时间小于20分钟,自动刷新
        if (expireTime - currentTime <= REFRESH_TIME) {
            String tokenKey = getTokenKey(loginUser.getToken());
            redisCache.expire(tokenKey, EXPIRE_TIME, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * 删除Token
     */
    public void deleteToken(String token) {
        String tokenKey = getTokenKey(token);
        redisCache.deleteObject(tokenKey);
    }

    private String getTokenKey(String token) {
        return LOGIN_TOKEN_KEY + token;
    }
}

4.7 全局异常处理器

作用:统一处理认证和授权异常

@RestController
@ControllerAdvice
public class ServiceExceptionHandler {

    /**
     * 处理认证异常(用户名或密码错误)
     */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseBody
    public RestResult<?> handle(AuthenticationException ex) {
        ex.printStackTrace();
        return RestResult.error("用户名或密码错误");
    }

    /**
     * 处理授权异常(权限不足)
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseBody
    public RestResult<?> handle(AccessDeniedException ex) {
        ex.printStackTrace();
        return RestResult.error("没有访问权限");
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public RestResult<?> handle(Exception ex) {
        ex.printStackTrace();
        return RestResult.error("请求异常, 请稍后重试!" + ex.getMessage());
    }
}

5. 开发者实现清单

5.1 必须实现的组件(7个核心类)

✅ 1. SecurityConfig(安全配置类)

位置config/SecurityConfig.java

必须实现的方法

  • passwordEncoder() - 配置密码编码器
  • authenticationManagerBean() - 暴露认证管理器
  • configure(AuthenticationManagerBuilder) - 配置认证方式
  • configure(HttpSecurity) - 配置HTTP安全策略

关键点

  • 禁用CSRF和Session
  • 配置公开接口(登录、登出等)
  • 添加JWT过滤器
  • 启用方法级权限注解 @EnableGlobalMethodSecurity

✅ 2. UserDetailsServiceImpl(用户详情服务)

位置security/UserDetailsServiceImpl.java

必须实现的方法

  • loadUserByUsername(String username) - 加载用户信息

关键点

  • 查询用户基本信息(包含密码)
  • 查询用户权限列表
  • 返回LoginUser对象
  • 用户不存在时抛出 UsernameNotFoundException

✅ 3. LoginUser(用户认证信息)

位置security/LoginUser.java

必须实现的方法(实现UserDetails接口):

  • getAuthorities() - 返回权限列表
  • getPassword() - 返回密码
  • getUsername() - 返回用户名
  • isEnabled() - 是否启用(检查status字段)
  • isAccountNonExpired() - 账户是否过期
  • isAccountNonLocked() - 账户是否锁定
  • isCredentialsNonExpired() - 密码是否过期

关键点

  • 封装用户信息和权限列表
  • 将权限字符串转换为 GrantedAuthority

✅ 4. JwtAuthenticationFilter(JWT认证过滤器)

位置security/JwtAuthenticationFilter.java

必须实现的方法

  • doFilterInternal() - 过滤器逻辑

关键点

  • 从请求头提取Token
  • 验证Token有效性
  • 从Redis获取用户信息
  • 设置认证信息到SecurityContext
  • 继续过滤器链

✅ 5. JwtUtil(JWT工具类)

位置utils/JwtUtil.java

必须实现的方法

  • generateToken(Map<String, Object> claims) - 生成Token
  • getUserIdFromToken(String token) - 从Token获取用户ID
  • getUsernameFromToken(String token) - 从Token获取用户名
  • validateToken(String token) - 验证Token
  • parseToken(String token) - 解析Token

关键点

  • 使用HMAC-SHA512算法签名
  • 设置过期时间
  • 异常处理(ExpiredJwtException等)

✅ 6. TokenService(Token服务)

位置security/TokenService.java

必须实现的方法

  • createToken(LoginUser loginUser) - 创建Token并存储到Redis
  • getLoginUser(String token) - 从Redis获取用户信息
  • verifyToken(LoginUser loginUser) - 刷新Token有效期
  • deleteToken(String token) - 删除Token(登出时使用)

关键点

  • Token存储在Redis中,key为 login_tokens:{token}
  • 自动刷新机制:距离过期时间小于20分钟时自动续期
  • 默认过期时间24小时

✅ 7. 登录/登出Controller

位置modules/system/controller/SysUserController.java

必须实现的方法

  • login(LoginBody loginBody) - 登录接口
  • logout() - 登出接口

关键点

  • 使用 AuthenticationManager.authenticate() 进行认证
  • 认证成功后生成Token
  • 将Token存储到Redis
  • 返回Token和用户信息给客户端
  • 登出时删除Redis中的Token

5.2 数据访问层(Mapper)

✅ 8. SysUserMapper

位置modules/system/mapper/SysUserMapper.java

必须实现的方法

// 根据用户名查询用户(包含角色)
SysUser selectUserWithRolesAndPermissions(@Param("username") String username);

// 根据用户ID查询权限列表
List<String> selectPermissionsByUserId(@Param("userId") Long userId);

✅ 9. SysUserMapper.xml

位置modules/system/mapper/dao/SysUserMapper.xml

必须实现的SQL

  • 用户+角色关联查询
  • 用户权限查询(用户→角色→菜单)

5.3 服务层(Service)

✅ 10. SysUserService

位置modules/system/service/SysUserService.java

必须实现的方法

// 根据用户名查询用户(包含角色和权限)
SysUser getUserWithRolesAndPermissions(String username);

// 根据用户ID查询权限列表
List<String> getPermissionsByUserId(Long userId);

// 根据用户名查询用户(不含角色)
SysUser getUserByUsername(String username);

// 验证密码是否匹配
boolean matchesPassword(String rawPassword, String encodedPassword);

5.4 实体类(Entity)

✅ 11. SysUser

位置modules/system/entity/SysUser.java

必须包含的字段

  • id - 用户ID
  • username - 用户名
  • password - 密码(BCrypt加密)
  • status - 状态(0-禁用,1-正常)
  • roles - 角色列表(非数据库字段)
  • permissions - 权限列表(非数据库字段)

关键点

  • password 字段添加 @JsonIgnore 注解,防止返回给前端

5.5 配置文件

✅ 12. pom.xml

必须添加的依赖

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<!-- JAXB API(JDK 11+必需) -->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.1</version>
</dependency>

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

✅ 13. application.yaml

必须配置的项

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: your-password
    database: 0

logging:
  level:
    org.springframework.security: DEBUG  # 开发时开启,便于调试

5.6 完整实现清单总结

序号 组件名称 位置 类型 优先级
1 SecurityConfig config/ 配置类 🔴 必须
2 UserDetailsServiceImpl security/ 服务类 🔴 必须
3 LoginUser security/ 实体类 🔴 必须
4 JwtAuthenticationFilter security/ 过滤器 🔴 必须
5 JwtUtil utils/ 工具类 🔴 必须
6 TokenService security/ 服务类 🔴 必须
7 SysUserController controller/ 控制器 🔴 必须
8 SysUserMapper mapper/ Mapper接口 🔴 必须
9 SysUserMapper.xml mapper/dao/ MyBatis XML 🔴 必须
10 SysUserService service/ 服务接口 🔴 必须
11 SysUser entity/ 实体类 🔴 必须
12 ServiceExceptionHandler exception/ 异常处理 🟡 推荐
13 RedisCache utils/ Redis工具类 🟡 推荐

6. 代码示例

6.1 登录接口完整实现

@RestController
@RequestMapping("/auth")
public class SysUserController {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private SysUserService sysUserService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 用户名密码登录
     */
    @PostMapping("/login")
    public RestResult<?> login(@RequestBody LoginBody loginBody) {
        try {
            // 1. 使用Spring Security进行认证
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginBody.getUsername(),
                    loginBody.getPassword()
                )
            );

            // 2. 认证成功,设置认证信息
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 3. 获取用户信息
            SysUser user = sysUserService.getUserByUsername(loginBody.getUsername());

            // 4. 生成JWT Token
            Map<String, Object> claims = new HashMap<>();
            claims.put("username", user.getUsername());
            claims.put("userId", user.getId());
            String token = jwtUtil.generateToken(claims);

            // 5. 将Token存储到Redis(24小时过期)
            redisTemplate.opsForValue().set(
                "token:" + user.getId(),
                token,
                24,
                TimeUnit.HOURS
            );

            // 6. 返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("token", token);
            result.put("userInfo", user);

            return RestResult.ok("登录成功", result);

        } catch (AuthenticationException e) {
            // 认证失败
            return RestResult.error("用户名或密码错误");
        }
    }

    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public RestResult<?> logout() {
        // 1. 获取当前用户
        Authentication authentication = SecurityContextHolder.getContext()
            .getAuthentication();

        if (authentication != null) {
            // 2. 删除Redis中的Token
            String username = authentication.getName();
            SysUser user = sysUserService.getUserByUsername(username);
            if (user != null) {
                redisTemplate.delete("token:" + user.getId());
            }
        }

        // 3. 清空认证信息
        SecurityContextHolder.clearContext();

        return RestResult.ok("退出成功");
    }
}

6.2 方法级权限控制

@RestController
@RequestMapping("/system/user")
public class UserController {

    @Autowired
    private SysUserService userService;

    /**
     * 查询用户列表
     * 需要 system:user:list 权限
     */
    @GetMapping("/list")
    @PreAuthorize("hasAuthority('system:user:list')")
    public RestResult<?> list() {
        List<SysUser> users = userService.list();
        return RestResult.ok(users);
    }

    /**
     * 新增用户
     * 需要 system:user:add 权限
     */
    @PostMapping("/add")
    @PreAuthorize("hasAuthority('system:user:add')")
    public RestResult<?> add(@RequestBody SysUser user) {
        userService.save(user);
        return RestResult.ok("新增成功");
    }

    /**
     * 修改用户
     * 需要 system:user:edit 权限
     */
    @PutMapping("/edit")
    @PreAuthorize("hasAuthority('system:user:edit')")
    public RestResult<?> edit(@RequestBody SysUser user) {
        userService.updateById(user);
        return RestResult.ok("修改成功");
    }

    /**
     * 删除用户
     * 需要 system:user:remove 权限
     */
    @DeleteMapping("/remove/{id}")
    @PreAuthorize("hasAuthority('system:user:remove')")
    public RestResult<?> remove(@PathVariable Long id) {
        userService.removeById(id);
        return RestResult.ok("删除成功");
    }

    /**
     * 组合权限判断
     * 需要同时拥有 system:user:list 和 system:user:export 权限
     */
    @GetMapping("/export")
    @PreAuthorize("hasAuthority('system:user:list') and hasAuthority('system:user:export')")
    public RestResult<?> export() {
        // 导出Excel逻辑
        return RestResult.ok("导出成功");
    }

    /**
     * 角色判断
     * 需要admin角色
     */
    @GetMapping("/admin-only")
    @PreAuthorize("hasRole('admin')")
    public RestResult<?> adminOnly() {
        return RestResult.ok("管理员专属功能");
    }
}

6.3 前端按钮级权限控制

Vue示例(使用自定义指令)

// permission.js - 自定义权限指令
import store from '@/store'

export default {
  install(Vue) {
    // v-hasPermission 指令
    Vue.directive('hasPermission', {
      inserted(el, binding, vnode) {
        const { value } = binding
        const permissions = store.getters.permissions

        if (value && value instanceof Array && value.length > 0) {
          const hasPermission = permissions.some(permission => {
            return value.includes(permission)
          })

          if (!hasPermission) {
            // 没有权限,移除按钮
            el.parentNode && el.parentNode.removeChild(el)
          }
        }
      }
    })
  }
}
<!-- UserList.vue -->
<template>
  <div>
    <el-table :data="userList">
      <el-table-column prop="username" label="用户名"/>
      <el-table-column label="操作">
        <template slot-scope="scope">
          <!-- 新增按钮 - 需要 system:user:add 权限 -->
          <el-button
            v-hasPermission="['system:user:add']"
            @click="handleAdd()">
            新增
          </el-button>

          <!-- 编辑按钮 - 需要 system:user:edit 权限 -->
          <el-button
            v-hasPermission="['system:user:edit']"
            @click="handleEdit(scope.row)">
            编辑
          </el-button>

          <!-- 删除按钮 - 需要 system:user:remove 权限 -->
          <el-button
            v-hasPermission="['system:user:remove']"
            @click="handleDelete(scope.row)">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

React示例(使用Hook)

// usePermission.js
import { useSelector } from 'react-redux'

export const usePermission = () => {
  const permissions = useSelector(state => state.user.permissions)

  const hasPermission = (permission) => {
    if (Array.isArray(permission)) {
      return permission.some(p => permissions.includes(p))
    }
    return permissions.includes(permission)
  }

  return { hasPermission }
}

// UserList.jsx
import { usePermission } from '@/hooks/usePermission'

const UserList = () => {
  const { hasPermission } = usePermission()

  return (
    <div>
      <Table dataSource={userList}>
        <Column title="用户名" dataIndex="username" />
        <Column
          title="操作"
          render={(text, record) => (
            <>
              {/* 新增按钮 */}
              {hasPermission('system:user:add') && (
                <Button onClick={() => handleAdd()}>新增</Button>
              )}

              {/* 编辑按钮 */}
              {hasPermission('system:user:edit') && (
                <Button onClick={() => handleEdit(record)}>编辑</Button>
              )}

              {/* 删除按钮 */}
              {hasPermission('system:user:remove') && (
                <Button onClick={() => handleDelete(record)}>删除</Button>
              )}
            </>
          )}
        />
      </Table>
    </div>
  )
}

7. 测试指南

7.1 登录测试

# 1. 登录获取Token
curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin",
    "password": "admin123"
  }'

# 响应示例
{
  "success": true,
  "message": "登录成功",
  "code": 200,
  "result": {
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3MDAwMDAwMDB9.xxx",
    "userInfo": {
      "id": 1,
      "username": "admin",
      "nickname": "超级管理员"
    }
  },
  "timestamp": 1700000000000
}

7.2 携带Token访问受保护接口

# 2. 使用Token访问受保护的接口
curl -X GET http://localhost:8080/system/user/list \
  -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx"

# 有权限 - 返回数据
{
  "success": true,
  "code": 200,
  "result": [...]
}

# 无权限 - 返回403
{
  "success": false,
  "message": "没有访问权限",
  "code": 403
}

7.3 测试不同权限级别

# 测试超级管理员(拥有所有权限)
# 1. 以admin身份登录
TOKEN_ADMIN="eyJhbGciOiJIUzUxMiJ9.xxx"

# 2. 测试新增用户(需要 system:user:add 权限)
curl -X POST http://localhost:8080/system/user/add \
  -H "Authorization: Bearer $TOKEN_ADMIN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "test",
    "password": "123456",
    "nickname": "测试用户"
  }'
# 预期:成功


# 测试普通用户(权限有限)
# 1. 以user身份登录
TOKEN_USER="eyJhbGciOiJIUzUxMiJ9.yyy"

# 2. 测试新增用户(普通用户没有此权限)
curl -X POST http://localhost:8080/system/user/add \
  -H "Authorization: Bearer $TOKEN_USER" \
  -H "Content-Type: application/json" \
  -d '{...}'
# 预期:403 没有访问权限

7.4 退出登录测试

# 3. 退出登录
curl -X POST http://localhost:8080/auth/logout \
  -H "Authorization: Bearer $TOKEN_ADMIN"

# 响应
{
  "success": true,
  "message": "退出成功",
  "code": 200
}

# 4. 退出后再次访问受保护接口
curl -X GET http://localhost:8080/system/user/list \
  -H "Authorization: Bearer $TOKEN_ADMIN"

# 预期:401 未认证(因为Token已从Redis中删除)

7.5 Postman测试集合

创建Postman环境变量:

{
  "base_url": "http://localhost:8080",
  "token": ""
}

测试脚本(自动保存Token):

// 在登录接口的 Tests 标签页添加
if (pm.response.code === 200) {
    var jsonData = pm.response.json();
    if (jsonData.success && jsonData.result.token) {
        pm.environment.set("token", jsonData.result.token);
        console.log("Token已保存:", jsonData.result.token);
    }
}

8. 常见问题

8.1 认证问题

Q1: 登录时提示"Bad credentials",但密码确实正确?

原因

  1. 数据库中的密码不是正确的BCrypt加密值
  2. UserDetailsService.loadUserByUsername() 返回的密码为null
  3. 密码编码器配置不正确

解决方法

# 1. 测试密码是否匹配
curl -X POST "http://localhost:8080/auth/test-password" \
  -d "username=admin&password=admin123"

# 2. 生成正确的BCrypt密码
curl -X POST "http://localhost:8080/auth/generate-password" \
  -d "password=admin123"

# 3. 更新数据库密码
UPDATE sys_user SET password = '$2a$10$xxx' WHERE username = 'admin';

Q2: SecurityConfig中应该使用 permitAll() 还是 anonymous()

区别

  • permitAll():允许所有人访问(已认证 + 未认证)
  • anonymous():只允许匿名用户访问(已认证用户会被拒绝)

登录接口必须使用 permitAll(),否则认证过程会失败!

// ✓ 正确
.antMatchers("/auth/login", "/auth/logout").permitAll()

// ✗ 错误(会导致认证失败)
.antMatchers("/auth/login", "/auth/logout").anonymous()

8.2 JWT问题

Q3: JDK 11运行时报错:NoClassDefFoundError: javax/xml/bind/DatatypeConverter

原因:Java 11移除了 javax.xml.bind

解决方法:在 pom.xml 中添加JAXB依赖

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.1</version>
</dependency>

Q4: Token验证失败,但Token确实有效?

检查项

  1. JWT密钥是否一致(secret)
  2. Token是否过期
  3. Redis中是否存在该Token
  4. Token格式是否正确(Bearer + 空格 + token)
# 正确的请求头格式
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx

# 错误示例
Authorization: eyJhbGciOiJIUzUxMiJ9.xxx  # 缺少Bearer前缀
Authorization: BearereyJhbGciOiJIUzUxMiJ9.xxx  # 缺少空格

8.3 权限问题

Q5: @PreAuthorize 注解不生效?

原因:未开启方法级权限验证

解决方法:在SecurityConfig上添加注解

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
}

Q6: 用户明明有权限,但还是返回403?

检查步骤

  1. 确认权限标识是否正确(区分大小写)
  2. 确认数据库中用户-角色-权限关联是否正确
  3. 确认 getPermissionsByUserId() 是否正确返回权限列表
  4. 确认LoginUser的 getAuthorities() 方法是否正确转换权限
// 调试:打印用户权限
@GetMapping("/debug/permissions")
public RestResult<?> getMyPermissions() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) auth.getPrincipal();
    return RestResult.ok(loginUser.getPermissions());
}

8.4 Redis问题

Q7: Token生成成功,但后续请求获取不到用户信息?

原因:Redis连接或序列化问题

检查项

  1. Redis服务是否正常运行
  2. Redis连接配置是否正确
  3. RedisTemplate序列化配置
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 使用Jackson序列化
        Jackson2JsonRedisSerializer<Object> serializer =
            new Jackson2JsonRedisSerializer<>(Object.class);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        return template;
    }
}

8.5 跨域问题

Q8: 前后端分离时,登录接口报跨域错误?

解决方法:在SecurityConfig中配置CORS

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .cors()  // 启用CORS
        .and()
        .csrf().disable()
        // ...
}

// 配置CORS规则
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);

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

9. 最佳实践

9.1 密码安全

  1. 永远使用BCrypt加密密码 ```java // ✓ 正确 String encodedPassword = passwordEncoder.encode(rawPassword);

// ✗ 错误 String encodedPassword = MD5(rawPassword); // MD5不安全


2. **密码字段添加@JsonIgnore**
   ```java
   @JsonIgnore
   private String password;
  1. 密码复杂度要求
    • 最小长度8位
    • 包含大小写字母、数字、特殊字符
    • 定期更换密码

9.2 Token安全

  1. Token存储位置

    • ✓ 前端:localStorage 或 sessionStorage
    • ✗ 前端:Cookie(容易被CSRF攻击)
    • ✓ 后端:Redis(可快速失效)
  2. Token刷新策略

    // 距离过期时间小于20分钟时自动刷新
    if (expireTime - currentTime <= 20 * 60 * 1000) {
       redisCache.expire(tokenKey, 24 * 60 * 60 * 1000);
    }
    
    1. Token密钥管理

      # application.yaml - 生产环境从环境变量读取
      jwt:
      secret: ${JWT_SECRET:your-default-secret-key}
      expiration: 86400000  # 24小时
      

9.3 权限设计

  1. 权限粒度

    • 粗粒度:模块级(system:user)
    • 中粒度:功能级(system:user:list)
    • 细粒度:按钮级(system:user:add)
  2. 权限命名规范

    模块:功能:操作
    system:user:add      # 系统管理-用户管理-新增
    system:user:edit     # 系统管理-用户管理-编辑
    report:sales:export  # 报表管理-销售报表-导出
    
  3. 数据权限控制

    // 根据用户所属部门过滤数据
    @DataScope(deptAlias = "d", userAlias = "u")
    @PreAuthorize("hasAuthority('system:user:list')")
    public List<SysUser> list() {
       return userService.selectUserList();
    }
    

    9.4 日志记录

    @Aspect
    @Component
    public class LoginLogAspect {
    
    @Around("@annotation(com.xxx.annotation.LoginLog)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
    
        try {
            Object result = joinPoint.proceed();
            long time = System.currentTimeMillis() - startTime;
    
            // 记录登录成功日志
            saveLog(joinPoint, time, "SUCCESS");
    
            return result;
        } catch (Exception e) {
            // 记录登录失败日志
            saveLog(joinPoint, 0, "FAIL");
            throw e;
        }
    }
    }
    

10. 性能优化

10.1 Redis优化

  1. 使用Redis Pipeline批量操作

    redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
       connection.set("key1".getBytes(), "value1".getBytes());
       connection.set("key2".getBytes(), "value2".getBytes());
       return null;
    });
    
    1. 合理设置过期时间
    2. Token:24小时
    3. 验证码:5分钟
    4. 短期缓存:1小时

    10.2 数据库优化

    1. 为常用查询添加索引 ```sql -- 用户名索引 CREATE UNIQUE INDEX idx_username ON sys_user(username);

    -- 用户-角色关联索引 CREATE INDEX idx_user_id ON sys_user_role(user_id); CREATE INDEX idx_role_id ON sys_user_role(role_id); ```

  2. 使用MyBatis二级缓存

    <!-- SysUserMapper.xml -->
    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
    

    11. 附录

    11.1 完整项目结构

    ```

pacsonline_new/ ├── src/main/java/com/zskk/pacsonline/ │ ├── config/ │ │ ├── SecurityConfig.java # Security配置 │ │ └── RedisConfig.java # Redis配置 │ ├── security/ │ │ ├── UserDetailsServiceImpl.java # 用户详情服务 │ │ ├── LoginUser.java # 用户认证信息 │ │ ├── JwtAuthenticationFilter.java # JWT过滤器 │ │ └── TokenService.java # Token服务 │ ├── utils/ │ │ ├── JwtUtil.java # JWT工具类 │ │ └── RedisCache.java # Redis工具类 │ ├── modules/system/ │ │ ├── controller/ │ │ │ └── SysUserController.java # 用户控制器 │ │ ├── service/ │ │ │ ├── SysUserService.java # 用户服务接口 │ │ │ └── impl/ │ │ │ └── SysUserServiceImpl.java │ │ ├── mapper/ │ │ │ ├── SysUserMapper.java # 用户Mapper接口 │ │ │ └── dao/ │ │ │ └── SysUserMapper.xml # MyBatis XML │ │ └── entity/ │ │ ├── SysUser.java # 用户实体 │ │ ├── SysRole.java # 角色实体 │ │ └── SysMenu.java # 菜单实体 │ └── component/ │ └── exception/ │ └── ServiceExceptionHandler.java # 全局异常处理 ├── src/main/resources/ │ ├── application.yaml # 配置文件 │ └── mapper/ └── doc/

└── security_tables.sql              # 数据库表SQL

```

11.2 参考资料


12. 总结

本文档详细介绍了基于Spring Security + JWT的权限认证系统的完整实现方案,涵盖了从数据库设计到代码实现的所有关键环节。

核心要点回顾

  1. 7个核心类必须实现:SecurityConfig、UserDetailsServiceImpl、LoginUser、JwtAuthenticationFilter、JwtUtil、TokenService、登录Controller

  2. RBAC权限模型:用户-角色-权限三级关联

  3. 三级权限控制:接口级(URL)、方法级(@PreAuthorize)、按钮级(前端控制)

  4. 关键配置

    • 登录接口使用 permitAll() 而非 anonymous()
    • JDK 11+ 需要添加JAXB依赖
    • 密码必须使用BCrypt加密
    • Token存储在Redis中,支持自动刷新
  5. 安全最佳实践

    • 密码加@JsonIgnore注解
    • Token密钥从环境变量读取
    • 记录登录日志
    • 合理设置Token过期时间

遵循本文档的实现方案,可以快速搭建一个安全、可靠、易扩展的权限认证系统。


文档版本:v1.0 最后更新:2025-10-29 作者:gengjunfang 项目地址:pacsonline_new