这是一个基于 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)

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