【JAVA 进阶】SpringBoot集成Sa-Token权限校验框架深度解析

文章目录

    • 引言
    • 第一章:SA-Token框架概述与核心特性
      • [1.1 SA-Token简介与设计理念](#1.1 SA-Token简介与设计理念)
        • [1.1.1 什么是SA-Token](#1.1.1 什么是SA-Token)
        • [1.1.2 SA-Token的设计理念](#1.1.2 SA-Token的设计理念)
      • [1.2 SA-Token核心特性详解](#1.2 SA-Token核心特性详解)
        • [1.2.1 登录认证特性](#1.2.1 登录认证特性)
        • [1.2.2 权限认证特性](#1.2.2 权限认证特性)
        • [1.2.3 会话管理特性](#1.2.3 会话管理特性)
      • [1.3 SA-Token架构设计](#1.3 SA-Token架构设计)
        • [1.3.1 核心组件架构](#1.3.1 核心组件架构)
        • [1.3.2 扩展机制设计](#1.3.2 扩展机制设计)
    • 第二章:SpringBoot集成SA-Token基础配置
      • [2.1 项目环境搭建](#2.1 项目环境搭建)
        • [2.1.1 Maven依赖配置](#2.1.1 Maven依赖配置)
        • [2.1.2 应用配置文件](#2.1.2 应用配置文件)
        • [2.1.3 数据库表结构设计](#2.1.3 数据库表结构设计)
      • [2.2 核心配置类实现](#2.2 核心配置类实现)
        • [2.2.1 SA-Token配置类](#2.2.1 SA-Token配置类)
        • [2.2.2 权限认证接口实现](#2.2.2 权限认证接口实现)
        • [2.2.3 全局异常处理器](#2.2.3 全局异常处理器)
      • [2.3 实体类和数据访问层](#2.3 实体类和数据访问层)
        • [2.3.1 实体类定义](#2.3.1 实体类定义)
        • [2.3.2 数据访问层实现](#2.3.2 数据访问层实现)
      • [2.4 业务服务层实现](#2.4 业务服务层实现)
        • [2.4.1 用户服务实现](#2.4.1 用户服务实现)
        • [2.4.2 角色服务实现](#2.4.2 角色服务实现)
        • [2.4.3 权限服务实现](#2.4.3 权限服务实现)
    • 第三章:权限认证与授权实战
      • [3.1 登录认证实现](#3.1 登录认证实现)
        • [3.1.1 登录控制器实现](#3.1.1 登录控制器实现)
        • [3.1.2 验证码服务实现](#3.1.2 验证码服务实现)
        • [4.1.2 SSO-Server端实现](#4.1.2 SSO-Server端实现)
        • [4.1.3 SSO-Client端实现](#4.1.3 SSO-Client端实现)
      • [4.2 OAuth2.0集成](#4.2 OAuth2.0集成)
        • [4.2.1 OAuth2配置](#4.2.1 OAuth2配置)
      • [4.3 微服务网关鉴权](#4.3 微服务网关鉴权)
        • [4.3.1 网关鉴权配置](#4.3.1 网关鉴权配置)
      • [4.4 多账户体系支持](#4.4 多账户体系支持)
        • [4.4.1 多账户配置](#4.4.1 多账户配置)
    • 第五章:生产环境最佳实践
      • [5.1 性能优化策略](#5.1 性能优化策略)
        • [5.1.1 Token存储优化](#5.1.1 Token存储优化)
        • [5.1.2 权限缓存优化](#5.1.2 权限缓存优化)
      • [5.2 安全加固措施](#5.2 安全加固措施)
        • [5.2.1 Token安全增强](#5.2.1 Token安全增强)
        • [5.2.2 防攻击策略](#5.2.2 防攻击策略)
      • [5.3 监控与日志](#5.3 监控与日志)
        • [5.3.1 认证监控](#5.3.1 认证监控)
        • [5.3.2 审计日志](#5.3.2 审计日志)
    • 第六章:总结与展望
      • [6.1 知识点回顾](#6.1 知识点回顾)
        • [6.1.1 核心概念与特性](#6.1.1 核心概念与特性)
        • [6.1.2 技术架构要点](#6.1.2 技术架构要点)
        • [6.1.3 实战应用总结](#6.1.3 实战应用总结)
      • [6.2 最佳实践总结](#6.2 最佳实践总结)
        • [6.2.1 开发规范](#6.2.1 开发规范)
        • [6.2.2 性能优化建议](#6.2.2 性能优化建议)
        • [6.2.3 安全防护要点](#6.2.3 安全防护要点)
      • [6.3 技术发展趋势](#6.3 技术发展趋势)
        • [6.3.1 云原生权限管理](#6.3.1 云原生权限管理)
        • [6.3.2 零信任安全架构](#6.3.2 零信任安全架构)
        • [6.3.3 AI驱动的智能权限](#6.3.3 AI驱动的智能权限)
      • [6.4 扩展阅读与学习资源](#6.4 扩展阅读与学习资源)
        • [6.4.1 官方文档与社区](#6.4.1 官方文档与社区)
        • [6.4.2 相关技术书籍](#6.4.2 相关技术书籍)
        • [6.4.3 在线学习资源](#6.4.3 在线学习资源)
      • [6.5 实践练习与思考](#6.5 实践练习与思考)
        • [6.5.1 动手实践项目](#6.5.1 动手实践项目)
        • [6.5.2 思考讨论题](#6.5.2 思考讨论题)
        • [6.5.3 开源贡献](#6.5.3 开源贡献)
      • [6.6 结语](#6.6 结语)

引言

在现代Web应用开发中,权限管理是一个不可或缺的核心功能。传统的权限框架如Spring Security虽然功能强大,但配置复杂、学习成本高,对于中小型项目来说往往显得过于臃肿。SA-Token作为一个轻量级的Java权限认证框架,以其简洁的API设计、丰富的功能特性和极低的学习成本,正在成为越来越多开发者的首选。

SA-Token(Simple And Token)是一个轻量级Java权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权等一系列权限相关问题。它以简单、强大、优雅为设计理念,让权限认证变得简单而不失灵活。

本文将从SA-Token的基础概念出发,深入探讨其在SpringBoot项目中的集成方案,通过丰富的代码示例和实战案例,帮助读者全面掌握SA-Token的使用技巧和最佳实践。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的技术洞察和实践指导。

第一章:SA-Token框架概述与核心特性

1.1 SA-Token简介与设计理念

1.1.1 什么是SA-Token

SA-Token是一个轻量级Java权限认证框架,专注于解决Web应用中的权限认证问题。与传统的重量级框架不同,SA-Token采用了更加简洁直观的API设计,让开发者能够快速上手并高效开发。

java 复制代码
// SA-Token的核心理念:简单即是美
// 登录用户
StpUtil.login(userId);

// 检查登录状态
StpUtil.checkLogin();

// 获取当前用户ID
Object userId = StpUtil.getLoginId();

// 注销登录
StpUtil.logout();
1.1.2 SA-Token的设计理念

SA-Token的设计遵循以下核心理念:

  1. 简单性:API设计简洁明了,学习成本低
  2. 灵活性:支持多种认证模式和扩展机制
  3. 高性能:轻量级设计,运行效率高
  4. 易集成:与主流框架无缝集成
java 复制代码
/**
 * SA-Token设计理念体现
 */
@RestController
public class AuthController {
    
    /**
     * 用户登录 - 体现简单性
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest request) {
        // 验证用户名密码(省略具体实现)
        User user = userService.authenticate(request.getUsername(), request.getPassword());
        
        if (user != null) {
            // 一行代码完成登录
            StpUtil.login(user.getId());
            
            // 获取Token信息
            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
            
            return Result.success(tokenInfo);
        }
        
        return Result.error("用户名或密码错误");
    }
    
    /**
     * 获取用户信息 - 体现灵活性
     */
    @GetMapping("/userinfo")
    public Result getUserInfo() {
        // 检查登录状态,未登录会抛出异常
        StpUtil.checkLogin();
        
        // 获取当前登录用户ID
        Object loginId = StpUtil.getLoginId();
        
        // 获取用户详细信息
        User user = userService.getById(loginId);
        
        return Result.success(user);
    }
    
    /**
     * 需要特定权限的接口 - 体现权限控制的简洁性
     */
    @GetMapping("/admin/users")
    @SaCheckPermission("user:list") // 注解方式权限校验
    public Result getUserList() {
        List<User> users = userService.getAllUsers();
        return Result.success(users);
    }
}

1.2 SA-Token核心特性详解

1.2.1 登录认证特性

SA-Token提供了完整的登录认证解决方案,支持多种登录模式和会话管理策略。

java 复制代码
/**
 * 登录认证核心特性演示
 */
@Service
public class AuthService {
    
    /**
     * 基础登录功能
     */
    public void basicLogin(Long userId) {
        // 基础登录
        StpUtil.login(userId);
        
        // 指定设备登录
        StpUtil.login(userId, "PC");
        
        // 登录并指定Token有效期(单位:秒)
        StpUtil.login(userId, 3600);
        
        // 登录时携带扩展信息
        StpUtil.login(userId, new SaLoginModel()
                .setDevice("mobile")
                .setTimeout(7200)
                .setIsLastingCookie(true));
    }
    
    /**
     * 会话查询功能
     */
    public void sessionQuery() {
        // 获取当前登录用户ID
        Object loginId = StpUtil.getLoginId();
        
        // 获取当前登录用户ID,并转换为指定类型
        Long userId = StpUtil.getLoginIdAsLong();
        String userIdStr = StpUtil.getLoginIdAsString();
        
        // 获取当前登录设备
        String device = StpUtil.getLoginDevice();
        
        // 获取Token剩余有效时间(单位:秒)
        long timeout = StpUtil.getTokenTimeout();
        
        // 获取Token信息
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        System.out.println("Token名称:" + tokenInfo.getTokenName());
        System.out.println("Token值:" + tokenInfo.getTokenValue());
        System.out.println("是否登录:" + tokenInfo.getIsLogin());
        System.out.println("登录ID:" + tokenInfo.getLoginId());
        System.out.println("登录类型:" + tokenInfo.getLoginType());
        System.out.println("Token超时时间:" + tokenInfo.getTokenTimeout());
    }
    
    /**
     * 登录状态检查
     */
    public void loginCheck() {
        // 检查当前是否登录,如未登录则抛出异常
        StpUtil.checkLogin();
        
        // 检查当前是否登录,返回boolean值
        boolean isLogin = StpUtil.isLogin();
        
        // 检查指定用户是否登录
        boolean userLogin = StpUtil.isLogin(10001);
        
        // 检查当前Token是否有效
        boolean isValid = StpUtil.getTokenInfo().getIsLogin();
    }
    
    /**
     * 注销登录功能
     */
    public void logout() {
        // 注销当前用户登录
        StpUtil.logout();
        
        // 注销指定用户登录
        StpUtil.logout(10001);
        
        // 注销指定用户在指定设备的登录
        StpUtil.logout(10001, "PC");
        
        // 踢掉指定用户下线
        StpUtil.kickout(10001);
        
        // 踢掉指定用户在指定设备下线
        StpUtil.kickout(10001, "mobile");
    }
}
1.2.2 权限认证特性

SA-Token提供了灵活的权限认证机制,支持基于角色和权限的访问控制。

java 复制代码
/**
 * 权限认证特性演示
 */
@Service
public class PermissionService {
    
    /**
     * 权限校验
     */
    public void permissionCheck() {
        // 检查当前用户是否拥有指定权限
        StpUtil.checkPermission("user:add");
        
        // 检查当前用户是否拥有指定权限,返回boolean
        boolean hasPermission = StpUtil.hasPermission("user:delete");
        
        // 检查当前用户是否拥有指定权限列表中的任意一个
        StpUtil.checkPermissionOr("user:add", "user:edit", "user:delete");
        
        // 检查当前用户是否拥有指定权限列表中的所有权限
        StpUtil.checkPermissionAnd("user:add", "role:add");
    }
    
    /**
     * 角色校验
     */
    public void roleCheck() {
        // 检查当前用户是否拥有指定角色
        StpUtil.checkRole("admin");
        
        // 检查当前用户是否拥有指定角色,返回boolean
        boolean hasRole = StpUtil.hasRole("admin");
        
        // 检查当前用户是否拥有指定角色列表中的任意一个
        StpUtil.checkRoleOr("admin", "manager", "operator");
        
        // 检查当前用户是否拥有指定角色列表中的所有角色
        StpUtil.checkRoleAnd("admin", "manager");
    }
    
    /**
     * 获取权限和角色信息
     */
    public void getPermissionInfo() {
        // 获取当前用户的权限列表
        List<String> permissions = StpUtil.getPermissionList();
        
        // 获取当前用户的角色列表
        List<String> roles = StpUtil.getRoleList();
        
        // 获取指定用户的权限列表
        List<String> userPermissions = StpUtil.getPermissionList(10001);
        
        // 获取指定用户的角色列表
        List<String> userRoles = StpUtil.getRoleList(10001);
        
        System.out.println("当前用户权限:" + permissions);
        System.out.println("当前用户角色:" + roles);
    }
}
1.2.3 会话管理特性

SA-Token提供了强大的会话管理功能,支持会话存储、会话共享、会话监听等特性。

java 复制代码
/**
 * 会话管理特性演示
 */
@Service
public class SessionService {
    
    /**
     * Session存储操作
     */
    public void sessionStorage() {
        // 获取当前用户的Session对象
        SaSession session = StpUtil.getSession();
        
        // 在Session中存储数据
        session.set("username", "张三");
        session.set("email", "zhangsan@example.com");
        session.set("loginTime", System.currentTimeMillis());
        
        // 从Session中获取数据
        String username = session.get("username", String.class);
        String email = (String) session.get("email");
        
        // 获取Session中的所有key
        Set<String> keys = session.keys();
        
        // 删除Session中的指定数据
        session.delete("email");
        
        // 清空Session
        session.clear();
        
        // 获取Session的剩余存活时间
        long timeout = session.getTimeout();
        
        // 修改Session的存活时间
        session.updateTimeout(3600);
    }
    
    /**
     * Token-Session双Token模式
     */
    public void tokenSessionMode() {
        // 获取Token-Session(专门存储业务数据的Session)
        SaSession tokenSession = StpUtil.getTokenSession();
        
        // 在Token-Session中存储数据
        tokenSession.set("currentProject", "SA-Token集成项目");
        tokenSession.set("theme", "dark");
        
        // Token-Session与User-Session的区别:
        // User-Session: 以用户为单位,同一用户的多次登录共享同一个Session
        // Token-Session: 以Token为单位,每个Token都有自己独立的Session
        
        // 获取User-Session
        SaSession userSession = StpUtil.getSession();
        userSession.set("userInfo", "这是用户级别的数据");
        
        // 获取指定用户的Session
        SaSession specificUserSession = StpUtil.getSessionByLoginId(10001);
        specificUserSession.set("lastLoginTime", System.currentTimeMillis());
    }
    
    /**
     * 自定义Session操作
     */
    public void customSession() {
        // 获取自定义Session
        SaSession customSession = SaSessionCustomUtil.getSessionById("custom-session-001");
        
        // 在自定义Session中存储数据
        customSession.set("customData", "这是自定义Session数据");
        
        // 设置自定义Session的存活时间
        customSession.updateTimeout(1800);
        
        // 删除自定义Session
        SaSessionCustomUtil.deleteSessionById("custom-session-001");
        
        // 获取所有自定义Session的ID列表
        List<String> sessionIds = SaSessionCustomUtil.searchSessionId("custom-*", 0, 100, true);
    }
}

1.3 SA-Token架构设计

1.3.1 核心组件架构

SA-Token采用模块化设计,核心组件包括:

java 复制代码
/**
 * SA-Token核心组件架构演示
 */
public class SaTokenArchitecture {
    
    /**
     * 1. StpLogic - 权限认证逻辑核心
     */
    public void stpLogicDemo() {
        // StpLogic是SA-Token的核心逻辑类
        // 所有的登录、权限校验等操作都通过StpLogic实现
        
        // 获取默认的StpLogic实例
        StpLogic stpLogic = StpUtil.stpLogic;
        
        // 使用StpLogic进行登录
        stpLogic.login(10001);
        
        // 使用StpLogic进行权限校验
        stpLogic.checkPermission("user:add");
        
        // 自定义StpLogic实现多账户体系
        StpLogic adminLogic = new StpLogic("admin");
        StpLogic userLogic = new StpLogic("user");
        
        // 管理员登录
        adminLogic.login(1001);
        
        // 普通用户登录
        userLogic.login(2001);
    }
    
    /**
     * 2. SaTokenDao - 数据持久化接口
     */
    public void saTokenDaoDemo() {
        // SaTokenDao负责Token和Session的持久化
        // 默认实现:SaTokenDaoDefaultImpl(基于内存)
        
        // 获取当前使用的Dao实例
        SaTokenDao dao = SaManager.getSaTokenDao();
        
        // 存储Token
        dao.set("token:abc123", "user:10001", 3600);
        
        // 获取Token对应的值
        String value = dao.get("token:abc123");
        
        // 删除Token
        dao.delete("token:abc123");
        
        // 获取Token剩余存活时间
        long timeout = dao.getTimeout("token:abc123");
        
        // 修改Token存活时间
        dao.updateTimeout("token:abc123", 7200);
    }
    
    /**
     * 3. SaTokenConfig - 全局配置类
     */
    public void saTokenConfigDemo() {
        // 获取全局配置对象
        SaTokenConfig config = SaManager.getConfig();
        
        // 查看配置信息
        System.out.println("Token名称:" + config.getTokenName());
        System.out.println("Token超时时间:" + config.getTimeout());
        System.out.println("是否允许同一账号并发登录:" + config.getIsConcurrent());
        System.out.println("是否共享Token:" + config.getIsShare());
        System.out.println("Token风格:" + config.getTokenStyle());
        
        // 动态修改配置(不推荐在生产环境使用)
        config.setTokenName("Authorization");
        config.setTimeout(7200);
    }
    
    /**
     * 4. SaStrategy - 策略模式接口
     */
    public void saStrategyDemo() {
        // SA-Token使用策略模式来处理各种业务逻辑
        
        // 自定义Token生成策略
        SaStrategy.me.createToken = (loginId, loginType) -> {
            return "custom-token-" + loginId + "-" + System.currentTimeMillis();
        };
        
        // 自定义Session生成策略
        SaStrategy.me.createSession = (sessionId) -> {
            return new SaSession(sessionId);
        };
        
        // 自定义权限验证失败处理策略
        SaStrategy.me.notPermission = (loginType, permission) -> {
            throw new SaTokenException("权限不足:" + permission);
        };
        
        // 自定义角色验证失败处理策略
        SaStrategy.me.notRole = (loginType, role) -> {
            throw new SaTokenException("角色不足:" + role);
        };
    }
}
1.3.2 扩展机制设计

SA-Token提供了丰富的扩展机制,支持自定义实现各种组件:

java 复制代码
/**
 * SA-Token扩展机制演示
 */
public class SaTokenExtension {
    
    /**
     * 自定义权限数据源
     */
    @Component
    public class CustomStpInterface implements StpInterface {
        
        @Autowired
        private UserService userService;
        
        @Autowired
        private RoleService roleService;
        
        @Autowired
        private PermissionService permissionService;
        
        /**
         * 返回指定用户的权限列表
         */
        @Override
        public List<String> getPermissionList(Object loginId, String loginType) {
            Long userId = Long.valueOf(loginId.toString());
            
            // 从数据库查询用户权限
            List<Permission> permissions = permissionService.getPermissionsByUserId(userId);
            
            return permissions.stream()
                    .map(Permission::getPermissionCode)
                    .collect(Collectors.toList());
        }
        
        /**
         * 返回指定用户的角色列表
         */
        @Override
        public List<String> getRoleList(Object loginId, String loginType) {
            Long userId = Long.valueOf(loginId.toString());
            
            // 从数据库查询用户角色
            List<Role> roles = roleService.getRolesByUserId(userId);
            
            return roles.stream()
                    .map(Role::getRoleCode)
                    .collect(Collectors.toList());
        }
    }
    
    /**
     * 自定义Token持久化实现
     */
    @Component
    public class CustomSaTokenDao implements SaTokenDao {
        
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        
        @Override
        public String get(String key) {
            return (String) redisTemplate.opsForValue().get(key);
        }
        
        @Override
        public void set(String key, String value, long timeout) {
            if (timeout == SaTokenDao.NEVER_EXPIRE) {
                redisTemplate.opsForValue().set(key, value);
            } else {
                redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
            }
        }
        
        @Override
        public void update(String key, String value) {
            long expire = getTimeout(key);
            if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {
                return;
            }
            this.set(key, value, expire);
        }
        
        @Override
        public void delete(String key) {
            redisTemplate.delete(key);
        }
        
        @Override
        public long getTimeout(String key) {
            Long expire = redisTemplate.getExpire(key);
            return expire == null ? SaTokenDao.NOT_VALUE_EXPIRE : expire;
        }
        
        @Override
        public void updateTimeout(String key, long timeout) {
            redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
        
        @Override
        public Object getObject(String key) {
            return redisTemplate.opsForValue().get(key);
        }
        
        @Override
        public void setObject(String key, Object object, long timeout) {
            if (timeout == SaTokenDao.NEVER_EXPIRE) {
                redisTemplate.opsForValue().set(key, object);
            } else {
                redisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
            }
        }
        
        @Override
        public void updateObject(String key, Object object) {
            long expire = getObjectTimeout(key);
            if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {
                return;
            }
            this.setObject(key, object, expire);
        }
        
        @Override
        public long getObjectTimeout(String key) {
            Long expire = redisTemplate.getExpire(key);
            return expire == null ? SaTokenDao.NOT_VALUE_EXPIRE : expire;
        }
        
        @Override
        public void updateObjectTimeout(String key, long timeout) {
            redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
        
        @Override
        public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
            // 实现数据搜索逻辑
            Set<String> keys = redisTemplate.keys(prefix + "*" + keyword + "*");
            List<String> list = new ArrayList<>(keys);
            
            // 排序
            if (sortType) {
                Collections.sort(list);
            } else {
                Collections.sort(list, Collections.reverseOrder());
            }
            
            // 分页
            int fromIndex = start;
            int toIndex = Math.min(start + size, list.size());
            
            if (fromIndex >= list.size()) {
                return new ArrayList<>();
            }
            
            return list.subList(fromIndex, toIndex);
        }
    }
}

第二章:SpringBoot集成SA-Token基础配置

2.1 项目环境搭建

2.1.1 Maven依赖配置

首先,我们需要在SpringBoot项目中添加SA-Token的相关依赖:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>satoken-demo</artifactId>
    <version>1.0.0</version>
    <name>SA-Token集成示例</name>
    <description>SpringBoot集成SA-Token权限校验框架示例项目</description>
    
    <properties>
        <java.version>8</java.version>
        <sa-token.version>1.34.0</sa-token.version>
    </properties>
    
    <dependencies>
        <!-- SpringBoot Web启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- SA-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>${sa-token.version}</version>
        </dependency>
        
        <!-- SA-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>${sa-token.version}</version>
        </dependency>
        
        <!-- 提供Redis连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        
        <!-- SpringBoot数据库启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- MySQL数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- JSON处理 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        
        <!-- Lombok简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- SpringBoot测试启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
2.1.2 应用配置文件

application.yml中配置SA-Token和相关组件:

yaml 复制代码
# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /api

# 数据源配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/satoken_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456
    
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

# SA-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: Authorization
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: true
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false
  # 是否从cookie中读取token
  is-read-cookie: true
  # 是否从header中读取token
  is-read-header: true
  # 是否从body中读取token
  is-read-body: false
  # token前缀
  token-prefix: "Bearer"
  # jwt秘钥
  jwt-secret-key: abcdefghijklmnopqrstuvwxyz
  
# 日志配置
logging:
  level:
    com.example: debug
    org.springframework.web: debug
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
2.1.3 数据库表结构设计

创建用户权限相关的数据库表:

sql 复制代码
-- 用户表
CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `avatar` varchar(200) DEFAULT NULL COMMENT '头像',
  `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_code` varchar(50) NOT NULL COMMENT '角色编码',
  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
  `description` varchar(200) DEFAULT NULL COMMENT '角色描述',
  `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 权限表
CREATE TABLE `sys_permission` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限ID',
  `permission_code` varchar(100) NOT NULL COMMENT '权限编码',
  `permission_name` varchar(100) NOT NULL COMMENT '权限名称',
  `resource_type` varchar(20) DEFAULT NULL COMMENT '资源类型:menu-菜单,button-按钮',
  `url` varchar(200) DEFAULT NULL COMMENT '资源路径',
  `method` varchar(10) DEFAULT NULL COMMENT '请求方法',
  `parent_id` bigint DEFAULT '0' COMMENT '父权限ID',
  `sort_order` int DEFAULT '0' COMMENT '排序',
  `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_permission_code` (`permission_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

-- 用户角色关联表
CREATE TABLE `sys_user_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_role` (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 角色权限关联表
CREATE TABLE `sys_role_permission` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `permission_id` bigint NOT NULL COMMENT '权限ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_permission` (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';

-- 插入测试数据
INSERT INTO `sys_user` (`username`, `password`, `nickname`, `email`, `status`) VALUES
('admin', '$2a$10$7JB720yubVSOfvVMe6/YqO4wkhWGEn4bJJnNpSn0kfzOLuTOQHHiq', '系统管理员', 'admin@example.com', 1),
('user', '$2a$10$7JB720yubVSOfvVMe6/YqO4wkhWGEn4bJJnNpSn0kfzOLuTOQHHiq', '普通用户', 'user@example.com', 1);

INSERT INTO `sys_role` (`role_code`, `role_name`, `description`, `status`) VALUES
('admin', '系统管理员', '拥有系统所有权限', 1),
('user', '普通用户', '拥有基础权限', 1);

INSERT INTO `sys_permission` (`permission_code`, `permission_name`, `resource_type`, `url`, `method`) VALUES
('system:user:list', '用户列表', 'menu', '/system/user/list', 'GET'),
('system:user:add', '添加用户', 'button', '/system/user/add', 'POST'),
('system:user:edit', '编辑用户', 'button', '/system/user/edit', 'PUT'),
('system:user:delete', '删除用户', 'button', '/system/user/delete', 'DELETE'),
('system:role:list', '角色列表', 'menu', '/system/role/list', 'GET'),
('system:role:add', '添加角色', 'button', '/system/role/add', 'POST');

INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES
(1, 1),
(2, 2);

INSERT INTO `sys_role_permission` (`role_id`, `permission_id`) VALUES
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),
(2, 1), (2, 5);

2.2 核心配置类实现

2.2.1 SA-Token配置类
java 复制代码
/**
 * SA-Token配置类
 */
@Configuration
@EnableConfigurationProperties
public class SaTokenConfig {
    
    /**
     * 获取StpInterface权限认证接口的实现类
     */
    @Bean
    public StpInterface stpInterface() {
        return new StpInterfaceImpl();
    }
    
    /**
     * SA-Token全局异常处理
     */
    @Bean
    public GlobalExceptionHandler globalExceptionHandler() {
        return new GlobalExceptionHandler();
    }
    
    /**
     * SA-Token拦截器配置
     */
    @Configuration
    public static class SaTokenInterceptorConfig implements WebMvcConfigurer {
        
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 注册Sa-Token拦截器,校验规则为StpUtil.checkLogin()登录校验
            registry.addInterceptor(new SaInterceptor(handle -> {
                // 指定一条match规则
                SaRouter.match("/**")    // 拦截所有路由
                        .notMatch("/auth/login")        // 排除登录接口
                        .notMatch("/auth/register")     // 排除注册接口
                        .notMatch("/auth/captcha")      // 排除验证码接口
                        .notMatch("/doc.html")          // 排除swagger文档
                        .notMatch("/swagger-ui/**")     // 排除swagger资源
                        .notMatch("/swagger-resources/**") // 排除swagger资源
                        .notMatch("/v2/api-docs")       // 排除swagger接口
                        .notMatch("/v3/api-docs")       // 排除swagger接口
                        .notMatch("/webjars/**")        // 排除swagger资源
                        .notMatch("/favicon.ico")       // 排除网站图标
                        .notMatch("/actuator/**")       // 排除监控端点
                        .check(r -> StpUtil.checkLogin()); // 登录校验
            })).addPathPatterns("/**");
        }
    }
    
    /**
     * 自定义JSON序列化方式
     */
    @Bean
    @Primary
    public SaJsonTemplate saJsonTemplate() {
        return new SaJsonTemplateForFastjson();
    }
}
2.2.2 权限认证接口实现
java 复制代码
/**
 * 自定义权限验证接口扩展
 */
@Component
public class StpInterfaceImpl implements StpInterface {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private RoleService roleService;
    
    @Autowired
    private PermissionService permissionService;
    
    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        try {
            Long userId = Long.valueOf(loginId.toString());
            
            // 查询用户权限列表
            List<String> permissions = permissionService.getPermissionsByUserId(userId);
            
            log.debug("用户[{}]拥有权限: {}", userId, permissions);
            return permissions;
            
        } catch (Exception e) {
            log.error("获取用户权限列表失败, loginId: {}", loginId, e);
            return Collections.emptyList();
        }
    }
    
    /**
     * 返回一个账号所拥有的角色标识集合
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        try {
            Long userId = Long.valueOf(loginId.toString());
            
            // 查询用户角色列表
            List<String> roles = roleService.getRolesByUserId(userId);
            
            log.debug("用户[{}]拥有角色: {}", userId, roles);
            return roles;
            
        } catch (Exception e) {
            log.error("获取用户角色列表失败, loginId: {}", loginId, e);
            return Collections.emptyList();
        }
    }
}
2.2.3 全局异常处理器
java 复制代码
/**
 * 全局异常处理器
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 拦截:未登录异常
     */
    @ExceptionHandler(NotLoginException.class)
    public Result handleNotLoginException(NotLoginException e) {
        String message = "";
        
        // 判断场景值,定制化异常信息
        switch (e.getType()) {
            case NotLoginException.NOT_TOKEN:
                message = "未提供Token";
                break;
            case NotLoginException.INVALID_TOKEN:
                message = "Token无效";
                break;
            case NotLoginException.TOKEN_TIMEOUT:
                message = "Token已过期";
                break;
            case NotLoginException.BE_REPLACED:
                message = "Token已被顶下线";
                break;
            case NotLoginException.KICK_OUT:
                message = "Token已被踢下线";
                break;
            default:
                message = "当前会话未登录";
                break;
        }
        
        log.warn("用户未登录访问受保护资源: {}", message);
        return Result.error(401, message);
    }
    
    /**
     * 拦截:缺少权限异常
     */
    @ExceptionHandler(NotPermissionException.class)
    public Result handleNotPermissionException(NotPermissionException e) {
        log.warn("用户权限不足, 缺少权限: {}", e.getPermission());
        return Result.error(403, "权限不足,缺少权限:" + e.getPermission());
    }
    
    /**
     * 拦截:缺少角色异常
     */
    @ExceptionHandler(NotRoleException.class)
    public Result handleNotRoleException(NotRoleException e) {
        log.warn("用户角色不足, 缺少角色: {}", e.getRole());
        return Result.error(403, "角色不足,缺少角色:" + e.getRole());
    }
    
    /**
     * 拦截:禁用账号异常
     */
    @ExceptionHandler(DisableServiceException.class)
    public Result handleDisableServiceException(DisableServiceException e) {
        log.warn("账号被禁用, 禁用服务: {}, 禁用级别: {}, 禁用时间: {}秒", 
                e.getService(), e.getLevel(), e.getDisableTime());
        return Result.error(423, "账号已被禁用:" + e.getDisableTime() + "秒后解封");
    }
    
    /**
     * 拦截:二级认证异常
     */
    @ExceptionHandler(NotSafeException.class)
    public Result handleNotSafeException(NotSafeException e) {
        log.warn("二级认证失败: {}", e.getMessage());
        return Result.error(901, "请完成二级认证:" + e.getMessage());
    }
    
    /**
     * 拦截:服务封禁异常
     */
    @ExceptionHandler(SaTokenException.class)
    public Result handleSaTokenException(SaTokenException e) {
        log.error("SA-Token异常: {}", e.getMessage(), e);
        return Result.error(500, "系统异常:" + e.getMessage());
    }
    
    /**
     * 拦截:其他所有异常
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("系统异常: {}", e.getMessage(), e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

2.3 实体类和数据访问层

2.3.1 实体类定义
java 复制代码
/**
 * 用户实体类
 */
@Entity
@Table(name = "sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false, length = 50)
    private String username;
    
    @Column(nullable = false, length = 100)
    private String password;
    
    @Column(length = 50)
    private String nickname;
    
    @Column(length = 100)
    private String email;
    
    @Column(length = 20)
    private String phone;
    
    @Column(length = 200)
    private String avatar;
    
    @Column(columnDefinition = "TINYINT DEFAULT 1")
    private Integer status;
    
    @CreationTimestamp
    @Column(name = "create_time")
    private LocalDateTime createTime;
    
    @UpdateTimestamp
    @Column(name = "update_time")
    private LocalDateTime updateTime;
    
    // 多对多关联角色
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "sys_user_role",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
}

/**
 * 角色实体类
 */
@Entity
@Table(name = "sys_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false, length = 50)
    private String roleCode;
    
    @Column(nullable = false, length = 50)
    private String roleName;
    
    @Column(length = 200)
    private String description;
    
    @Column(columnDefinition = "TINYINT DEFAULT 1")
    private Integer status;
    
    @CreationTimestamp
    @Column(name = "create_time")
    private LocalDateTime createTime;
    
    @UpdateTimestamp
    @Column(name = "update_time")
    private LocalDateTime updateTime;
    
    // 多对多关联权限
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "sys_role_permission",
        joinColumns = @JoinColumn(name = "role_id"),
        inverseJoinColumns = @JoinColumn(name = "permission_id")
    )
    private Set<Permission> permissions = new HashSet<>();
}

/**
 * 权限实体类
 */
@Entity
@Table(name = "sys_permission")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permission {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false, length = 100)
    private String permissionCode;
    
    @Column(nullable = false, length = 100)
    private String permissionName;
    
    @Column(length = 20)
    private String resourceType;
    
    @Column(length = 200)
    private String url;
    
    @Column(length = 10)
    private String method;
    
    @Column(columnDefinition = "BIGINT DEFAULT 0")
    private Long parentId;
    
    @Column(columnDefinition = "INT DEFAULT 0")
    private Integer sortOrder;
    
    @Column(columnDefinition = "TINYINT DEFAULT 1")
    private Integer status;
    
    @CreationTimestamp
    @Column(name = "create_time")
    private LocalDateTime createTime;
    
    @UpdateTimestamp
    @Column(name = "update_time")
    private LocalDateTime updateTime;
}
2.3.2 数据访问层实现
java 复制代码
/**
 * 用户数据访问接口
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    /**
     * 根据用户名查找用户
     */
    Optional<User> findByUsername(String username);
    
    /**
     * 根据用户名和状态查找用户
     */
    Optional<User> findByUsernameAndStatus(String username, Integer status);
    
    /**
     * 检查用户名是否存在
     */
    boolean existsByUsername(String username);
    
    /**
     * 检查邮箱是否存在
     */
    boolean existsByEmail(String email);
    
    /**
     * 根据状态查找用户列表
     */
    List<User> findByStatus(Integer status);
    
    /**
     * 根据用户名模糊查询
     */
    @Query("SELECT u FROM User u WHERE u.username LIKE %:username%")
    List<User> findByUsernameLike(@Param("username") String username);
}

/**
 * 角色数据访问接口
 */
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    
    /**
     * 根据角色编码查找角色
     */
    Optional<Role> findByRoleCode(String roleCode);
    
    /**
     * 根据状态查找角色列表
     */
    List<Role> findByStatus(Integer status);
    
    /**
     * 检查角色编码是否存在
     */
    boolean existsByRoleCode(String roleCode);
    
    /**
     * 根据用户ID查找角色列表
     */
    @Query("SELECT r FROM Role r JOIN r.users u WHERE u.id = :userId AND r.status = 1")
    List<Role> findByUserId(@Param("userId") Long userId);
}

/**
 * 权限数据访问接口
 */
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {
    
    /**
     * 根据权限编码查找权限
     */
    Optional<Permission> findByPermissionCode(String permissionCode);
    
    /**
     * 根据状态查找权限列表
     */
    List<Permission> findByStatus(Integer status);
    
    /**
     * 根据资源类型查找权限列表
     */
    List<Permission> findByResourceType(String resourceType);
    
    /**
     * 根据父权限ID查找子权限列表
     */
    List<Permission> findByParentId(Long parentId);
    
    /**
     * 根据用户ID查找权限列表
     */
    @Query("SELECT DISTINCT p FROM Permission p " +
           "JOIN p.roles r " +
           "JOIN r.users u " +
           "WHERE u.id = :userId AND p.status = 1")
    List<Permission> findByUserId(@Param("userId") Long userId);
    
    /**
     * 根据角色ID查找权限列表
     */
    @Query("SELECT p FROM Permission p JOIN p.roles r WHERE r.id = :roleId AND p.status = 1")
    List<Permission> findByRoleId(@Param("roleId") Long roleId);
}

2.4 业务服务层实现

2.4.1 用户服务实现
java 复制代码
/**
 * 用户服务实现类
 */
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RoleRepository roleRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * 用户认证
     */
    @Override
    public User authenticate(String username, String password) {
        log.debug("用户认证开始, username: {}", username);
        
        // 查找用户
        Optional<User> userOpt = userRepository.findByUsernameAndStatus(username, 1);
        if (!userOpt.isPresent()) {
            log.warn("用户不存在或已被禁用, username: {}", username);
            return null;
        }
        
        User user = userOpt.get();
        
        // 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            log.warn("用户密码错误, username: {}", username);
            return null;
        }
        
        log.info("用户认证成功, userId: {}, username: {}", user.getId(), username);
        return user;
    }
    
    /**
     * 根据ID获取用户
     */
    @Override
    @Transactional(readOnly = true)
    public User getById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    /**
     * 根据用户名获取用户
     */
    @Override
    @Transactional(readOnly = true)
    public User getByUsername(String username) {
        return userRepository.findByUsername(username).orElse(null);
    }
    
    /**
     * 创建用户
     */
    @Override
    public User createUser(UserCreateRequest request) {
        log.info("创建用户开始, username: {}", request.getUsername());
        
        // 检查用户名是否已存在
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new BusinessException("用户名已存在");
        }
        
        // 检查邮箱是否已存在
        if (StringUtils.hasText(request.getEmail()) && 
            userRepository.existsByEmail(request.getEmail())) {
            throw new BusinessException("邮箱已存在");
        }
        
        // 创建用户对象
        User user = User.builder()
                .username(request.getUsername())
                .password(passwordEncoder.encode(request.getPassword()))
                .nickname(request.getNickname())
                .email(request.getEmail())
                .phone(request.getPhone())
                .status(1)
                .build();
        
        // 保存用户
        user = userRepository.save(user);
        
        // 分配默认角色
        if (request.getRoleIds() != null && !request.getRoleIds().isEmpty()) {
            assignRoles(user.getId(), request.getRoleIds());
        } else {
            // 分配默认用户角色
            Role defaultRole = roleRepository.findByRoleCode("user").orElse(null);
            if (defaultRole != null) {
                assignRoles(user.getId(), Collections.singletonList(defaultRole.getId()));
            }
        }
        
        log.info("用户创建成功, userId: {}, username: {}", user.getId(), user.getUsername());
        return user;
    }
    
    /**
     * 更新用户信息
     */
    @Override
    public User updateUser(Long userId, UserUpdateRequest request) {
        log.info("更新用户信息开始, userId: {}", userId);
        
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
        
        // 更新基本信息
        if (StringUtils.hasText(request.getNickname())) {
            user.setNickname(request.getNickname());
        }
        if (StringUtils.hasText(request.getEmail())) {
            // 检查邮箱是否已被其他用户使用
            if (userRepository.existsByEmail(request.getEmail()) && 
                !request.getEmail().equals(user.getEmail())) {
                throw new BusinessException("邮箱已被其他用户使用");
            }
            user.setEmail(request.getEmail());
        }
        if (StringUtils.hasText(request.getPhone())) {
            user.setPhone(request.getPhone());
        }
        if (StringUtils.hasText(request.getAvatar())) {
            user.setAvatar(request.getAvatar());
        }
        
        // 保存更新
        user = userRepository.save(user);
        
        log.info("用户信息更新成功, userId: {}", userId);
        return user;
    }
    
    /**
     * 修改密码
     */
    @Override
    public void changePassword(Long userId, String oldPassword, String newPassword) {
        log.info("修改用户密码开始, userId: {}", userId);
        
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
        
        // 验证旧密码
        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
            throw new BusinessException("原密码错误");
        }
        
        // 更新密码
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
        
        log.info("用户密码修改成功, userId: {}", userId);
    }
    
    /**
     * 分配角色
     */
    @Override
    public void assignRoles(Long userId, List<Long> roleIds) {
        log.info("分配用户角色开始, userId: {}, roleIds: {}", userId, roleIds);
        
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
        
        // 清除现有角色
        user.getRoles().clear();
        
        // 分配新角色
        if (roleIds != null && !roleIds.isEmpty()) {
            List<Role> roles = roleRepository.findAllById(roleIds);
            user.getRoles().addAll(roles);
        }
        
        userRepository.save(user);
        
        log.info("用户角色分配成功, userId: {}, roleCount: {}", userId, user.getRoles().size());
    }
    
    /**
     * 启用/禁用用户
     */
    @Override
    public void updateUserStatus(Long userId, Integer status) {
        log.info("更新用户状态开始, userId: {}, status: {}", userId, status);
        
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
        
        user.setStatus(status);
        userRepository.save(user);
        
        // 如果是禁用用户,则踢下线
        if (status == 0) {
            StpUtil.kickout(userId);
            log.info("用户已被踢下线, userId: {}", userId);
        }
        
        log.info("用户状态更新成功, userId: {}, status: {}", userId, status);
    }
    
    /**
     * 删除用户
     */
    @Override
    public void deleteUser(Long userId) {
        log.info("删除用户开始, userId: {}", userId);
        
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
        
        // 踢下线
        StpUtil.kickout(userId);
        
        // 删除用户
        userRepository.delete(user);
        
        log.info("用户删除成功, userId: {}", userId);
    }
    
    /**
     * 获取用户列表
     */
    @Override
    @Transactional(readOnly = true)
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    /**
     * 分页获取用户列表
     */
    @Override
    @Transactional(readOnly = true)
    public Page<User> getUserPage(Pageable pageable) {
        return userRepository.findAll(pageable);
    }
}
2.4.2 角色服务实现
java 复制代码
/**
 * 角色服务实现类
 */
@Service
@Transactional
@Slf4j
public class RoleServiceImpl implements RoleService {
    
    @Autowired
    private RoleRepository roleRepository;
    
    @Autowired
    private PermissionRepository permissionRepository;
    
    /**
     * 根据用户ID获取角色列表
     */
    @Override
    @Transactional(readOnly = true)
    public List<String> getRolesByUserId(Long userId) {
        List<Role> roles = roleRepository.findByUserId(userId);
        return roles.stream()
                .map(Role::getRoleCode)
                .collect(Collectors.toList());
    }
    
    /**
     * 创建角色
     */
    @Override
    public Role createRole(RoleCreateRequest request) {
        log.info("创建角色开始, roleCode: {}", request.getRoleCode());
        
        // 检查角色编码是否已存在
        if (roleRepository.existsByRoleCode(request.getRoleCode())) {
            throw new BusinessException("角色编码已存在");
        }
        
        // 创建角色对象
        Role role = Role.builder()
                .roleCode(request.getRoleCode())
                .roleName(request.getRoleName())
                .description(request.getDescription())
                .status(1)
                .build();
        
        // 保存角色
        role = roleRepository.save(role);
        
        // 分配权限
        if (request.getPermissionIds() != null && !request.getPermissionIds().isEmpty()) {
            assignPermissions(role.getId(), request.getPermissionIds());
        }
        
        log.info("角色创建成功, roleId: {}, roleCode: {}", role.getId(), role.getRoleCode());
        return role;
    }
    
    /**
     * 分配权限
     */
    @Override
    public void assignPermissions(Long roleId, List<Long> permissionIds) {
        log.info("分配角色权限开始, roleId: {}, permissionIds: {}", roleId, permissionIds);
        
        Role role = roleRepository.findById(roleId)
                .orElseThrow(() -> new BusinessException("角色不存在"));
        
        // 清除现有权限
        role.getPermissions().clear();
        
        // 分配新权限
        if (permissionIds != null && !permissionIds.isEmpty()) {
            List<Permission> permissions = permissionRepository.findAllById(permissionIds);
            role.getPermissions().addAll(permissions);
        }
        
        roleRepository.save(role);
        
        log.info("角色权限分配成功, roleId: {}, permissionCount: {}", roleId, role.getPermissions().size());
    }
}
2.4.3 权限服务实现
java 复制代码
/**
 * 权限服务实现类
 */
@Service
@Transactional
@Slf4j
public class PermissionServiceImpl implements PermissionService {
    
    @Autowired
    private PermissionRepository permissionRepository;
    
    /**
     * 根据用户ID获取权限列表
     */
    @Override
    @Transactional(readOnly = true)
    public List<String> getPermissionsByUserId(Long userId) {
        List<Permission> permissions = permissionRepository.findByUserId(userId);
        return permissions.stream()
                .map(Permission::getPermissionCode)
                .collect(Collectors.toList());
    }
    
    /**
     * 获取所有权限
     */
    @Override
    @Transactional(readOnly = true)
    public List<Permission> getAllPermissions() {
        return permissionRepository.findByStatus(1);
    }
    
    /**
     * 构建权限树
     */
    @Override
    @Transactional(readOnly = true)
    public List<PermissionTreeNode> buildPermissionTree() {
        List<Permission> allPermissions = getAllPermissions();
        
        // 构建权限树
        Map<Long, PermissionTreeNode> nodeMap = new HashMap<>();
        List<PermissionTreeNode> rootNodes = new ArrayList<>();
        
        // 创建所有节点
        for (Permission permission : allPermissions) {
            PermissionTreeNode node = PermissionTreeNode.builder()
                    .id(permission.getId())
                    .permissionCode(permission.getPermissionCode())
                    .permissionName(permission.getPermissionName())
                    .resourceType(permission.getResourceType())
                    .url(permission.getUrl())
                    .method(permission.getMethod())
                    .parentId(permission.getParentId())
                    .sortOrder(permission.getSortOrder())
                    .children(new ArrayList<>())
                    .build();
            nodeMap.put(permission.getId(), node);
        }
        
        // 构建树形结构
        for (PermissionTreeNode node : nodeMap.values()) {
            if (node.getParentId() == 0) {
                rootNodes.add(node);
            } else {
                PermissionTreeNode parent = nodeMap.get(node.getParentId());
                if (parent != null) {
                    parent.getChildren().add(node);
                }
            }
        }
        
        // 排序
        sortPermissionTree(rootNodes);
        
        return rootNodes;
    }
    
    private void sortPermissionTree(List<PermissionTreeNode> nodes) {
        nodes.sort(Comparator.comparing(PermissionTreeNode::getSortOrder));
        for (PermissionTreeNode node : nodes) {
            if (!node.getChildren().isEmpty()) {
                sortPermissionTree(node.getChildren());
            }
        }
    }
}

第三章:权限认证与授权实战

3.1 登录认证实现

3.1.1 登录控制器实现
java 复制代码
/**
 * 认证控制器
 */
@RestController
@RequestMapping("/auth")
@Slf4j
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private CaptchaService captchaService;
    
    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Result login(@RequestBody @Valid LoginRequest request) {
        log.info("用户登录请求, username: {}", request.getUsername());
        
        // 验证验证码
        if (!captchaService.verifyCaptcha(request.getCaptchaKey(), request.getCaptchaCode())) {
            return Result.error("验证码错误");
        }
        
        // 用户认证
        User user = userService.authenticate(request.getUsername(), request.getPassword());
        if (user == null) {
            return Result.error("用户名或密码错误");
        }
        
        // 检查用户状态
        if (user.getStatus() != 1) {
            return Result.error("账号已被禁用");
        }
        
        // 执行登录
        StpUtil.login(user.getId(), new SaLoginModel()
                .setDevice(request.getDevice())
                .setTimeout(request.getRememberMe() ? 30 * 24 * 3600 : -1) // 记住我30天
                .setIsLastingCookie(request.getRememberMe()));
        
        // 获取Token信息
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        
        // 构建登录响应
        LoginResponse response = LoginResponse.builder()
                .tokenName(tokenInfo.getTokenName())
                .tokenValue(tokenInfo.getTokenValue())
                .isLogin(tokenInfo.getIsLogin())
                .loginId(tokenInfo.getLoginId())
                .loginType(tokenInfo.getLoginType())
                .tokenTimeout(tokenInfo.getTokenTimeout())
                .sessionTimeout(tokenInfo.getSessionTimeout())
                .tokenSessionTimeout(tokenInfo.getTokenSessionTimeout())
                .tokenActivityTimeout(tokenInfo.getTokenActivityTimeout())
                .loginDevice(tokenInfo.getLoginDevice())
                .tag(tokenInfo.getTag())
                .userInfo(UserInfo.builder()
                        .id(user.getId())
                        .username(user.getUsername())
                        .nickname(user.getNickname())
                        .email(user.getEmail())
                        .avatar(user.getAvatar())
                        .build())
                .build();
        
        log.info("用户登录成功, userId: {}, username: {}, tokenValue: {}", 
                user.getId(), user.getUsername(), tokenInfo.getTokenValue());
        
        return Result.success(response);
    }
    
    /**
     * 用户注销
     */
    @PostMapping("/logout")
    public Result logout() {
        Object loginId = StpUtil.getLoginId();
        StpUtil.logout();
        
        log.info("用户注销成功, userId: {}", loginId);
        return Result.success("注销成功");
    }
    
    /**
     * 获取当前用户信息
     */
    @GetMapping("/userinfo")
    public Result getUserInfo() {
        // 检查登录状态
        StpUtil.checkLogin();
        
        // 获取当前用户ID
        Long userId = StpUtil.getLoginIdAsLong();
        
        // 查询用户信息
        User user = userService.getById(userId);
        if (user == null) {
            return Result.error("用户不存在");
        }
        
        // 获取用户权限和角色
        List<String> permissions = StpUtil.getPermissionList();
        List<String> roles = StpUtil.getRoleList();
        
        // 构建用户信息响应
        UserInfoResponse response = UserInfoResponse.builder()
                .id(user.getId())
                .username(user.getUsername())
                .nickname(user.getNickname())
                .email(user.getEmail())
                .phone(user.getPhone())
                .avatar(user.getAvatar())
                .status(user.getStatus())
                .createTime(user.getCreateTime())
                .permissions(permissions)
                .roles(roles)
                .build();
        
        return Result.success(response);
    }
    
    /**
     * 刷新Token
     */
    @PostMapping("/refresh")
    public Result refreshToken() {
        // 检查登录状态
        StpUtil.checkLogin();
        
        // 续签Token
        StpUtil.renewTimeout(7200); // 续签2小时
        
        // 获取新的Token信息
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        
        return Result.success(tokenInfo);
    }
    
    /**
     * 获取验证码
     */
    @GetMapping("/captcha")
    public Result getCaptcha() {
        CaptchaResponse captcha = captchaService.generateCaptcha();
        return Result.success(captcha);
    }
}
3.1.2 验证码服务实现
java 复制代码
/**
 * 验证码服务实现
 */
@Service
@Slf4j
public class CaptchaServiceImpl implements CaptchaService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String CAPTCHA_PREFIX = "captcha:";
    private static final int CAPTCHA_EXPIRE_TIME = 300; // 5分钟
    private static final int CAPTCHA_LENGTH = 4;
    
    /**
     * 生成验证码
     */
    @Override
    public CaptchaResponse generateCaptcha() {
        // 生成验证码key
        String captchaKey = UUID.randomUUID().toString();
        
        // 生成验证码内容
        String captchaCode = generateRandomCode(CAPTCHA_LENGTH);
        
        // 生成验证码图片
        String captchaImage = generateCaptchaImage(captchaCode);
        
        // 存储到Redis
        redisTemplate.opsForValue().set(
                CAPTCHA_PREFIX + captchaKey, 
                captchaCode.toLowerCase(), 
                CAPTCHA_EXPIRE_TIME, 
                TimeUnit.SECONDS
        );
        
        log.debug("生成验证码, key: {}, code: {}", captchaKey, captchaCode);
        
        return CaptchaResponse.builder()
                .captchaKey(captchaKey)
                .captchaImage(captchaImage)
                .expireTime(CAPTCHA_EXPIRE_TIME)
                .build();
    }
    
    /**
     * 验证验证码
     */
    @Override
    public boolean verifyCaptcha(String captchaKey, String captchaCode) {
        if (!StringUtils.hasText(captchaKey) || !StringUtils.hasText(captchaCode)) {
            return false;
        }
        
        String redisKey = CAPTCHA_PREFIX + captchaKey;
        String storedCode = redisTemplate.opsForValue().get(redisKey);
        
        if (storedCode == null) {
            log.warn("验证码已过期或不存在, key: {}", captchaKey);
            return false;
        }
        
        // 验证后删除验证码
        redisTemplate.delete(redisKey);
        
        boolean isValid = storedCode.equalsIgnoreCase(captchaCode);
        log.debug("验证码校验结果, key: {}, code: {}, result: {}", captchaKey, captchaCode, isValid);
        
        return isValid;
    }
    
    private String generateRandomCode(int length) {
        String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        
        for (int i = 0; i < length; i++) {
            sb.append(chars.charAt(random.nextInt(chars.length())));
        }
        
        return sb.toString();
    }
    
    private String generateCaptchaImage(String code) {
        // 创建图片
        int width = 120;
        int height = 40;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        
        // 设置背景色
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        
        // 设置字体
        g.setFont(new Font("Arial", Font.BOLD, 20));
        
        // 绘制验证码
        Random random = new Random();
        for (int i = 0; i < code.length(); i++) {
            g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
            g.drawString(String.valueOf(code.charAt(i)), 20 + i * 20, 25);
        }
        
        // 添加干扰线
        for (int i = 0; i < 5; i++) {
            g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
            g.drawLine(random.nextInt(width), random.nextInt(height), 
                      random.nextInt(width), random.nextInt(height));
        }
        
        g.dispose();
        
        // 转换为Base64
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(image, "png", baos);
            byte[] imageBytes = baos.toByteArray();
            return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes);
        } catch (IOException e) {
            log.error("生成验证码图片失败", e);
            return null;
        }
    }
}

## 第四章:高级特性与扩展应用

### 4.1 单点登录(SSO)实现

#### 4.1.1 SSO基础配置

SA-Token提供了强大的单点登录功能,支持同域和跨域的SSO实现。

```java
/**
 * SSO配置类
 */
@Configuration
public class SsoConfig {
    
    /**
     * SSO相关配置
     */
    @Bean
    @ConfigurationProperties(prefix = "sa-token.sso")
    public SaSsoConfig getSaSsoConfig() {
        return new SaSsoConfig()
                // SSO-Server端 统一认证地址
                .setAuthUrl("http://sa-sso-server.com:9000/sso/auth")
                // SSO-Server端 ticket校验地址
                .setCheckTicketUrl("http://sa-sso-server.com:9000/sso/checkTicket")
                // SSO-Server端 单点注销地址
                .setSloUrl("http://sa-sso-server.com:9000/sso/signout")
                // 当前Client端 单点注销回调URL
                .setSsoLogoutCall("http://sa-sso-client1.com:9001/sso/logoutCall")
                // 是否打开单点注销功能
                .setIsSlo(true);
    }
}
4.1.2 SSO-Server端实现
java 复制代码
/**
 * SSO认证服务端控制器
 */
@RestController
@RequestMapping("/sso")
@Slf4j
public class SsoServerController {
    
    /**
     * SSO统一认证页面
     */
    @GetMapping("/auth")
    public SaResult auth(String redirect, String mode, HttpServletRequest request) {
        log.info("SSO统一认证,redirect={}, mode={}", redirect, mode);
        
        // 如果已经登录,则直接重定向到Client端
        if (StpUtil.isLogin()) {
            return SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);
        }
        
        // 未登录,显示登录页面
        return SaResult.ok().setData(buildLoginPage(redirect, mode));
    }
    
    /**
     * 处理登录请求
     */
    @PostMapping("/doLogin")
    public SaResult doLogin(String username, String password, String redirect) {
        log.info("SSO登录处理,username={}, redirect={}", username, redirect);
        
        // 验证用户名密码
        if (validateUser(username, password)) {
            // 登录成功,生成ticket并重定向
            StpUtil.login(username);
            return SaSsoUtil.buildRedirectUrl(username, redirect);
        }
        
        return SaResult.error("用户名或密码错误");
    }
    
    /**
     * 校验ticket
     */
    @GetMapping("/checkTicket")
    public SaResult checkTicket(String ticket, String ssoLogoutCall) {
        log.info("校验ticket,ticket={}, ssoLogoutCall={}", ticket, ssoLogoutCall);
        
        // 校验ticket,获取账号id
        Object loginId = SaSsoUtil.checkTicket(ticket);
        if (loginId != null) {
            // 注册此客户端的单点注销回调URL
            SaSsoUtil.registerClient(loginId, ssoLogoutCall);
            return SaResult.data(loginId);
        }
        
        return SaResult.error("无效的ticket");
    }
    
    /**
     * 单点注销
     */
    @GetMapping("/signout")
    public SaResult signout(String loginId, String secretkey) {
        log.info("SSO单点注销,loginId={}", loginId);
        
        // 校验秘钥
        SaSsoUtil.checkSecretkey(secretkey);
        
        // 遍历通知所有Client端注销
        SaSsoUtil.singleLogout(loginId);
        
        return SaResult.ok("单点注销成功");
    }
    
    /**
     * 验证用户凭据
     */
    private boolean validateUser(String username, String password) {
        // 这里应该连接数据库验证用户信息
        // 为了演示,简单验证
        return "admin".equals(username) && "123456".equals(password);
    }
    
    /**
     * 构建登录页面HTML
     */
    private String buildLoginPage(String redirect, String mode) {
        return """
            <!DOCTYPE html>
            <html>
            <head>
                <title>SSO统一认证中心</title>
                <meta charset="utf-8">
                <style>
                    .login-form { width: 300px; margin: 100px auto; padding: 20px; border: 1px solid #ddd; }
                    .form-item { margin: 10px 0; }
                    .form-item input { width: 100%; padding: 8px; }
                    .btn { width: 100%; padding: 10px; background: #007bff; color: white; border: none; cursor: pointer; }
                </style>
            </head>
            <body>
                <div class="login-form">
                    <h2>统一认证中心</h2>
                    <form action="/sso/doLogin" method="post">
                        <input type="hidden" name="redirect" value="%s">
                        <div class="form-item">
                            <input type="text" name="username" placeholder="用户名" required>
                        </div>
                        <div class="form-item">
                            <input type="password" name="password" placeholder="密码" required>
                        </div>
                        <div class="form-item">
                            <button type="submit" class="btn">登录</button>
                        </div>
                    </form>
                </div>
            </body>
            </html>
            """.formatted(redirect != null ? redirect : "");
    }
}
4.1.3 SSO-Client端实现
java 复制代码
/**
 * SSO客户端控制器
 */
@RestController
@RequestMapping("/sso")
@Slf4j
public class SsoClientController {
    
    /**
     * 首页
     */
    @GetMapping("/")
    public SaResult index() {
        String loginId = (String) StpUtil.getLoginIdDefaultNull();
        if (loginId != null) {
            return SaResult.ok("欢迎用户:" + loginId);
        }
        return SaResult.ok("当前未登录").setData("<a href='/sso/login'>点击登录</a>");
    }
    
    /**
     * 发起登录
     */
    @GetMapping("/login")
    public SaResult login(String back) {
        // 构建授权地址
        String authUrl = SaSsoUtil.buildAuthUrl();
        log.info("重定向到SSO认证中心:{}", authUrl);
        return SaResult.ok().setData("redirect:" + authUrl);
    }
    
    /**
     * SSO登录回调
     */
    @GetMapping("/login/callback")
    public SaResult loginCallback(String ticket, String back) {
        log.info("SSO登录回调,ticket={}, back={}", ticket, back);
        
        // 根据ticket进行登录
        Object loginId = SaSsoUtil.checkTicket(ticket, "/sso/logoutCall");
        if (loginId != null) {
            StpUtil.login(loginId);
            return SaResult.ok("登录成功").setData("用户ID:" + loginId);
        }
        
        return SaResult.error("登录失败");
    }
    
    /**
     * 单点注销回调
     */
    @GetMapping("/logoutCall")
    public SaResult logoutCall(String loginId, String secretkey) {
        log.info("收到单点注销回调,loginId={}", loginId);
        
        // 校验秘钥
        SaSsoUtil.checkSecretkey(secretkey);
        
        // 注销当前用户
        StpUtil.logout(loginId);
        
        return SaResult.ok("注销成功");
    }
    
    /**
     * 查询登录状态
     */
    @GetMapping("/isLogin")
    public SaResult isLogin() {
        boolean isLogin = StpUtil.isLogin();
        Object loginId = StpUtil.getLoginIdDefaultNull();
        
        return SaResult.ok()
                .set("isLogin", isLogin)
                .set("loginId", loginId)
                .set("tokenInfo", StpUtil.getTokenInfo());
    }
}

4.2 OAuth2.0集成

4.2.1 OAuth2配置

SA-Token提供了完整的OAuth2.0支持,可以快速构建OAuth2认证服务器。

java 复制代码
/**
 * OAuth2配置类
 */
@Configuration
public class OAuth2Config {
    
    /**
     * OAuth2配置
     */
    @Bean
    @ConfigurationProperties(prefix = "sa-token.oauth2")
    public SaOAuth2Config oauth2Config() {
        return new SaOAuth2Config()
                // 是否打开模式:授权码(Authorization Code)
                .setIsCode(true)
                // 是否打开模式:隐藏式(Implicit)
                .setIsImplicit(true)
                // 是否打开模式:密码式(Password)
                .setIsPassword(true)
                // 是否打开模式:客户端凭证(Client Credentials)
                .setIsClient(true)
                // 是否在每次Refresh-Token刷新Access-Token时,产生一个新的Refresh-Token
                .setIsNewRefresh(true);
    }
    
    /**
     * OAuth2数据加载器
     */
    @Component
    public static class OAuth2DataLoader implements SaOAuth2DataLoader {
        
        @Autowired
        private ClientService clientService;
        
        @Autowired
        private UserService userService;
        
        /**
         * 根据 client_id 获取 Client 信息
         */
        @Override
        public SaClientModel getClientModel(String clientId) {
            // 从数据库查询客户端信息
            Client client = clientService.getByClientId(clientId);
            if (client == null) {
                return null;
            }
            
            return new SaClientModel()
                    .setClientId(client.getClientId())
                    .setClientSecret(client.getClientSecret())
                    .setAllowUrl(client.getAllowUrl())
                    .setContractScope(client.getContractScope())
                    .setIsAutoMode(client.getIsAutoMode());
        }
        
        /**
         * 根据 ClientId 和 LoginId 获取openid
         */
        @Override
        public String getOpenid(String clientId, Object loginId) {
            // 可以根据 clientId 和 loginId 生成openid
            return DigestUtils.md5Hex(clientId + ":" + loginId);
        }
        
        /**
         * 校验:指定 LoginId 是否对指定 Client 授权给定 Scope
         */
        @Override
        public boolean isGrant(Object loginId, String clientId, String scope) {
            // 查询用户是否已授权
            return userService.hasGranted(String.valueOf(loginId), clientId, scope);
        }
        
        /**
         * 保存:指定 LoginId 对指定 Client 授权给定 Scope
         */
        @Override
        public void saveGrant(Object loginId, String clientId, String scope) {
            // 保存用户授权信息
            userService.saveGrant(String.valueOf(loginId), clientId, scope);
        }
    }
}

4.3 微服务网关鉴权

4.3.1 网关鉴权配置

在微服务架构中,SA-Token可以作为统一的鉴权组件集成到网关中。

java 复制代码
/**
 * 网关鉴权配置
 */
@Configuration
@EnableWebFluxSecurity
public class GatewayAuthConfig {
    
    /**
     * 注册Sa-Token全局过滤器
     */
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")
                // 排除地址
                .addExclude("/favicon.ico", "/actuator/**")
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 登录校验 -- 拦截所有路由,并排除/user/doLogin用于开放登录
                    SaRouter.match("/**", "/auth/login", r -> StpUtil.checkLogin());
                    
                    // 权限认证 -- 不同模块, 校验不同权限
                    SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
                    SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
                    SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
                    
                    // 角色认证 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
                    SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                });
    }
    
    /**
     * 配置跨域
     */
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        
        return new CorsWebFilter(source);
    }
}

4.4 多账户体系支持

4.4.1 多账户配置

SA-Token支持多账户体系,可以同时管理用户账户和管理员账户。

java 复制代码
/**
 * 多账户体系配置
 */
@Configuration
public class MultiAccountConfig {
    
    /**
     * 用户账户StpLogic
     */
    @Bean("userStpLogic")
    public StpLogic getUserStpLogic() {
        return new StpLogic("user") {
            // 重写获取权限列表的方法
            @Override
            public List<String> getPermissionList(Object loginId, String loginType) {
                // 查询用户权限
                return userService.getPermissionsByUserId(String.valueOf(loginId));
            }
            
            // 重写获取角色列表的方法
            @Override
            public List<String> getRoleList(Object loginId, String loginType) {
                // 查询用户角色
                return userService.getRolesByUserId(String.valueOf(loginId));
            }
        };
    }
    
    /**
     * 管理员账户StpLogic
     */
    @Bean("adminStpLogic")
    public StpLogic getAdminStpLogic() {
        return new StpLogic("admin") {
            @Override
            public List<String> getPermissionList(Object loginId, String loginType) {
                // 查询管理员权限
                return adminService.getPermissionsByAdminId(String.valueOf(loginId));
            }
            
            @Override
            public List<String> getRoleList(Object loginId, String loginType) {
                // 查询管理员角色
                return adminService.getRolesByAdminId(String.valueOf(loginId));
            }
        };
    }
}

第五章:生产环境最佳实践

5.1 性能优化策略

5.1.1 Token存储优化

在高并发场景下,Token的存储和检索性能至关重要。

java 复制代码
/**
 * Token存储优化配置
 */
@Configuration
public class TokenStorageOptimization {
    
    /**
     * Redis连接池优化
     */
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 连接池配置
        GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = 
                new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(200);
        poolConfig.setMaxIdle(50);
        poolConfig.setMinIdle(10);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);
        poolConfig.setTestWhileIdle(true);
        
        // Lettuce连接工厂
        LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .poolConfig(poolConfig)
                .commandTimeout(Duration.ofSeconds(5))
                .shutdownTimeout(Duration.ofSeconds(10))
                .build();
        
        RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration();
        serverConfig.setHostName("localhost");
        serverConfig.setPort(6379);
        serverConfig.setDatabase(0);
        
        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
    
    /**
     * 自定义Token存储实现
     */
    @Component
    public class OptimizedSaTokenDao implements SaTokenDao {
        
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        
        private final String TOKEN_PREFIX = "satoken:";
        
        @Override
        public String get(String key) {
            try {
                Object value = redisTemplate.opsForValue().get(TOKEN_PREFIX + key);
                return value != null ? value.toString() : null;
            } catch (Exception e) {
                log.error("Redis获取Token失败,key={}", key, e);
                return null;
            }
        }
        
        @Override
        public void set(String key, String value, long timeout) {
            try {
                if (timeout > 0) {
                    redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value, 
                            Duration.ofSeconds(timeout));
                } else {
                    redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value);
                }
            } catch (Exception e) {
                log.error("Redis设置Token失败,key={}, value={}", key, value, e);
            }
        }
        
        @Override
        public void update(String key, String value) {
            try {
                Long expire = redisTemplate.getExpire(TOKEN_PREFIX + key);
                if (expire != null && expire > 0) {
                    redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value, 
                            Duration.ofSeconds(expire));
                } else {
                    redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value);
                }
            } catch (Exception e) {
                log.error("Redis更新Token失败,key={}, value={}", key, value, e);
            }
        }
        
        @Override
        public void delete(String key) {
            try {
                redisTemplate.delete(TOKEN_PREFIX + key);
            } catch (Exception e) {
                log.error("Redis删除Token失败,key={}", key, e);
            }
        }
        
        @Override
        public long getTimeout(String key) {
            try {
                Long expire = redisTemplate.getExpire(TOKEN_PREFIX + key);
                return expire != null ? expire : -1;
            } catch (Exception e) {
                log.error("Redis获取Token过期时间失败,key={}", key, e);
                return -1;
            }
        }
        
        @Override
        public void updateTimeout(String key, long timeout) {
            try {
                redisTemplate.expire(TOKEN_PREFIX + key, Duration.ofSeconds(timeout));
            } catch (Exception e) {
                log.error("Redis更新Token过期时间失败,key={}, timeout={}", key, timeout, e);
            }
        }
    }
}
5.1.2 权限缓存优化
java 复制代码
/**
 * 权限缓存优化服务
 */
@Service
@Slf4j
public class PermissionCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private UserService userService;
    
    private static final String PERMISSION_CACHE_PREFIX = "permission:";
    private static final String ROLE_CACHE_PREFIX = "role:";
    private static final int CACHE_EXPIRE_SECONDS = 3600; // 1小时
    
    /**
     * 获取用户权限列表(带缓存)
     */
    public List<String> getUserPermissions(String userId) {
        String cacheKey = PERMISSION_CACHE_PREFIX + userId;
        
        try {
            // 先从缓存获取
            List<String> permissions = (List<String>) redisTemplate.opsForValue().get(cacheKey);
            if (permissions != null) {
                log.debug("从缓存获取用户权限,userId={}", userId);
                return permissions;
            }
            
            // 缓存未命中,从数据库查询
            permissions = userService.getPermissionsByUserId(userId);
            
            // 写入缓存
            redisTemplate.opsForValue().set(cacheKey, permissions, 
                    Duration.ofSeconds(CACHE_EXPIRE_SECONDS));
            
            log.debug("从数据库查询用户权限并缓存,userId={}, permissions={}", userId, permissions);
            return permissions;
            
        } catch (Exception e) {
            log.error("获取用户权限失败,userId={}", userId, e);
            // 缓存异常时直接查询数据库
            return userService.getPermissionsByUserId(userId);
        }
    }
    
    /**
     * 获取用户角色列表(带缓存)
     */
    public List<String> getUserRoles(String userId) {
        String cacheKey = ROLE_CACHE_PREFIX + userId;
        
        try {
            List<String> roles = (List<String>) redisTemplate.opsForValue().get(cacheKey);
            if (roles != null) {
                log.debug("从缓存获取用户角色,userId={}", userId);
                return roles;
            }
            
            roles = userService.getRolesByUserId(userId);
            redisTemplate.opsForValue().set(cacheKey, roles, 
                    Duration.ofSeconds(CACHE_EXPIRE_SECONDS));
            
            log.debug("从数据库查询用户角色并缓存,userId={}, roles={}", userId, roles);
            return roles;
            
        } catch (Exception e) {
            log.error("获取用户角色失败,userId={}", userId, e);
            return userService.getRolesByUserId(userId);
        }
    }
    
    /**
     * 清除用户权限缓存
     */
    public void clearUserPermissionCache(String userId) {
        try {
            redisTemplate.delete(PERMISSION_CACHE_PREFIX + userId);
            redisTemplate.delete(ROLE_CACHE_PREFIX + userId);
            log.info("清除用户权限缓存,userId={}", userId);
        } catch (Exception e) {
            log.error("清除用户权限缓存失败,userId={}", userId, e);
        }
    }
    
    /**
     * 批量预热权限缓存
     */
    @Async
    public void preloadPermissionCache(List<String> userIds) {
        log.info("开始预热权限缓存,用户数量={}", userIds.size());
        
        for (String userId : userIds) {
            try {
                getUserPermissions(userId);
                getUserRoles(userId);
                Thread.sleep(10); // 避免过快请求
            } catch (Exception e) {
                log.error("预热用户权限缓存失败,userId={}", userId, e);
            }
        }
        
        log.info("权限缓存预热完成");
    }
}

5.2 安全加固措施

5.2.1 Token安全增强
java 复制代码
/**
 * Token安全增强配置
 */
@Configuration
public class TokenSecurityConfig {
    
    /**
     * 自定义Token生成策略
     */
    @Bean
    public SaTokenAction saTokenAction() {
        return new SaTokenAction() {
            
            @Override
            public String createToken(Object loginId, String loginType) {
                // 生成更安全的Token
                String timestamp = String.valueOf(System.currentTimeMillis());
                String randomStr = UUID.randomUUID().toString().replace("-", "");
                String userAgent = SaHolder.getRequest().getHeader("User-Agent");
                String clientIp = SaFoxUtil.getClientIP();
                
                // 组合信息进行加密
                String tokenData = loginId + ":" + loginType + ":" + timestamp + 
                        ":" + randomStr + ":" + DigestUtils.md5Hex(userAgent + clientIp);
                
                // 使用AES加密
                return AESUtil.encrypt(tokenData, getTokenSecret());
            }
            
            @Override
            public Object getLoginIdByToken(String tokenValue) {
                try {
                    // 解密Token
                    String tokenData = AESUtil.decrypt(tokenValue, getTokenSecret());
                    String[] parts = tokenData.split(":");
                    
                    if (parts.length >= 5) {
                        String loginId = parts[0];
                        String timestamp = parts[2];
                        
                        // 检查Token时效性(额外的时间校验)
                        long createTime = Long.parseLong(timestamp);
                        long maxAge = 24 * 60 * 60 * 1000; // 24小时
                        if (System.currentTimeMillis() - createTime > maxAge) {
                            throw new SaTokenException("Token已过期");
                        }
                        
                        return loginId;
                    }
                } catch (Exception e) {
                    log.error("Token解析失败", e);
                }
                return null;
            }
        };
    }
    
    /**
     * Token签名密钥
     */
    private String getTokenSecret() {
        // 从配置文件或环境变量获取密钥
        return "your-secret-key-here";
    }
    
    /**
     * IP白名单验证
     */
    @Component
    public class IpWhitelistValidator {
        
        private final Set<String> whitelistIps = new HashSet<>();
        
        @PostConstruct
        public void init() {
            // 从配置文件加载IP白名单
            whitelistIps.add("127.0.0.1");
            whitelistIps.add("192.168.1.0/24");
        }
        
        public boolean isAllowed(String clientIp) {
            return whitelistIps.contains(clientIp) || isInSubnet(clientIp);
        }
        
        private boolean isInSubnet(String clientIp) {
            // 实现子网匹配逻辑
            for (String subnet : whitelistIps) {
                if (subnet.contains("/") && matchSubnet(clientIp, subnet)) {
                    return true;
                }
            }
            return false;
        }
        
        private boolean matchSubnet(String ip, String subnet) {
            // 子网匹配实现
            try {
                String[] subnetParts = subnet.split("/");
                String networkIp = subnetParts[0];
                int prefixLength = Integer.parseInt(subnetParts[1]);
                
                InetAddress targetAddr = InetAddress.getByName(ip);
                InetAddress networkAddr = InetAddress.getByName(networkIp);
                
                byte[] targetBytes = targetAddr.getAddress();
                byte[] networkBytes = networkAddr.getAddress();
                
                int bytesToCheck = prefixLength / 8;
                int bitsToCheck = prefixLength % 8;
                
                // 检查完整字节
                for (int i = 0; i < bytesToCheck; i++) {
                    if (targetBytes[i] != networkBytes[i]) {
                        return false;
                    }
                }
                
                // 检查剩余位
                if (bitsToCheck > 0 && bytesToCheck < targetBytes.length) {
                    int mask = 0xFF << (8 - bitsToCheck);
                    return (targetBytes[bytesToCheck] & mask) == (networkBytes[bytesToCheck] & mask);
                }
                
                return true;
            } catch (Exception e) {
                log.error("子网匹配失败", e);
                return false;
            }
        }
    }
}
5.2.2 防攻击策略
java 复制代码
/**
 * 防攻击策略实现
 */
@Component
@Slf4j
public class SecurityDefenseService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String LOGIN_ATTEMPT_PREFIX = "login_attempt:";
    private static final String RATE_LIMIT_PREFIX = "rate_limit:";
    private static final int MAX_LOGIN_ATTEMPTS = 5;
    private static final int LOGIN_LOCK_DURATION = 300; // 5分钟
    private static final int RATE_LIMIT_REQUESTS = 100;
    private static final int RATE_LIMIT_WINDOW = 60; // 1分钟
    
    /**
     * 检查登录尝试次数
     */
    public boolean checkLoginAttempts(String clientIp, String username) {
        String key = LOGIN_ATTEMPT_PREFIX + clientIp + ":" + username;
        
        try {
            Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
            if (attempts != null && attempts >= MAX_LOGIN_ATTEMPTS) {
                log.warn("登录尝试次数超限,IP={}, username={}, attempts={}", clientIp, username, attempts);
                return false;
            }
            return true;
        } catch (Exception e) {
            log.error("检查登录尝试次数失败", e);
            return true; // 异常时允许登录
        }
    }
    
    /**
     * 记录登录失败
     */
    public void recordLoginFailure(String clientIp, String username) {
        String key = LOGIN_ATTEMPT_PREFIX + clientIp + ":" + username;
        
        try {
            Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
            attempts = attempts != null ? attempts + 1 : 1;
            
            redisTemplate.opsForValue().set(key, attempts, Duration.ofSeconds(LOGIN_LOCK_DURATION));
            
            log.info("记录登录失败,IP={}, username={}, attempts={}", clientIp, username, attempts);
        } catch (Exception e) {
            log.error("记录登录失败次数异常", e);
        }
    }
    
    /**
     * 清除登录失败记录
     */
    public void clearLoginFailures(String clientIp, String username) {
        String key = LOGIN_ATTEMPT_PREFIX + clientIp + ":" + username;
        
        try {
            redisTemplate.delete(key);
            log.info("清除登录失败记录,IP={}, username={}", clientIp, username);
        } catch (Exception e) {
            log.error("清除登录失败记录异常", e);
        }
    }
    
    /**
     * 检查请求频率限制
     */
    public boolean checkRateLimit(String clientIp) {
        String key = RATE_LIMIT_PREFIX + clientIp;
        
        try {
            Integer requests = (Integer) redisTemplate.opsForValue().get(key);
            if (requests != null && requests >= RATE_LIMIT_REQUESTS) {
                log.warn("请求频率超限,IP={}, requests={}", clientIp, requests);
                return false;
            }
            
            // 增加请求计数
            if (requests == null) {
                redisTemplate.opsForValue().set(key, 1, Duration.ofSeconds(RATE_LIMIT_WINDOW));
            } else {
                redisTemplate.opsForValue().increment(key);
            }
            
            return true;
        } catch (Exception e) {
            log.error("检查请求频率限制失败", e);
            return true; // 异常时允许请求
        }
    }
    
    /**
     * SQL注入检测
     */
    public boolean detectSqlInjection(String input) {
        if (input == null || input.isEmpty()) {
            return false;
        }
        
        String[] sqlKeywords = {
                "select", "insert", "update", "delete", "drop", "create", "alter",
                "union", "exec", "execute", "script", "javascript", "vbscript",
                "onload", "onerror", "onclick", "'", "\"", ";", "--", "/*", "*/"
        };
        
        String lowerInput = input.toLowerCase();
        for (String keyword : sqlKeywords) {
            if (lowerInput.contains(keyword)) {
                log.warn("检测到可疑SQL注入尝试,input={}", input);
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * XSS攻击检测
     */
    public boolean detectXssAttack(String input) {
        if (input == null || input.isEmpty()) {
            return false;
        }
        
        String[] xssPatterns = {
                "<script", "</script>", "javascript:", "vbscript:", "onload=", 
                "onerror=", "onclick=", "onmouseover=", "onfocus=", "onblur=",
                "alert(", "confirm(", "prompt(", "document.cookie", "document.write"
        };
        
        String lowerInput = input.toLowerCase();
        for (String pattern : xssPatterns) {
            if (lowerInput.contains(pattern)) {
                log.warn("检测到可疑XSS攻击尝试,input={}", input);
                return true;
            }
        }
        
        return false;
    }
}

5.3 监控与日志

5.3.1 认证监控
java 复制代码
/**
 * 认证监控服务
 */
@Service
@Slf4j
public class AuthMonitorService {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private final Counter loginSuccessCounter;
    private final Counter loginFailureCounter;
    private final Counter logoutCounter;
    private final Timer authenticationTimer;
    
    public AuthMonitorService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.loginSuccessCounter = Counter.builder("auth.login.success")
                .description("成功登录次数")
                .register(meterRegistry);
        this.loginFailureCounter = Counter.builder("auth.login.failure")
                .description("登录失败次数")
                .register(meterRegistry);
        this.logoutCounter = Counter.builder("auth.logout")
                .description("注销次数")
                .register(meterRegistry);
        this.authenticationTimer = Timer.builder("auth.authentication.duration")
                .description("认证耗时")
                .register(meterRegistry);
    }
    
    /**
     * 记录登录成功
     */
    public void recordLoginSuccess(String userId, String clientIp, String userAgent) {
        loginSuccessCounter.increment();
        
        // 记录详细日志
        log.info("用户登录成功 - userId={}, clientIp={}, userAgent={}", userId, clientIp, userAgent);
        
        // 记录登录历史
        recordLoginHistory(userId, clientIp, userAgent, true);
        
        // 更新在线用户统计
        updateOnlineUserStats(userId, true);
    }
    
    /**
     * 记录登录失败
     */
    public void recordLoginFailure(String username, String clientIp, String reason) {
        loginFailureCounter.increment(Tags.of("reason", reason));
        
        log.warn("用户登录失败 - username={}, clientIp={}, reason={}", username, clientIp, reason);
        
        // 记录失败历史
        recordLoginHistory(username, clientIp, null, false);
    }
    
    /**
     * 记录注销
     */
    public void recordLogout(String userId, String clientIp) {
        logoutCounter.increment();
        
        log.info("用户注销 - userId={}, clientIp={}", userId, clientIp);
        
        // 更新在线用户统计
        updateOnlineUserStats(userId, false);
    }
    
    /**
     * 记录认证耗时
     */
    public void recordAuthenticationTime(Duration duration) {
        authenticationTimer.record(duration);
    }
    
    /**
     * 记录登录历史
     */
    private void recordLoginHistory(String userId, String clientIp, String userAgent, boolean success) {
        try {
            LoginHistory history = new LoginHistory();
            history.setUserId(userId);
            history.setClientIp(clientIp);
            history.setUserAgent(userAgent);
            history.setSuccess(success);
            history.setLoginTime(LocalDateTime.now());
            
            // 异步保存到数据库
            CompletableFuture.runAsync(() -> {
                // 保存登录历史逻辑
                saveLoginHistory(history);
            });
            
        } catch (Exception e) {
            log.error("记录登录历史失败", e);
        }
    }
    
    /**
     * 更新在线用户统计
     */
    private void updateOnlineUserStats(String userId, boolean online) {
        try {
            String key = "online_users";
            if (online) {
                redisTemplate.opsForSet().add(key, userId);
            } else {
                redisTemplate.opsForSet().remove(key, userId);
            }
            
            // 更新Micrometer指标
            Long onlineCount = redisTemplate.opsForSet().size(key);
            Gauge.builder("auth.online.users")
                    .description("在线用户数")
                    .register(meterRegistry, this, obj -> onlineCount != null ? onlineCount : 0);
                    
        } catch (Exception e) {
            log.error("更新在线用户统计失败", e);
        }
    }
    
    /**
     * 获取认证统计信息
     */
    public AuthStatistics getAuthStatistics() {
        try {
            AuthStatistics stats = new AuthStatistics();
            
            // 从Micrometer获取统计数据
            stats.setLoginSuccessCount((long) loginSuccessCounter.count());
            stats.setLoginFailureCount((long) loginFailureCounter.count());
            stats.setLogoutCount((long) logoutCounter.count());
            stats.setAverageAuthTime(authenticationTimer.mean(TimeUnit.MILLISECONDS));
            
            // 获取在线用户数
            Long onlineUsers = redisTemplate.opsForSet().size("online_users");
            stats.setOnlineUserCount(onlineUsers != null ? onlineUsers : 0);
            
            return stats;
        } catch (Exception e) {
            log.error("获取认证统计信息失败", e);
            return new AuthStatistics();
        }
    }
    
    /**
     * 保存登录历史(实际实现)
     */
    private void saveLoginHistory(LoginHistory history) {
        // 实际的数据库保存逻辑
        log.debug("保存登录历史:{}", history);
    }
}
5.3.2 审计日志
java 复制代码
/**
 * 审计日志服务
 */
@Service
@Slf4j
public class AuditLogService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @EventListener
    public void handleLoginEvent(LoginEvent event) {
        AuditLog auditLog = AuditLog.builder()
                .userId(event.getUserId())
                .action("LOGIN")
                .resource("AUTH")
                .clientIp(event.getClientIp())
                .userAgent(event.getUserAgent())
                .timestamp(LocalDateTime.now())
                .success(event.isSuccess())
                .details(event.getDetails())
                .build();
                
        saveAuditLog(auditLog);
    }
    
    @EventListener
    public void handlePermissionCheckEvent(PermissionCheckEvent event) {
        AuditLog auditLog = AuditLog.builder()
                .userId(event.getUserId())
                .action("PERMISSION_CHECK")
                .resource(event.getResource())
                .permission(event.getPermission())
                .clientIp(SaFoxUtil.getClientIP())
                .timestamp(LocalDateTime.now())
                .success(event.isSuccess())
                .details(event.getDetails())
                .build();
                
        saveAuditLog(auditLog);
    }
    
    /**
     * 保存审计日志
     */
    private void saveAuditLog(AuditLog auditLog) {
        try {
            // 异步保存到数据库
            CompletableFuture.runAsync(() -> {
                // 实际的数据库保存逻辑
                log.info("审计日志:{}", auditLog);
            });
            
            // 同时保存到Redis用于实时查询
            String key = "audit_logs:" + LocalDate.now().toString();
            redisTemplate.opsForList().leftPush(key, auditLog);
            redisTemplate.expire(key, Duration.ofDays(7)); // 保留7天
            
        } catch (Exception e) {
            log.error("保存审计日志失败", e);
        }
    }
    
    /**
     * 查询审计日志
     */
    public List<AuditLog> queryAuditLogs(String userId, LocalDate date, String action) {
        try {
            String key = "audit_logs:" + date.toString();
            List<Object> logs = redisTemplate.opsForList().range(key, 0, -1);
            
            return logs.stream()
                    .map(obj -> (AuditLog) obj)
                    .filter(log -> userId == null || userId.equals(log.getUserId()))
                    .filter(log -> action == null || action.equals(log.getAction()))
                    .collect(Collectors.toList());
                    
        } catch (Exception e) {
            log.error("查询审计日志失败", e);
            return Collections.emptyList();
        }
    }
}

第六章:总结与展望

6.1 知识点回顾

通过本文的深入学习,我们全面掌握了SA-Token权限认证框架的核心技术和实践应用。让我们回顾一下主要的知识点:

6.1.1 核心概念与特性

SA-Token作为一个轻量级的Java权限认证框架,具有以下核心优势:

  • 简洁的API设计StpUtil.login()StpUtil.checkLogin()等简单易用的API
  • 丰富的功能特性:支持登录认证、权限验证、角色验证、踢人下线、会话管理等
  • 灵活的集成方式:与SpringBoot、SpringCloud等主流框架无缝集成
  • 强大的扩展能力:支持自定义Token生成策略、存储方式、权限验证逻辑等
6.1.2 技术架构要点
java 复制代码
/**
 * SA-Token技术架构核心组件回顾
 */
public class SaTokenArchitectureReview {
    
    /**
     * 1. 核心组件
     */
    // StpUtil - 权限认证工具类
    // SaTokenDao - Token存储接口
    // SaTokenConfig - 框架配置类
    // StpInterface - 权限数据接口
    
    /**
     * 2. 认证流程
     */
    public void authenticationFlow() {
        // 用户登录 -> 生成Token -> 存储会话 -> 返回Token
        StpUtil.login(userId);
        
        // 请求验证 -> 解析Token -> 校验会话 -> 检查权限
        StpUtil.checkLogin();
        StpUtil.checkPermission("user:list");
    }
    
    /**
     * 3. 扩展机制
     */
    // 自定义Token生成:实现SaTokenAction接口
    // 自定义存储方式:实现SaTokenDao接口  
    // 自定义权限验证:实现StpInterface接口
    // 自定义配置策略:继承SaTokenConfig类
}
6.1.3 实战应用总结

在实际项目中,我们学会了如何:

  1. 基础集成:SpringBoot项目中快速集成SA-Token
  2. 权限设计:构建RBAC权限模型,实现细粒度权限控制
  3. 高级特性:单点登录、OAuth2.0、微服务网关鉴权等企业级应用
  4. 性能优化:Token存储优化、权限缓存、连接池配置等
  5. 安全加固:防暴力破解、SQL注入检测、XSS防护等
  6. 监控运维:认证监控、审计日志、性能指标等

6.2 最佳实践总结

6.2.1 开发规范
java 复制代码
/**
 * SA-Token开发最佳实践
 */
@Component
public class SaTokenBestPractices {
    
    /**
     * 1. 统一异常处理
     */
    @ExceptionHandler(NotLoginException.class)
    public SaResult handleNotLoginException(NotLoginException e) {
        return SaResult.error("请先登录").setCode(401);
    }
    
    /**
     * 2. 权限注解使用
     */
    @SaCheckPermission("user:list")
    @GetMapping("/users")
    public SaResult getUsers() {
        // 业务逻辑
        return SaResult.ok();
    }
    
    /**
     * 3. 会话管理
     */
    public void sessionManagement() {
        // 设置会话数据
        StpUtil.getSession().set("userInfo", userInfo);
        
        // 获取会话数据
        UserInfo info = (UserInfo) StpUtil.getSession().get("userInfo");
        
        // 清理会话
        StpUtil.getSession().clear();
    }
    
    /**
     * 4. 多端登录控制
     */
    public void multiDeviceLogin() {
        // 允许多端登录
        StpUtil.login(userId, "PC");
        StpUtil.login(userId, "MOBILE");
        
        // 踢掉其他端
        StpUtil.kickout(userId, "PC");
    }
}
6.2.2 性能优化建议
  1. 合理配置Token过期时间:平衡安全性和用户体验
  2. 使用Redis集群:提高Token存储的可用性和性能
  3. 权限缓存策略:减少数据库查询,提升响应速度
  4. 异步日志记录:避免影响主业务流程性能
  5. 连接池优化:合理配置数据库和Redis连接池参数
6.2.3 安全防护要点
  1. Token安全:使用强加密算法,定期轮换密钥
  2. 传输安全:HTTPS传输,避免Token泄露
  3. 存储安全:Redis密码保护,网络隔离
  4. 访问控制:IP白名单,请求频率限制
  5. 审计监控:完整的操作日志,异常告警机制

6.3 技术发展趋势

6.3.1 云原生权限管理

随着云原生技术的发展,权限管理也在向云原生方向演进:

yaml 复制代码
# Kubernetes RBAC集成示例
apiVersion: v1
kind: ConfigMap
metadata:
  name: satoken-config
data:
  application.yml: |
    sa-token:
      token-name: satoken
      timeout: 2592000
      is-concurrent: true
      token-style: uuid
      # 云原生配置
      jwt:
        secret-key: ${JWT_SECRET:default-secret}
      redis:
        host: ${REDIS_HOST:redis-service}
        port: ${REDIS_PORT:6379}
        password: ${REDIS_PASSWORD:}
6.3.2 零信任安全架构

零信任安全模型要求对每个请求都进行验证:

java 复制代码
/**
 * 零信任安全验证
 */
@Component
public class ZeroTrustValidator {
    
    public boolean validateRequest(HttpServletRequest request) {
        // 1. 身份验证
        if (!StpUtil.isLogin()) {
            return false;
        }
        
        // 2. 设备验证
        if (!validateDevice(request)) {
            return false;
        }
        
        // 3. 网络验证
        if (!validateNetwork(request)) {
            return false;
        }
        
        // 4. 行为验证
        if (!validateBehavior(request)) {
            return false;
        }
        
        return true;
    }
}
6.3.3 AI驱动的智能权限

未来的权限系统将更加智能化:

java 复制代码
/**
 * AI智能权限推荐
 */
@Service
public class IntelligentPermissionService {
    
    /**
     * 基于用户行为的权限推荐
     */
    public List<String> recommendPermissions(String userId) {
        // 分析用户历史行为
        UserBehavior behavior = analyzeUserBehavior(userId);
        
        // 机器学习模型预测
        List<String> recommendations = mlModel.predict(behavior);
        
        return recommendations;
    }
    
    /**
     * 异常行为检测
     */
    public boolean detectAnomalousAccess(String userId, String resource) {
        // 获取用户正常访问模式
        AccessPattern normalPattern = getUserAccessPattern(userId);
        
        // 当前访问行为
        AccessBehavior currentBehavior = getCurrentBehavior(userId, resource);
        
        // AI模型检测异常
        return anomalyDetectionModel.isAnomalous(normalPattern, currentBehavior);
    }
}

6.4 扩展阅读与学习资源

6.4.1 官方文档与社区
6.4.2 相关技术书籍
  1. 《Spring Security实战》- 深入理解Spring Security权限框架
  2. 《OAuth 2.0实战》- 掌握OAuth2.0协议和实现
  3. 《微服务安全架构与实践》- 微服务环境下的安全设计
  4. 《Redis实战》- 深入学习Redis在权限系统中的应用
6.4.3 在线学习资源
  • 慕课网:SA-Token实战课程
  • 极客时间:权限系统设计专栏
  • B站:SA-Token作者孔明老师的视频教程
  • 掘金社区:SA-Token技术文章和实践分享

6.5 实践练习与思考

6.5.1 动手实践项目

为了更好地掌握SA-Token,建议完成以下实践项目:

  1. 基础项目:构建一个简单的用户管理系统

    • 用户注册、登录、注销
    • 基本的权限控制
    • 会话管理
  2. 进阶项目:开发企业级权限管理平台

    • RBAC权限模型
    • 动态权限配置
    • 多租户支持
  3. 高级项目:微服务权限网关

    • 统一认证中心
    • 服务间鉴权
    • 分布式会话管理
6.5.2 思考讨论题
  1. 架构设计:如何在大型分布式系统中设计高可用的权限服务?
  2. 性能优化:面对百万级用户的权限验证,如何优化性能?
  3. 安全防护:如何防范权限系统面临的各种安全威胁?
  4. 技术选型:SA-Token与Spring Security的适用场景对比?
6.5.3 开源贡献

鼓励读者参与SA-Token开源社区建设:

  • 提交Bug报告和功能建议
  • 贡献代码和文档
  • 分享使用经验和最佳实践
  • 帮助其他开发者解决问题

6.6 结语

SA-Token作为一个优秀的权限认证框架,以其简洁、强大、灵活的特性,为Java开发者提供了一个高效的权限管理解决方案。通过本文的深入学习,相信读者已经掌握了SA-Token的核心技术和实践应用。

在实际项目开发中,权限管理不仅仅是技术问题,更是业务安全的重要保障。希望读者能够结合具体的业务场景,灵活运用SA-Token的各种特性,构建安全、高效、易维护的权限系统。

技术在不断发展,权限管理领域也在持续演进。保持学习的热情,关注技术发展趋势,积极参与开源社区,是每个技术人员成长的必经之路。

最后,感谢SA-Token开源团队的辛勤付出,感谢所有为权限管理技术发展做出贡献的开发者们。让我们一起推动Java权限认证技术的发展,为构建更安全的软件系统而努力!


如果这篇文章对你有帮助,请不要忘记点赞👍、收藏⭐、分享📤!你的支持是我创作的最大动力!

有任何问题欢迎在评论区讨论,我会及时回复大家!让我们一起在技术的道路上不断前行! 🚀

相关推荐
想ai抽5 小时前
Flink中的Lookup join和Temporal join 的语法是一样的吗?
java·大数据·flink
小白学大数据5 小时前
Java爬虫性能优化:以喜马拉雅音频元数据抓取为例
java·爬虫·性能优化
熬了夜的程序员5 小时前
【LeetCode】80. 删除有序数组中的重复项 II
java·数据结构·算法·leetcode·职场和发展·排序算法·动态规划
乐之者v5 小时前
Grafana监控可视化
java·grafana
DokiDoki之父5 小时前
SpringMVC—请求映射路径 & get请求与Post请求发送请求参数 & 5种类型参数传递 & json数据传递参数 & 日期型参数的传递 & 响应
spring
weixin_419658316 小时前
Spring的三级缓存和SpringMVC的流程
java·spring·缓存
lang201509286 小时前
Spring Boot Actuator应用信息Application Information全解析
spring boot·后端·elasticsearch
paopaokaka_luck6 小时前
基于SpringBoot+Vue的DIY手工社预约管理系统(Echarts图形化、腾讯地图API)
java·vue.js·人工智能·spring boot·后端·echarts
自在极意功。6 小时前
贪心算法深度解析:从理论到实战的完整指南
java·算法·ios·贪心算法