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

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

📁 核心权限控制文件目录

1. 认证与授权核心组件

🔐 Shiro 核心配置类

表格

文件名称 核心功能
ShiroConfig.java 1. 配置 URL 过滤规则(匿名访问 / 需要认证的接口划分)2. 集成 JWT 过滤器到 Shiro 过滤链3. 配置 Redis 缓存管理器(缓存用户权限 / 角色)4. 启用 Shiro 注解支持(如@RequiresRoles/@RequiresPermissions)5. 初始化 SecurityManager 并关联自定义 Realm
java 复制代码
@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.java 1. 重写doGetAuthenticationInfo:实现用户认证逻辑(JWT Token 有效性校验)2. 重写doGetAuthorizationInfo:实现权限授权逻辑(加载用户角色 / 权限)3. JWT Token 签名验证、过期时间检查4. 用户状态校验(如是否禁用 / 注销)
java 复制代码
@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.java 1. 拦截所有请求,提取 Header 中的 JWT Token2. 预处理 Token,校验格式和有效性3. 捕获认证异常并统一返回(如 Token 过期 / 无效)4. 将有效 Token 封装为 JwtToken 交给 Shiro 处理
JwtToken.java 1. 自定义 Token 类,封装 JWT 字符串2. 实现 Shiro 的 AuthenticationToken 接口3. 提供 Token 获取 / 设置方法
JwtUtil.java 1. Token 生成(含用户 ID / 过期时间 / 签名)2. Token 验证(签名校验、过期检查)3. Token 解析(提取用户信息)4. 结合 Redis 实现 Token 黑名单 / 刷新机制

JwtFilter.java

typescript 复制代码
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

typescript 复制代码
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

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/.impl 1. 用户 - 角色关联查询(根据用户 ID 查角色)2. 角色分配 / 移除3. 批量操作用户角色
ApeRoleMenuService.java/.impl 1. 角色 - 权限关联查询(根据角色 ID 查权限)2. 权限分配 / 移除3. 批量更新角色权限
ApeUserService.java/.impl 1. 用户信息查询(根据用户名 / ID)2. 用户密码加密验证3. 用户状态检查

ApeUserRoleService.java/.impl

java 复制代码
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);

}
scala 复制代码
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

java 复制代码
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);
}
scala 复制代码
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

markdown 复制代码
/**
 * 用户模块业务层接口
 * <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);
}
scala 复制代码
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.java 1. 根据用户 ID 查询角色 ID 列表2. 根据角色 ID 查询用户列表3. 批量插入 / 删除用户角色关联
ApeRoleMenuMapper.java 1. 根据角色 ID 查询权限编码列表2. 根据权限 ID 查询角色列表3. 批量插入 / 删除角色权限关联

ApeUserRoleMapper.java

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

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.java 1. /login:用户登录(用户名 + 密码验证→生成 Token→缓存 Redis)2. /register:用户注册(密码加密存储)3. /logout:用户登出(删除 Redis 中 Token)4. /refreshToken:Token 刷新(生成新 Token,失效旧 Token)5. /resetPwd:密码重置(验证旧密码→加密新密码)

4. 辅助工具类

⚙️ 配置类

表格

文件名称 核心功能
RedisConfig.java 1. RedisTemplate 配置(序列化方式)2. 缓存过期时间配置3. 自定义 Redis 缓存管理器(供 Shiro 使用)
CorsConfig.java 1. 跨域请求配置(允许前端域名访问)2. 放行 Header 中的 Token 字段3. 预检请求(OPTIONS)处理
WebMvcConfig.java 1. 拦截器配置2. 静态资源放行3. 请求参数解析器配置
RedisConfig.java
typescript 复制代码
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

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

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

PasswordUtils.java

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)

2. 授权流程(Authorization)

3. 登出流程(Logout)

📌 核心设计要点

  1. 无状态认证:基于 JWT Token 实现无状态认证,服务端无需存储会话,仅通过Redis缓存Token状态
  2. 权限缓存:用户角色/ 权限首次查询后缓存到 Redis,避免重复查询数据库,提升性能
  3. 多层校验:JwtFilter 前置校验 Token 格式→ShiroRealm 校验用户状态→Shiro 注解校验接口权限
  4. 安全加固:密码加密(盐值 + 迭代)、Token 过期机制、登出即时失效、跨域安全配置
相关推荐
扶苏10021 小时前
深入理解 Vue 3 的 watch
前端·javascript·vue.js
前端 贾公子1 小时前
组件 v-model 的封装实现原理及 Input 组件的核心实现(上)
服务器·前端·javascript
weixin199701080161 小时前
亚马逊商品详情页前端性能优化实战
前端·性能优化
全栈前端老曹2 小时前
【Redis】 监控与慢查询日志 —— slowlog、INFO 命令、RedisInsight 可视化监控
前端·数据库·redis·缓存·全栈·数据库监控·slowlog
扶苏10022 小时前
Vue 3 的组合式 API(Composition API)优势
前端·javascript·vue.js
万少2 小时前
这可能是程序员离用AI赚钱最容易的一个机会了
前端·ai编程
范什么特西2 小时前
狂神---死锁
java·前端·javascript
weixin199701080162 小时前
易贝(eBay)商品详情页前端性能优化实战
前端·性能优化
用户600071819102 小时前
【翻译】Rolldown 工作原理解析:符号关联、CJS/ESM 模块解析与导出分析
前端