基于Spring Boot + Vue项目online_learn的权限控制机制分析

7 阅读17分钟

这是一个基于 Apache Shiro + JWT + Redis 的完整权限控制系统。以下是详细的学习资料清单:

📁 核心权限控制文件目录

1. 认证与授权核心组件

🔐 Shiro 核心配置类

表格

文件名称核心功能
ShiroConfig.java1. 配置 URL 过滤规则(匿名访问 / 需要认证的接口划分)2. 集成 JWT 过滤器到 Shiro 过滤链3. 配置 Redis 缓存管理器(缓存用户权限 / 角色)4. 启用 Shiro 注解支持(如@RequiresRoles/@RequiresPermissions)5. 初始化 SecurityManager 并关联自定义 Realm
@Configuration
public class ShiroConfig {
    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 配置Shiro过滤器工厂(核心:URL访问规则+自定义JWT过滤器)
     *
     * @param securityManager Shiro核心安全管理器(自动注入)
     * @return ShiroFilterFactoryBean 过滤器工厂
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        // ========== 1. 配置URL过滤链(LinkedHashMap保证顺序,先匹配优先) ==========
        Map<String, String> filterChainMap = new LinkedHashMap<>();
        // 匿名访问(无需登录)的URL
        filterChainMap.put("/login", "anon");                      // 登录接口
        filterChainMap.put("/login/register", "anon");             // 注册接口
        // 公共数据接口
        filterChainMap.put("/classification/getApeClassificationList", "anon");
        filterChainMap.put("/school/getApeSchoolList", "anon");
        filterChainMap.put("/major/getApeMajorList", "anon");
        // 静态资源/文件接口
        filterChainMap.put("/user/setUserAvatar/**", "anon");      // 头像设置
        filterChainMap.put("/common/**", "anon");                  // 通用接口
        filterChainMap.put("/img/**", "anon");                     // 图片资源
        filterChainMap.put("/video/**", "anon");                   // 视频资源
        filterChainMap.put("/file/**", "anon");                    // 文件资源
        // Swagger/API文档接口(补充:防止文档被拦截)
        filterChainMap.put("/swagger-ui/**", "anon");
        filterChainMap.put("/v3/api-docs/**", "anon");
        filterChainMap.put("/doc.html", "anon");
        // 所有其他URL:必须通过JWT认证
        filterChainMap.put("/**", "jwt");

        // ========== 2. 配置自定义过滤器 ==========
        Map<String, Filter> filters = new HashMap<>(1);
        // 注册JWT过滤器,名称与过滤链中的"jwt"对应
        filters.put("jwt", new JwtFilter());
        shiroFilter.setFilters(filters);

        // ========== 3. 绑定配置 ==========
        shiroFilter.setFilterChainDefinitionMap(filterChainMap);

        logger.info("Shiro过滤链配置完成,匿名URL数量:{}", filterChainMap.entrySet().stream()
                .filter(entry -> "anon".equals(entry.getValue())).count());
        return shiroFilter;
    }

    /**
     * 配置Shiro核心安全管理器(整合Realm+Redis缓存+关闭Session)
     *
     * @param shiroRealm        自定义Realm(认证/授权逻辑)
     * @param redisProperties   Spring Boot Redis配置属性
     * @return DefaultWebSecurityManager 安全管理器
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm, RedisProperties redisProperties) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        // 1. 绑定自定义Realm(核心:认证/授权的核心逻辑)
        securityManager.setRealm(shiroRealm);

        // 2. 关闭Shiro默认Session(适配JWT无状态认证)
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false); // 禁用Session存储
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        logger.info("Shiro默认Session已关闭,启用JWT无状态认证");

        // 3. 配置Redis缓存管理器(替换内存缓存,支持分布式)
        securityManager.setCacheManager(redisCacheManager(redisProperties));
        logger.info("Shiro Redis缓存管理器配置完成");

        return securityManager;
    }

    /**
     * 配置Redis缓存管理器(shiro-redis插件)
     * 用于缓存用户权限信息,减少数据库查询
     *
     * @param redisProperties Redis配置属性
     * @return RedisCacheManager 缓存管理器
     */
    public RedisCacheManager redisCacheManager(RedisProperties redisProperties) {
        RedisCacheManager cacheManager = new RedisCacheManager();
        // 绑定Redis连接管理器
        cacheManager.setRedisManager(redisManager(redisProperties));
        // 设置用户唯一标识字段(对应ApeUser的id字段,作为缓存Key的一部分)
        cacheManager.setPrincipalIdFieldName("id");
        // 权限缓存过期时间(单位:毫秒,200000ms=3分20秒,可根据业务调整)
        cacheManager.setExpire(200000);
        // 设置缓存前缀(避免与其他应用的Redis Key冲突)
        cacheManager.setKeyPrefix("shiro:cache:");
        return cacheManager;
    }

    /**
     * 配置Redis连接管理器(shiro-redis插件)
     * 读取Spring Boot的Redis配置,建立Redis连接
     *
     * @param redisProperties Redis配置属性(自动注入application.yml中的spring.redis配置)
     * @return RedisManager Redis连接管理器
     */
    @Bean
    public RedisManager redisManager(RedisProperties redisProperties) {
        RedisManager redisManager = new RedisManager();

        // 基础连接配置
        redisManager.setHost(redisProperties.getHost());
        redisManager.setPort(redisProperties.getPort());
        redisManager.setTimeout(redisProperties.getTimeout() != null ? redisProperties.getTimeout().toMillis() : 0);

        // 密码配置(非空时设置)
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            redisManager.setPassword(redisProperties.getPassword());
        }

        // 数据库索引(默认0,可根据配置调整)
        if (redisProperties.getDatabase() != null) {
            redisManager.setDatabase(redisProperties.getDatabase());
        }

        logger.info("Shiro Redis管理器配置完成:host={}:{}, database={}",
                redisProperties.getHost(), redisProperties.getPort(), redisProperties.getDatabase());

        // TODO:后续补充Redis集群配置(如setHosts方法)
        return redisManager;
    }

    /**
     * Shiro生命周期处理器(管理Shiro Bean的初始化和销毁)
     * 必须先于advisorAutoProxyCreator初始化,否则注解可能失效
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 配置AOP自动代理创建器(解决高版本Shiro注解导致404/失效问题)
     * @DependsOn:确保在lifecycleBeanPostProcessor之后初始化
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 设置为CGLIB代理(而非JDK动态代理),兼容所有类(包括无接口的类)
        proxyCreator.setProxyTargetClass(true);
        // 启用优化(提升代理性能)
        proxyCreator.setOptimize(true);
        return proxyCreator;
    }

    /**
     * 配置权限注解处理器(解析@RequiresPermissions/@RequiresRoles等注解)
     *
     * @param securityManager 安全管理器
     * @return AuthorizationAttributeSourceAdvisor 注解处理器
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        // 设置注解匹配器(确保能识别Shiro的权限注解)
        advisor.setAdviceBeanNamePattern("*AuthorizationAttributeSourceAdvisor");
        return advisor;
    }

👤 自定义 Realm(核心业务逻辑)

表格

文件名称核心功能
ShiroRealm.java1. 重写doGetAuthenticationInfo:实现用户认证逻辑(JWT Token 有效性校验)2. 重写doGetAuthorizationInfo:实现权限授权逻辑(加载用户角色 / 权限)3. JWT Token 签名验证、过期时间检查4. 用户状态校验(如是否禁用 / 注销)
@Component
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private ApeUserService apeUserService;

    @Autowired
    private ApeUserRoleService apeUserRoleService;

    @Autowired
    private ApeRoleMenuService apeRoleMenuService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
    * @description: 授权
    * @param: principals
    * @return:
    * @author shaozhujie
    * @date: 2023/9/7 15:11
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //根据用户名从自己的数据库中获取role和permission信息
        ApeUser apeUser = null;
        String loginAccount = null;
        if (principals != null) {
            apeUser = (ApeUser) principals.getPrimaryPrincipal();
            loginAccount = apeUser.getLoginAccount();
        }
        // 设置用户拥有的角色集合,比如“admin,test”
        Set<String> roleSet = apeUserRoleService.getUserRolesSet(loginAccount);
        simpleAuthorizationInfo.setRoles(roleSet);
        for (String role : roleSet) {
            // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
            Set<String> menuSet = apeRoleMenuService.getRoleMenusSet(role);
            simpleAuthorizationInfo.addStringPermissions(menuSet);
        }
        return simpleAuthorizationInfo;
    }

    /**
    * @description: 认证
    * @param: token
    * @return:
    * @author shaozhujie
    * @date: 2023/9/7 15:11
    */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getPrincipal();
        if (accessToken == null) {
            throw new AuthenticationException(ResultCode.COMMON_NO_TOKEN.getMessage());
        }
        // 校验token有效性
        ApeUser tokenEntity = this.checkUserTokenIsEffect(accessToken);
        return new SimpleAuthenticationInfo(tokenEntity, accessToken, getName());
    }

    /**
    * @description: * 校验token的有效性
     *springboot2.3.+新增了一个配置项server.error.includeMessage,默认是NEVER,
     *因此默认是不是输出message的,只要开启就可以了,否则无法拿到shiro抛出异常信息message
    * @param: token
    * @return:
    * @author shaozhujie
    * @date: 2023/9/14 11:12
    */
    public ApeUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密获得username,用于和数据库进行对比
        String userId = JwtUtil.getUserId(token);
        if (userId == null) {
            throw new AuthenticationException(ResultCode.COMMON_TOKEN_ILLEGAL.getMessage());
        }

        // 查询用户信息
        ApeUser loginUser = apeUserService.getById(userId);
        if (loginUser == null) {
            throw new UnknownAccountException(ResultCode.COMMON_USER_NOT_EXIST.getMessage());
        }
        // 判断用户状态
        if (loginUser.getStatus() != 0) {
            throw new LockedAccountException(ResultCode.COMMON_ACCOUNT_LOCKED.getMessage());
        }
        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, userId, loginUser.getPassword())) {
            throw new IncorrectCredentialsException(ResultCode.COMMON_TOKEN_FAILURE.getMessage());
        }
        return loginUser;
    }

    /**
    * @description: * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
     * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
     * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
     * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
     * 用户过期时间 = Jwt有效时间 * 2。
    * @param: token
        userId
        password
    * @return:
    * @author shaozhujie
    * @date: 2023/9/14 11:12
    */
    public boolean jwtTokenRefresh(String token, String userId, String password) {
        //如果缓存中的token为空,直接返回失效异常
        String cacheToken = stringRedisTemplate.opsForValue().get(Constants.PREFIX_USER_TOKEN + userId);
        if (!StringUtils.isBlank(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userId, password)) {
                JwtUtil.sign(userId, password);
            }
            return true;
        }
        return false;
    }

    /**
    * @description: 清除当前用户的权限认证缓存
    * @param: principals
    * @return:
    * @author shaozhujie
    * @date: 2023/9/7 15:10
    */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

🎫 JWT 相关组件(Token 处理)

表格

文件名称核心功能
JwtFilter.java1. 拦截所有请求,提取 Header 中的 JWT Token2. 预处理 Token,校验格式和有效性3. 捕获认证异常并统一返回(如 Token 过期 / 无效)4. 将有效 Token 封装为 JwtToken 交给 Shiro 处理
JwtToken.java1. 自定义 Token 类,封装 JWT 字符串2. 实现 Shiro 的 AuthenticationToken 接口3. 提供 Token 获取 / 设置方法
JwtUtil.java1. Token 生成(含用户 ID / 过期时间 / 签名)2. Token 验证(签名校验、过期检查)3. Token 解析(提取用户信息)4. 结合 Redis 实现 Token 黑名单 / 刷新机制

JwtFilter.java

public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
    * @description: 执行登录认证
    * @param: request
        response
        mappedValue
    * @return:
    * @author shaozhujie
    * @date: 2023/9/7 15:02
    */
    @SneakyThrows
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;
        try {
            executeLogin(request, response);
        } catch (IncorrectCredentialsException e) {
            JSONObject json = new JSONObject();
            json.put("code",1011);
            json.put("message",e.getMessage());
            json.put("timeStamp",System.currentTimeMillis());
            RequestUtils.returnJson(httpServletResponse,json.toJSONString());
            return false;
        } catch (LockedAccountException e) {
            JSONObject json = new JSONObject();
            json.put("code",1009);
            json.put("message",e.getMessage());
            json.put("timeStamp",System.currentTimeMillis());
            RequestUtils.returnJson(httpServletResponse,json.toJSONString());
            return false;
        } catch (UnknownAccountException e) {
            JSONObject json = new JSONObject();
            json.put("code",1008);
            json.put("message",e.getMessage());
            json.put("timeStamp",System.currentTimeMillis());
            RequestUtils.returnJson(httpServletResponse,json.toJSONString());
            return false;
        } catch (AuthenticationException e) {
            JSONObject json = new JSONObject();
            json.put("code",1006);
            json.put("message",e.getMessage());
            json.put("timeStamp",System.currentTimeMillis());
            RequestUtils.returnJson(httpServletResponse,json.toJSONString());
            return false;
        }
        return true;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = JwtUtil.getTokenByRequest(httpServletRequest);
        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

JwtToken.java

package com.ape.apeframework.custom;
import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 自定义token
 * @date 2023/8/11 9:59
 */
public class JwtToken implements AuthenticationToken {
    private static final long serialVersionUID = 1L;
    private String token;

    public JwtToken(String token){
        this.token = token;
    }

    @Override
    public String getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

JwtUtil.java

package com.ape.apecommon.utils;

import com.ape.apecommon.constant.Constants;
import com.ape.apecommon.utils.spring.SpringUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * JWT工具类
 * 核心功能:
 * 1. 生成JWT token(附带userId,基于HMAC256签名,缓存到Redis)
 * 2. 验证token有效性(签名+userId声明校验)
 * 3. 解析token中的userId
 * 4. 从HTTP请求头中提取token/解析userId
 *
 * @author shaozhujie
 * @version 1.0
 * @date 2023/8/11 10:00
 */
public class JwtUtil {
    // 日志记录器(便于排查token生成/验证异常)
    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    /**
     * Redis中token过期天数(实际生效的过期时间,JWT本身无过期)
     */
    public static final int REDIS_TOKEN_EXPIRE_DAYS = 3;

    // 从Spring容器获取Redis模板(懒加载,避免容器未初始化时获取失败)
    private static volatile StringRedisTemplate stringRedisTemplate;

    /**
     * 懒加载获取StringRedisTemplate Bean
     */
    private static StringRedisTemplate getStringRedisTemplate() {
        if (stringRedisTemplate == null) {
            synchronized (JwtUtil.class) {
                if (stringRedisTemplate == null) {
                    stringRedisTemplate = SpringUtils.getBean(StringRedisTemplate.class);
                }
            }
        }
        return stringRedisTemplate;
    }

    /**
     * 校验token是否正确
     *
     * @param token     待验证的JWT token
     * @param userId    预期的用户ID(校验token中的userId声明)
     * @param userPhone 签名秘钥(用户手机号,HMAC256加密用)
     * @return boolean 验证结果(true=有效,false=无效/异常)
     */
    public static boolean verify(String token, String userId, String userPhone) {
        // 前置参数校验
        if (!hasText(token) || !hasText(userId) || !hasText(userPhone)) {
            logger.warn("token验证失败:参数为空(token:{},userId:{})", token, userId);
            return false;
        }

        try {
            // 生成HMAC256算法(手机号作为秘钥)
            Algorithm algorithm = Algorithm.HMAC256(userPhone);
            // 创建验证器:校验签名 + userId声明匹配
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("userId", userId)
                    .build();
            // 执行验证
            verifier.verify(token);
            logger.info("token验证成功,userId:{}", userId);
            return true;
        } catch (Exception e) {
            logger.error("token验证失败(userId:{})", userId, e);
            return false;
        }
    }

    /**
     * 解析token中的userId(仅解析,不验证签名)
     *
     * @param token 待解析的JWT token
     * @return String userId(解析失败返回null)
     */
    public static String getUserId(String token) {
        if (!hasText(token)) {
            logger.warn("解析userId失败:token为空");
            return null;
        }

        try {
            DecodedJWT jwt = JWT.decode(token);
            String userId = jwt.getClaim("userId").asString();
            if (!hasText(userId)) {
                logger.warn("解析userId失败:token中无有效userId声明");
                return null;
            }
            return userId;
        } catch (Exception e) {
            logger.error("解析token中的userId失败", e);
            return null;
        }
    }

    /**
     * 生成JWT token(并缓存到Redis)
     *
     * @param userId    要存入token的用户ID
     * @param userPhone 签名秘钥(用户手机号)
     * @return String 生成的token(生成失败返回null)
     */
    public static String sign(String userId, String userPhone) {
        // 前置参数校验
        if (!hasText(userId) || !hasText(userPhone)) {
            logger.warn("生成token失败:userId或userPhone为空(userId:{})", userId);
            return null;
        }

        try {
            // 生成HMAC256加密算法
            Algorithm algorithm = Algorithm.HMAC256(userPhone);
            // 生成token(附带userId声明)
            String token = JWT.create()
                    .withClaim("userId", userId)
                    .sign(algorithm);

            // 缓存到Redis
            String redisKey = Constants.PREFIX_USER_TOKEN + userId;
            getStringRedisTemplate().opsForValue()
                    .set(redisKey, token, REDIS_TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);

            logger.info("生成token成功并缓存到Redis,userId:{},过期天数:{}", userId, REDIS_TOKEN_EXPIRE_DAYS);
            return token;
        } catch (Exception e) {
            logger.error("生成token失败(userId:{})", userId, e);
            return null;
        }
    }

    /**
     * 从HTTP请求头中获取token并解析userId
     *
     * @param request HTTP请求对象
     * @return String userId(获取/解析失败返回null)
     */
    public static String getUserIdByToken(HttpServletRequest request) {
        if (request == null) {
            logger.warn("从请求中获取userId失败:request为空");
            return null;
        }

        String token = getTokenByRequest(request);
        return getUserId(token);
    }

    /**
     * 从HTTP请求头中提取token
     *
     * @param request HTTP请求对象
     * @return String token(无token返回null)
     */
    public static String getTokenByRequest(HttpServletRequest request) {
        if (request == null) {
            logger.warn("提取token失败:request为空");
            return null;
        }

        String token = request.getHeader(Constants.X_ACCESS_TOKEN);
        if (!hasText(token)) {
            logger.debug("请求头中无有效token(header名称:{})", Constants.X_ACCESS_TOKEN);
        }
        return token;
    }

    /**
     * 私有工具方法:校验字符串是否有有效内容(非空且非空白)
     */
    private static boolean hasText(String str) {
        return str != null && !str.trim().isEmpty();
    }
}

2. 用户角色权限体系(数据层)

🗂️ 实体类(数据库映射)

表格

文件名称对应表 / 核心字段
ApeUser.java用户表:id、username、password(加密)、status(状态)、createTime 等
ApeRole.java角色表:id、roleName、roleCode(角色标识)、description 等
ApeMenu.java菜单 / 权限表:id、menuName、permCode(权限标识如 user:add)、parentId、type(菜单 / 按钮)等
ApeUserRole.java用户角色关联表:id、userId、roleId
ApeRoleMenu.java角色菜单关联表:id、roleId、menuId

🔄 服务层实现(业务逻辑)

表格

文件名称核心功能
ApeUserRoleService.java/.impl1. 用户 - 角色关联查询(根据用户 ID 查角色)2. 角色分配 / 移除3. 批量操作用户角色
ApeRoleMenuService.java/.impl1. 角色 - 权限关联查询(根据角色 ID 查权限)2. 权限分配 / 移除3. 批量更新角色权限
ApeUserService.java/.impl1. 用户信息查询(根据用户名 / ID)2. 用户密码加密验证3. 用户状态检查

ApeUserRoleService.java/.impl

package com.ape.apesystem.service;

import com.ape.apesystem.domain.ApeUserRole;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.Set;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 用户角色关系service
 * @date 2023/8/31 14:36
 */
public interface ApeUserRoleService extends IService<ApeUserRole> {

    /**
    * @description: 根据账号获取角色
    * @param: loginAccount
    * @return:
    * @author shaozhujie
    * @date: 2023/9/7 17:01
    */
    Set<String> getUserRolesSet(String loginAccount);

}
package com.ape.apesystem.service.impl;

import com.ape.apesystem.domain.ApeUserRole;
import com.ape.apesystem.mapper.ApeUserRoleMapper;
import com.ape.apesystem.service.ApeUserRoleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

import java.util.Set;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 用户角色关系service实现类
 * @date 2023/8/31 14:37
 */
@Service
public class ApeUserRoleServiceImpl extends ServiceImpl<ApeUserRoleMapper, ApeUserRole> implements ApeUserRoleService {

    /**
     * 根据账号获取角色
     */
    @Override
    public Set<String> getUserRolesSet(String loginAccount) {
        return baseMapper.getUserRolesSet(loginAccount);
    }

}

ApeRoleMenuService.java/.impl

package com.ape.apesystem.service;

import com.ape.apesystem.domain.ApeRoleMenu;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.Set;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 角色菜单关系service
 * @date 2023/8/31 10:57
 */
public interface ApeRoleMenuService extends IService<ApeRoleMenu> {

    /**
     * @description: 根据角色获取权限
     * @param: loginAccount
     * @return:
     * @author shaozhujie
     * @date: 2023/9/7 17:01
     */
    Set<String> getRoleMenusSet(String role);
}
package com.ape.apesystem.service.impl;

import com.ape.apesystem.domain.ApeMenu;
import com.ape.apesystem.mapper.ApeMenuMapper;
import com.ape.apesystem.service.ApeMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 菜单service实现类
 * @date 2023/8/30 9:24
 */
@Service
public class ApeMenuServiceImpl extends ServiceImpl<ApeMenuMapper, ApeMenu> implements ApeMenuService {

    /**
    * 根据用户获取菜单权限
    */
    @Override
    public List<ApeMenu> getMenuByUser(String id) {
        return baseMapper.getMenuByUser(id);
    }
}

ApeUserService.java/.impl

/**
 * 用户模块业务层接口
 * <p>
 * 核心职责:
 * 1. 继承MyBatis-Plus的IService,复用通用CRUD方法(save/delete/update/getById等);
 * 2. 定义用户模块专属的自定义业务方法(分页查询、账号校验、密码重置等)。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/28
 */
public interface ApeUserService extends IService<ApeUser> {
    Page<ApeUser> getUserPage(ApeUser apeUser);
}
package com.ape.apesystem.service.impl;

import com.ape.apesystem.domain.ApeUser;
import com.ape.apesystem.mapper.ApeUserMapper;
import com.ape.apesystem.service.ApeUserService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 用户service实现类
 * @date 2023/8/28 8:45
 */
@Service
public class ApeUserServiceImpl extends ServiceImpl<ApeUserMapper, ApeUser> implements ApeUserService {

    @Override
    public Page<ApeUser> getUserPage(ApeUser apeUser) {
        Page<ApeUser> page = new Page<>(apeUser.getPageNumber(), apeUser.getPageSize());
        return this.page(page);
    }
}

📊 Mapper 层(数据库操作)

表格

文件名称核心 SQL 功能
ApeUserRoleMapper.java1. 根据用户 ID 查询角色 ID 列表2. 根据角色 ID 查询用户列表3. 批量插入 / 删除用户角色关联
ApeRoleMenuMapper.java1. 根据角色 ID 查询权限编码列表2. 根据权限 ID 查询角色列表3. 批量插入 / 删除角色权限关联

ApeUserRoleMapper.java

package com.ape.apesystem.mapper;

import com.ape.apesystem.domain.ApeUserRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.Set;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 用户角色关系mapper
 * @date 2023/8/31 14:34
 */
public interface ApeUserRoleMapper extends BaseMapper<ApeUserRole> {

    /**
     * 根据账号获取角色
     */
    Set<String> getUserRolesSet(@Param("loginAccount") String loginAccount);
}

ApeRoleMenuMapper.java

package com.ape.apesystem.mapper;

import com.ape.apesystem.domain.ApeRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 角色mapper
 * @date 2023/8/31 10:16
 */
public interface ApeRoleMapper extends BaseMapper<ApeRole> {
}

3. 登录认证流程(接口层)

🚪 登录控制器

表格

文件名称核心接口
LoginController.java1. /login:用户登录(用户名 + 密码验证→生成 Token→缓存 Redis)2. /register:用户注册(密码加密存储)3. /logout:用户登出(删除 Redis 中 Token)4. /refreshToken:Token 刷新(生成新 Token,失效旧 Token)5. /resetPwd:密码重置(验证旧密码→加密新密码)

4. 辅助工具类

⚙️ 配置类

表格

文件名称核心功能
RedisConfig.java1. RedisTemplate 配置(序列化方式)2. 缓存过期时间配置3. 自定义 Redis 缓存管理器(供 Shiro 使用)
CorsConfig.java1. 跨域请求配置(允许前端域名访问)2. 放行 Header 中的 Token 字段3. 预检请求(OPTIONS)处理
WebMvcConfig.java1. 拦截器配置2. 静态资源放行3. 请求参数解析器配置
RedisConfig.java
package com.ape.apeframework.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: redis配置类
 * @date 2023/8/11 9:02
 */
@Configuration
public class RedisConfig {

    /**
     * 注入 RedisConnectionFactory
     */
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        setSerializer(redisTemplate, redisConnectionFactory);
        return redisTemplate;
    }

    /**
    * @description: 设置数据存入 redis 的序列化方式
    * @param: redisTemplate
        factory
    * @return:
    * @author shaozhujie
    * @date: 2023/9/14 11:05
    */
    private void setSerializer(RedisTemplate<String, Object> redisTemplate,
                               RedisConnectionFactory factory) {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
    }

}

CorsConfig.java

package com.ape.apeframework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
 * @author shaozhujie
 * @version 1.0
 * @description: 跨域
 * @date 2023/8/28 10:57
 */
@Configuration
public class CorsConfig {

    /**
    * @description: 配置跨域
    * @param:
    * @return:
    * @author shaozhujie
    * @date: 2023/9/14 11:03
    */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许cookies跨域
        corsConfiguration.setAllowCredentials(true);
        // #允许向该服务器提交请求的URI,*表示全部允许,自定义可以添加多个
        corsConfiguration.addAllowedOriginPattern("*");
        // #允许访问的头信息,*表示全部,可以添加多个
        corsConfiguration.addAllowedHeader("*");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        corsConfiguration.setMaxAge(1800L);
        // 允许提交请求的方法,*表示全部允许,一般OPTIONS,GET,POST三个够了
        corsConfiguration.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
}

WebMvcConfig.java

package com.ape.apeframework.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author shaozhujie
 * @version 1.0
 * @description: 图片、视频、文件拦截
 * @date 2023/10/20 8:39
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry){
        //歌手头像地址
        registry.addResourceHandler("/img/**").addResourceLocations(
                "file:"+System.getProperty("user.dir")+System.getProperty("file.separator")+"img"
                        +System.getProperty("file.separator")+System.getProperty("file.separator")
        );

        registry.addResourceHandler("/video/**").addResourceLocations(
                "file:"+System.getProperty("user.dir")+System.getProperty("file.separator")+"video"
                        +System.getProperty("file.separator")+System.getProperty("file.separator")
        );

        registry.addResourceHandler("/file/**").addResourceLocations(
                "file:"+System.getProperty("user.dir")+System.getProperty("file.separator")+"file"
                        +System.getProperty("file.separator")+System.getProperty("file.separator")
        );
    }

}

🛠️ 通用工具类

表格

文件名称核心功能
ShiroUtils.java1. 获取当前登录用户信息2. 检查当前用户角色 / 权限3. 退出 Shiro 登录4. Shiro 上下文操作
RedisUtils.java1. Redis 通用 CRUD 操作(String/Hash/List)2. 缓存过期时间设置3. 批量删除缓存4. Token 缓存专用方法(存入 / 查询 / 删除)
PasswordUtils.java1. 密码加密(MD5/SHA256 + 盐值 + 迭代)2. 密码验证(明文→加密后对比)3. 随机盐值生成
RequestUtils.java1. 从 Request 中提取 Token2. 获取客户端 IP3. 解析请求参数4. 响应结果封装

PasswordUtils.java

// 定义工具类所在的包路径
package com.ape.apecommon.utils;

// 导入Spring框架的MD5加密工具类(用于生成MD5哈希值)
import org.springframework.util.DigestUtils;
// 导入Spring框架的字符串工具类(用于非空/长度校验)
import org.springframework.util.StringUtils;
// 导入UUID工具类(用于生成随机盐值)
import java.util.UUID;

/**
* @description: 密码工具类(MD5加盐加密/解密验证)
* @author shaozhujie
* @date 2023/9/1 10:20
* @version 1.0
*/
// 定义密码工具类(工具类统一使用静态方法,无需实例化)
public class PasswordUtils {

    /**
    * @description: 加盐加密(自动生成盐值)
    * @param: password 明文密码
    * @return: String 加密结果,格式为「32位盐值$32位MD5加密密码」
    * @author shaozhujie
    * @date: 2023/9/1 10:21
    */
    // 静态加密方法:无盐值入参,自动生成随机盐值
    public static String encrypt(String password) {
        // 1. 生成32位随机盐值:UUID去除横线(UUID原始格式含4个横线,移除后为32位)
        String salt = UUID.randomUUID().toString().replace("-", "");
        // 2. 加密核心逻辑:盐值+明文密码拼接后,通过MD5生成16进制哈希字符串
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 3. 拼接盐值和加密密码:用$分隔,便于后续解密时拆分盐值
        String dbPassword = salt + "$" + finalPassword;
        // 返回最终加密结果(存入数据库的密码格式)
        return dbPassword;
    }
    /**
     * ===== 密码加密示例 =====
     * 原始明文密码:123456
     * 最终加密结果(存入数据库):7f9e8d7c6b5a4938271605f4e3d2c1b0$e10adc3949ba59abbe56e057f20f883e
     *
     * ===== 拆分结果 =====
     * 1. 随机生成的盐值(32位):7f9e8d7c6b5a4938271605f4e3d2c1b0
     * 2. 盐值+明文密码的MD5加密结果(32位):e10adc3949ba59abbe56e057f20f883e
     * 3. 盐值长度:32
     * 4. MD5加密结果长度:32
     * 5. 最终加密字符串总长度:65
     */


    /**
     * @description: 加盐加密(指定盐值)
     * @param: password 明文密码
     * @param: salt 自定义盐值(通常为数据库中存储的盐值)
     * @return: String 加密结果,格式为「盐值$32位MD5加密密码」
     * @author shaozhujie
     * @date: 2023/9/1 10:21
     */
    // 重载加密方法:手动传入盐值,用于解密验证时生成对比密码
    public static String encrypt(String password, String salt) {
        // 1. 加密核心逻辑:指定盐值+明文密码拼接后,生成MD5哈希字符串
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 2. 拼接盐值和加密密码:保持与自动生成盐值的格式一致
        String dbPassword = salt + "$" + finalPassword;
        // 返回拼接后的加密结果
        return dbPassword;
    }

    /**
     * @description: 验证加盐加密密码
     * @param: password 待验证的明文密码
     * @param: dbPassword 数据库存储的加密密码(格式:盐值$MD5加密密码)
     * @return: boolean 验证结果(true=密码正确,false=密码错误/参数异常)
     * @author shaozhujie
     * @date: 2023/9/1 10:21
     */
    // 静态解密验证方法:对比明文密码+盐值加密后是否与库中密码一致
    public static boolean decrypt(String password, String dbPassword) {
        // 初始化验证结果为false(默认密码错误)
        boolean result = false;
        // 参数合法性校验:
        // 1. 明文密码非空且有长度 2. 库中密码非空且有长度 
        // 3. 库中密码长度必须为65(32位盐+1位$+32位MD5) 4. 库中密码包含$分隔符
        if (StringUtils.hasLength(password) && StringUtils.hasLength(dbPassword) &&
                dbPassword.length() == 65 && dbPassword.contains("$")) {
            // 1. 按$拆分库中密码($需转义,避免正则匹配),得到盐值和加密密码数组
            String[] passwrodArr = dbPassword.split("\$");
            // 1.1 提取拆分后的盐值(数组第一个元素)
            String salt = passwrodArr[0];
            // 2. 用待验证的明文密码+库中盐值重新加密,生成对比密码
            String checkPassword = encrypt(password, salt);
            // 对比:重新加密的密码是否与库中密码完全一致
            if (dbPassword.equals(checkPassword)) {
                // 一致则验证成功,修改结果为true
                result = true;
            }
        }
        // 返回最终验证结果(参数异常/密码错误均返回false)
        return result;
    }
}

🚀 完整业务流程

1. 认证流程(Authentication)

image.png

2. 授权流程(Authorization)

image.png

3. 登出流程(Logout)

image.png

📌 核心设计要点

  1. 无状态认证:基于 JWT Token 实现无状态认证,服务端无需存储会话,仅通过Redis缓存Token状态
  2. 权限缓存:用户角色/ 权限首次查询后缓存到 Redis,避免重复查询数据库,提升性能
  3. 多层校验:JwtFilter 前置校验 Token 格式→ShiroRealm 校验用户状态→Shiro 注解校验接口权限
  4. 安全加固:密码加密(盐值 + 迭代)、Token 过期机制、登出即时失效、跨域安全配置