feat: 新增heritage-plugins安全插件(第2部分)

- 新增heritage-limit:接口限流插件,基于Redis实现
- 新增heritage-repect:防重复提交插件,防止表单重复提交
- 新增heritage-sa-token:Sa-Token认证插件,提供登录认证和权限验证
This commit is contained in:
Leo 2025-10-08 02:07:11 +08:00
parent 09457ecebd
commit 2c31bf6d53
22 changed files with 1225 additions and 0 deletions

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-limit</name>
<artifactId>heritage-limit</artifactId>
<description>限流插件</description>
<dependencies>
<!-- 全局公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- Aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,15 @@
package org.leocoder.heritage.limit.anno;
import org.leocoder.heritage.limit.config.CoderRedisLimitUtil;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({CoderRedisLimitUtil.class})
public @interface EnableCoderLimit {
}

View File

@ -0,0 +1,83 @@
package org.leocoder.heritage.limit.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.leocoder.heritage.common.anno.CoderLimit;
import org.leocoder.heritage.common.enmus.limit.LimitType;
import org.leocoder.heritage.common.exception.coder.ParamsException;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.leocoder.heritage.limit.config.CoderRedisLimitUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
/**
* @author Leocoder
* @description [CoderLimitAspect限流处理]
*/
@Slf4j
@Aspect
@Order(2)
@Component
public class CoderLimitAspect {
@Autowired
private CoderRedisLimitUtil CoderRedisLimitUtil;
/**
* @description [前置通知判断是否超出限流次数
*/
@Before("@annotation(limit)")
public void doBefore(JoinPoint point, CoderLimit limit) {
try {
// log.info("限流开始进入 =>");
// 拼接key
String key = getCombineKey(limit, point);
// 判断是否超出限流次数
if (!CoderRedisLimitUtil.limit(key, limit.count(), limit.time())) {
throw new ParamsException(limit.message());
}
} catch (ParamsException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("接口限流异常,请稍候再试");
}
}
/**
* @description [根据限流类型拼接key
*/
public String getCombineKey(CoderLimit limit, JoinPoint point) {
// 获取服务请求的对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
StringBuilder sb = new StringBuilder(limit.prefix());
// 按照IP限流
if (limit.type() == LimitType.IP) {
String ipAddr = IpUtil.getIpAddr(request);
// 检查字符串中是否包含逗号这种情况设置waf会出现多个IP第一个是真实IP后面的都是阿里云IP
int commaIndex = ipAddr.indexOf(',');
if (commaIndex > -1) {
// 如果有逗号取逗号前的部分
ipAddr = ipAddr.substring(0, commaIndex);
}
sb.append(ipAddr).append(":");
// log.info("限流IP{}", ipAddr);
}
// 拼接类名和方法名
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
sb.append(targetClass.getName()).append("-").append(method.getName());
return sb.toString();
}
}

View File

@ -0,0 +1,54 @@
package org.leocoder.heritage.limit.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* @author Leocoder
* @description [RedisLimitUtil接口限流]
*/
@Slf4j
@Component
public class CoderRedisLimitUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* @param key
* @param count 限流次数
* @param times 限流时间
* @description [限流]
* 通过 Lua 脚本根据 Redis 中缓存的键值判断限流时间也是 key 的过期时间访问次数是否超出了限流次数没超出则访问次数 +1返回 true超出了则返回 false
*/
public boolean limit(String key, int count, int times) {
try {
String script = "local lockKey = KEYS[1]\n" +
"local lockCount = KEYS[2]\n" +
"local lockExpire = KEYS[3]\n" +
"local currentCount = tonumber(redis.call('get', lockKey) or \"0\")\n" +
"if currentCount < tonumber(lockCount)\n" +
"then\n" +
" redis.call(\"INCRBY\", lockKey, \"1\")\n" +
" redis.call(\"expire\", lockKey, lockExpire)\n" +
" return true\n" +
"else\n" +
" return false\n" +
"end";
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
List<String> keys = Arrays.asList(key, String.valueOf(count), String.valueOf(times));
return Boolean.TRUE.equals(redisTemplate.execute(redisScript, keys));
} catch (Exception e) {
log.error("限流脚本执行失败:{}", e.getMessage());
}
return false;
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-repect</name>
<artifactId>heritage-repect</artifactId>
<description>防重复提交插件</description>
<dependencies>
<!-- 通用公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- Aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
package org.leocoder.heritage.repect.anno;
import org.leocoder.heritage.repect.aspect.RedisService;
import org.leocoder.heritage.repect.aspect.RepeatSubmitAspect;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({RepeatSubmitAspect.class, RedisService.class})
public @interface EnableCoderRepeatSubmit {
}

View File

@ -0,0 +1,206 @@
package org.leocoder.heritage.repect.aspect;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author Leocoder
* @description [RedisService]
*/
@Component
@RequiredArgsConstructor
public class RedisService {
public final RedisTemplate redisTemplate;
/**
* @param key 缓存的键值
* @param value 缓存的值
* @return 缓存的对象
* @description [缓存基本的对象IntegerString实体类等]
*/
public <T> ValueOperations<String, T> setCacheObject(String key, T value) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, value);
return operation;
}
/**
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
* @return 缓存的对象
* @description [缓存基本的对象IntegerString实体类等]
*/
public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, value, timeout, timeUnit);
return operation;
}
/**
* @param key 缓存键值
* @return 缓存键值对应的数据
* @description [获得缓存的基本对象]
*/
public <T> T getCacheObject(String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* @description [删除单个对象]
*/
public void deleteObject(String key) {
redisTemplate.delete(key);
}
/**
* @description [删除集合对象]
*/
public void deleteObject(Collection collection) {
redisTemplate.delete(collection);
}
/**
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
* @description [缓存List数据]
*/
public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList) {
ListOperations listOperation = redisTemplate.opsForList();
if (null != dataList) {
int size = dataList.size();
for (int i = 0; i < size; i++) {
listOperation.leftPush(key, dataList.get(i));
}
}
return listOperation;
}
/**
* @param key 缓存的键值
* @return 缓存键值对应的数据
* @description [获得缓存的list对象]
*/
public <T> List<T> getCacheList(String key) {
List<T> dataList = new ArrayList<>();
ListOperations<String, T> listOperation = redisTemplate.opsForList();
Long size = listOperation.size(key);
for (int i = 0; i < size; i++) {
dataList.add(listOperation.index(key, i));
}
return dataList;
}
/**
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
* @description [缓存Set]
*/
public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* @description [获得缓存的set]
*/
public <T> Set<T> getCacheSet(String key) {
Set<T> dataSet = new HashSet<>();
BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
dataSet = operation.members();
return dataSet;
}
/**
* @description [缓存Map]
*/
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) {
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<String, T> entry : dataMap.entrySet()) {
hashOperations.put(key, entry.getKey(), entry.getValue());
}
}
return hashOperations;
}
/**
* @description [获得缓存的Map]
*/
public <T> Map<String, T> getCacheMap(String key) {
Map<String, T> map = redisTemplate.opsForHash().entries(key);
return map;
}
/**
* @param pattern 字符串前缀
* @return 对象列表
* @description [获得缓存的基本对象列表]
*/
public Collection<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* @description [此key是否存在]
*/
public boolean haskey(String key) {
return redisTemplate.hasKey(key);
}
/**
* @description [获取key的过期时间]
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
public <T> ValueOperations<String, T> setBillObject(String key, List<Map<String, Object>> value) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, (T) value);
return operation;
}
/**
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
* @return 缓存的对象
* @description [缓存list<Map < String, Object>>]
*/
public <T> ValueOperations<String, T> setBillObject(String key, List<Map<String, Object>> value, Integer timeout, TimeUnit timeUnit) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, (T) value, timeout, timeUnit);
return operation;
}
/**
* @description [缓存Map]
*/
public <T> HashOperations<String, String, T> setCKdBillMap(String key, Map<String, T> dataMap) {
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<String, T> entry : dataMap.entrySet()) {
hashOperations.put(key, entry.getKey(), entry.getValue());
}
}
return hashOperations;
}
}

View File

@ -0,0 +1,91 @@
package org.leocoder.heritage.repect.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.leocoder.heritage.common.anno.CoderRepeatSubmit;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.exception.RepeatSubmitException;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;
/**
* @author Leocoder
* @description [NoRepeatSubmitAop]
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class RepeatSubmitAspect {
@Autowired
private RedisService redisService;
@Value("${sa-token.token-name}")
private String tokenName;
@Value("${sa-token.token-prefix}")
private String tokenPrefix;
@Around(value = "@annotation(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, CoderRepeatSubmit repeatSubmit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
// 请求地址
HttpServletRequest request = attributes.getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// System.out.println("方法名称:"+ signature.getMethod().getName());
// System.out.println("方法类型:"+ signature.getReturnType());
// System.out.println("参数名称:"+ Arrays.toString(signature.getParameterNames()));
// System.out.println("参数类型:"+ Arrays.toString(signature.getParameterTypes()));
// 获得客户端的IP地址
String userIp = IpUtil.getIpAddr(request);
// 检查字符串中是否包含逗号这种情况设置waf会出现多个IP第一个是真实IP后面的都是阿里云IP
int commaIndex = userIp.indexOf(',');
if (commaIndex > -1) {
// 如果有逗号取逗号前的部分
userIp = userIp.substring(0, commaIndex);
}
// 针对某个人的话就是用token如果没有token就使用客户端IP进行辨别
String authorization = request.getHeader(tokenName);
String header = null;
if (StringUtils.isNotBlank(authorization)) {
header = authorization.replace(" ", "").replace(tokenPrefix, "");
}
// 定义redis的key
String key = null;
if (StringUtils.isNotBlank(header)) {
// 这里是唯一标识根据情况而定里面添加用户ID或者IP地址最好否则同一个接口一秒只能使用一次
key = repeatSubmit.prefix() + userIp + ":[" + signature.getMethod().getName() + "-" + request.getServletPath() + "-" + header + "]";
} else {
// 这里是唯一标识根据情况而定里面添加用户ID或者IP地址最好否则同一个接口一秒只能使用一次
key = repeatSubmit.prefix() + userIp + ":[" + signature.getMethod().getName() + "-" + request.getServletPath() + "]";
}
// log.info("重复提交操作电脑IP{}", userIp);
// log.info("重复提交redis-key{}", key);
// 如果缓存中有这个url视为重复提交
if (!redisService.haskey(key)) {
// 通过执行下一步
Object o = joinPoint.proceed();
// 然后存入redis并且设置1s倒计时
redisService.setCacheObject(key, CoderCacheConstants.REPEAT_SUBMIT_KEY, repeatSubmit.value(), TimeUnit.MILLISECONDS);
// 返回结果
return o;
} else {
throw new RepeatSubmitException(500, repeatSubmit.message());
}
}
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-sa-token</name>
<artifactId>heritage-sa-token</artifactId>
<description>Sa-Token模块</description>
<dependencies>
<!-- 全局公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- MyBatisPlus模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-mybatisplus</artifactId>
<version>${revision}</version>
</dependency>
<!-- Aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Sa-Token 权限认证 start -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 Redis[使用 jackson 序列化方式] -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- 注意:无论使用哪种序列化方式,你都必须为项目提供一个 Redis 实例化方案 -->
<!-- 提供Redis连接池[Sa-Token] -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
package org.leocoder.heritage.satoken.anno;
import org.leocoder.heritage.satoken.config.CoderSaTokenFilter;
import org.leocoder.heritage.satoken.config.CoderSaTokenInterceptor;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ CoderSaTokenInterceptor.class, CoderSaTokenFilter.class })
public @interface EnableCoderSaToken {
}

View File

@ -0,0 +1,50 @@
package org.leocoder.heritage.satoken.config;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Leocoder
* @description [Sa-Token代码方式进行配置]
*/
@Configuration
public class CoderSaTokenFilter {
/**
* @description [全局过滤器-只用来设置跨域资源和开启浏览器默认XSS防护]
* @author Leocoder
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 前置函数在每次认证函数之前执行
.setBeforeAuth(r -> {
// ---------- 设置一些安全响应头 ----------
SaHolder.getResponse()
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Access-Control-Allow-Methods", "*")
.setHeader("Access-Control-Max-Age", "3600")
.setHeader("Access-Control-Allow-Headers", "*")
.setHeader("Content-Type", "application/json;charset=UTF-8")
// 服务器名称
.setServer("Coder-Admin")
// 是否可以在iframe显示视图 DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
.setHeader("X-Frame-Options", "SAMEORIGIN")
// 是否启用浏览器默认XSS防护 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时停止渲染页面
.setHeader("X-XSS-Protection", "1; mode=block")
// 禁用浏览器内容嗅探
.setHeader("X-Content-Type-Options", "nosniff");
// 如果是预检请求则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(obj -> {
})
.back();
});
}
}

View File

@ -0,0 +1,68 @@
package org.leocoder.heritage.satoken.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* @author Leocoder
* @description [Sa-Token代码方式进行配置]
*/
@Configuration
public class CoderSaTokenInterceptor implements WebMvcConfigurer {
@Value("${coder.filePath}")
private String baseFilePath;
/**
* @description [注册拦截器]
* 使用后必须携带 Authorization[此名称在yml中可自行配置]- Bearer token值[除非不被拦截但是获取不到当前会话用户ID]
* @author Leocoder
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册登录拦截器
registry.addInterceptor(new SaInterceptor(handle -> {
// 白名单
List<String> ignoreUrls = new ArrayList<>();
// favicon.ico浏览器标签Logo
ignoreUrls.add("/favicon.ico");
// 验证码
ignoreUrls.add("/captcha/**");
// 登录退出登录
ignoreUrls.add("/auth/**");
// 测试接口
// ignoreUrls.add("/coder/**");
// 上传文件接口
ignoreUrls.add("/CoderFile/**");
// 后端项目详情
ignoreUrls.add("/");
// 静态资源
// ignoreUrls.add("/*.html");
// ignoreUrls.add("/**/*.html");
// ignoreUrls.add("/**/*.css");
// ignoreUrls.add("/**/*.js");
// 上传路径
ignoreUrls.add(baseFilePath + "/**");
// Swagger API文档相关路径
ignoreUrls.add("/swagger-ui/**");
ignoreUrls.add("/v3/api-docs/**");
ignoreUrls.add("/v3/api-docs");
ignoreUrls.add("/swagger-ui.html");
ignoreUrls.add("/webjars/**");
// 除白名单路径外均需要登录认证
SaRouter.match("/**").notMatch(ignoreUrls).check(r -> StpUtil.checkLogin());
})).addPathPatterns("/**");
}
}

View File

@ -0,0 +1,159 @@
package org.leocoder.heritage.satoken.config;
import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.mybatisplus.mapper.system.SysLoginUserMapper;
import org.leocoder.heritage.satoken.service.loginlog.SaLoginLogService;
import org.springframework.stereotype.Component;
/**
* @author Leocoder
* @description [自定义侦听器的实现]
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class CoderSaTokenListener implements SaTokenListener {
private final SysLoginUserMapper sysLoginUserMapper;
private final SaLoginLogService saLoginLogService;
/**
* @description [每次登录时触发]
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
log.info("自定义侦听器实现-doLogin");
log.info("登录类型:{}, 登录ID{}, Token值{}, 登录Model{}", loginType, loginId, tokenValue, loginParameter);
}
/**
* @description [每次注销时触发]
*/
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
log.info("自定义侦听器实现 doLogout");
// 保存注销日志
saLoginLogService.addLoginLog(sysLoginUserMapper.selectById(Long.valueOf(String.valueOf(loginId))).getLoginName(), CoderConstants.ZERO_STRING, "退出登录");
}
/**
* @description [每次被踢下线时触发]
*/
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
log.info("自定义侦听器实现 doKickout");
// 保存踢下线日志
saLoginLogService.addLoginLog(sysLoginUserMapper.selectById(Long.valueOf(String.valueOf(loginId))).getLoginName(), CoderConstants.ZERO_STRING, "强退下线");
}
/**
* @description [每次被顶下线时触发]
*/
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
log.info("自定义侦听器实现 doReplaced");
// 保存被顶下线日志
saLoginLogService.addLoginLog(sysLoginUserMapper.selectById(Long.valueOf(String.valueOf(loginId))).getLoginName(), CoderConstants.ZERO_STRING, "被顶下线");
}
/**
* @description [每次被封禁时触发]
*/
@Override
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
log.info("自定义侦听器实现 doDisable");
// 保存被封禁时日志
saLoginLogService.addLoginLog(sysLoginUserMapper.selectById(Long.valueOf(String.valueOf(loginId))).getLoginName(), CoderConstants.ZERO_STRING, "账号被封禁");
}
/**
* @description [每次被解封时触发]
*/
@Override
public void doUntieDisable(String loginType, Object loginId, String service) {
log.info("自定义侦听器实现 doUntieDisable");
// 保存被解封时日志
saLoginLogService.addLoginLog(sysLoginUserMapper.selectById(Long.valueOf(String.valueOf(loginId))).getLoginName(), "0", "账号被解封");
}
/**
* @description [每次二级认证时触发]
*/
@Override
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
log.info("自定义侦听器实现 doOpenSafe");
}
/**
* @description [每次退出二级认证时触发]
*/
@Override
public void doCloseSafe(String loginType, String tokenValue, String service) {
log.info("自定义侦听器实现 doCloseSafe");
}
/**
* @description [每次创建Session时触发]
*/
@Override
public void doCreateSession(String id) {
log.info("自定义侦听器实现 doCreateSession");
}
/**
* @description [每次注销Session时触发]
*/
@Override
public void doLogoutSession(String id) {
log.info("自定义侦听器实现 doLogoutSession");
}
/**
* @description [每次Token续期时触发]
*/
@Override
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
log.info("自定义侦听器实现 doRenewTimeout");
}
/**
* @description [全局组件载入成功]
*/
@Override
public void doRegisterComponent(String compName, Object compObj) {
log.info("全局组件载入成功 doRegisterComponent");
}
/**
* @description [注解扩展]
*/
@Override
public void doRegisterAnnotationHandler(SaAnnotationHandlerInterface<?> handler) {
log.info("注解扩展实现 doRegisterAnnotationHandler");
}
/**
* @description [会话组件重置]
*/
@Override
public void doSetStpLogic(StpLogic stpLogic) {
log.info("会话组件重置成功 doSetStpLogic");
}
/**
* @description [全局配置]
*/
@Override
public void doSetConfig(SaTokenConfig config) {
log.info("全局配置实现 doSetConfig");
}
}

View File

@ -0,0 +1,39 @@
package org.leocoder.heritage.satoken.config;
import cn.dev33.satoken.secure.SaSecureUtil;
/**
* @author Leocoder
* @description [PasswordUtil-用户名+密码加密然后跟数据库进行对比]
*/
public class CoderSaTokenPasswordUtil {
/**
* MD5加密后迭代次数默认2次
*/
private static final int MD5ENCRYPTNUMBER = 2;
private CoderSaTokenPasswordUtil() {
throw new AssertionError();
}
/**
* @param salt 盐值随机数
* @param password 密码
* @description [字符串加密函数MD5实现]
*/
public static String getPassword(String password, String salt) {
String initMd5Pwd = password + salt;
for (int i = 0; i < MD5ENCRYPTNUMBER; i++) {
initMd5Pwd = SaSecureUtil.md5(initMd5Pwd);
}
return initMd5Pwd;
}
public static void main(String[] args) {
System.out.println("管理员密码:" + getPassword("123456", "20231123"));
System.out.println("Coder密码" + getPassword("123456", "666666"));
System.out.println("YXT密码" + getPassword("123456", "666666"));
}
}

View File

@ -0,0 +1,74 @@
package org.leocoder.heritage.satoken.config;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.collection.CollectionUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.satoken.service.menu.SaMenuService;
import org.leocoder.heritage.satoken.service.role.SaRoleService;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Leocoder
* @description [StpInterfaceImpl自定义权限验证接口扩展]
*/
@Slf4j
@RequiredArgsConstructor
// 保证此类被SpringBoot扫描完成Sa-Token的自定义权限验证扩展
@Component
public class CoderSaTokenStpInterfaceImpl implements StpInterface {
private final SaRoleService saRoleService;
private final SaMenuService saMenuService;
/**
* @description [返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)]
* @author Leocoder
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 1数据库查询用户拥有的角色码
List<String> roleCodeList = saRoleService.listAuthRoleCode(Long.valueOf(loginId.toString()));
// 2返回角色码集合
return roleCodeList;
}
/**
* @description [返回一个账号所拥有的权限码集合]
* @author Leocoder
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 1声明权限码集合
Set<String> permissionSet = new HashSet<>();
// 2遍历角色列表查询拥有的角色权限码
List<String> roleKeyList = getRoleList(loginId, loginType);
if (CollectionUtil.isEmpty(roleKeyList)) {
return new ArrayList<>(permissionSet);
}
// 3角色判断超级管理员 roleKeyList.contains(CoderConstants.CODER_ADMIN)
if (CoderLoginUtil.getIsCoderAdmin()) {
permissionSet.add("*");
return new ArrayList<>(permissionSet);
}
// 4角色判断其他角色
List<String> menuAuthlist = null;
for (String roleKey : roleKeyList) {
// 5根据角色码查询拥有的权限码
menuAuthlist = saMenuService.listMenuAuth(roleKey);
YUtil.isTrue(CollectionUtil.isEmpty(menuAuthlist), "该用户角色未分配菜单权限,禁止登录");
permissionSet.addAll(menuAuthlist);
}
// 6返回权限码集合
return new ArrayList<>(permissionSet);
}
}

View File

@ -0,0 +1,39 @@
package org.leocoder.heritage.satoken.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Leocoder
* @description [SaTokenConfigure-Sa-Token 使用全局拦截器完成注解鉴权功能为了不为项目带来不必要的性能负担拦截器默认处于关闭状态
* 因此为了使用注解鉴权你必须手动将 Sa-Token 的全局拦截器注册到你项目中]
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
// 排除静态资源
"/picture/**",
// 排除其他静态资源
"/favicon.ico",
"/static/**",
"/css/**",
"/js/**",
"/img/**",
// 排除API文档
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html",
"/webjars/**"
);
}
}

View File

@ -0,0 +1,17 @@
package org.leocoder.heritage.satoken.service.loginlog;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysLoginLog;
/**
* @author Leocoder
* @description [SysLoginLogService]
*/
public interface SaLoginLogService extends IService<SysLoginLog> {
/**
* @description [保存登录日志]
* @author Leocoder
*/
void addLoginLog(String loginName, String loginStatus, String message);
}

View File

@ -0,0 +1,75 @@
package org.leocoder.heritage.satoken.service.loginlog;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.enmus.log.ClientType;
import org.leocoder.heritage.common.utils.ip.IpAddressUtil;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.leocoder.heritage.common.utils.ip.ServletUtil;
import org.leocoder.heritage.domain.pojo.system.SysLoginLog;
import org.leocoder.heritage.mybatisplus.mapper.system.SysLoginLogMapper;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* @author Leocoder
* @description [SysLoginLogServiceImpl]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SaLoginLogServiceImpl extends ServiceImpl<SysLoginLogMapper, SysLoginLog> implements SaLoginLogService {
private final IpAddressUtil ipAddressUtil;
/**
* @description [保存登录日志]
* @author Leocoder
*/
@Override
public void addLoginLog(String loginName, String loginStatus, String message) {
// 1new一个登录日志对象用来装载信息
SysLoginLog sysLoginLog = new SysLoginLog();
try {
sysLoginLog.setLoginName(loginName);
sysLoginLog.setLoginStatus(loginStatus);
// LocalDateTime.now() 时间类型为 LocalDateTime类型
sysLoginLog.setLoginTime(LocalDateTime.now());
sysLoginLog.setMessage(message);
// 2登录IP地址
sysLoginLog.setLoginIp(IpUtil.getIpAddr(ServletUtil.getRequest()));
// 3登录地理位置
sysLoginLog.setLoginAddress(ipAddressUtil.getAddress(sysLoginLog.getLoginIp()));
// 4UserAgent信息
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtil.getRequest().getHeader("User-Agent"));
// 5登录浏览器
sysLoginLog.setBrowser(userAgent.getBrowser().getName());
// 6登录操作系统
sysLoginLog.setOs(userAgent.getOperatingSystem().getName());
// 7登录设备
String deviceName = userAgent.getOperatingSystem().getDeviceType().getName();
if (StringUtils.isBlank(deviceName)) {
deviceName = "Default";
} else if ("Computer".equals(deviceName)) {
deviceName = ClientType.PC.name();
} else if ("Mobile".equals(deviceName)) {
deviceName = ClientType.MOBILE.name();
}
sysLoginLog.setDeviceName(deviceName);
} catch (Exception e) {
sysLoginLog.setMessage(e.getMessage());
sysLoginLog.setLoginStatus("1");
log.error("登录日志异常信息:{}", e.getMessage());
e.printStackTrace();
} finally {
// 8登录日志保存
this.save(sysLoginLog);
}
}
}

View File

@ -0,0 +1,20 @@
package org.leocoder.heritage.satoken.service.menu;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysMenu;
import java.util.List;
/**
* @author Leocoder
* @description [SaMenuService]
*/
public interface SaMenuService extends IService<SysMenu> {
/**
* @description [根据角色编码查询菜单权限-Sa-Token权限]
* @author Leocoder
*/
List<String> listMenuAuth(String roleCode);
}

View File

@ -0,0 +1,32 @@
package org.leocoder.heritage.satoken.service.menu;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.pojo.system.SysMenu;
import org.leocoder.heritage.mybatisplus.mapper.system.SysMenuMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Leocoder
* @description [SysMenuServiceImpl]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SaMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SaMenuService {
private final SysMenuMapper menuMapper;
/**
* @description [根据角色编码查询菜单权限-Sa-Token权限]
* @author Leocoder
*/
@Override
public List<String> listMenuAuth(String roleCode) {
return menuMapper.listMenuAuth(roleCode);
}
}

View File

@ -0,0 +1,19 @@
package org.leocoder.heritage.satoken.service.role;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import java.util.List;
/**
* @author Leocoder
* @description [SaRoleService]
*/
public interface SaRoleService extends IService<SysRole> {
/**
* @description [查询用户拥有正常角色-Sa-Token角色权限]
* @author Leocoder
*/
List<String> listAuthRoleCode(Long userId);
}

View File

@ -0,0 +1,32 @@
package org.leocoder.heritage.satoken.service.role;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import org.leocoder.heritage.mybatisplus.mapper.system.SysRoleMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Leocoder
* @description [SysRoleServiceImpl]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SaRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SaRoleService {
private final SysRoleMapper roleMapper;
/**
* @description [查询用户拥有正常角色-Sa-Token角色权限]
* @author Leocoder
*/
@Override
public List<String> listAuthRoleCode(Long userId) {
return roleMapper.listAuthRoleCode(userId);
}
}