Compare commits

...

17 Commits

Author SHA1 Message Date
ayi 2c37cd2c75 测试ci
Sync All Branches to GitHub / sync (push) Successful in 1s
2026-02-10 16:53:19 +08:00
ayi aefb212285 修改gitlab-ci中的镜像标签 2026-02-10 16:45:30 +08:00
ayi 2e18a8a419 网管过滤器,用户id穿透到下游 2026-02-04 16:54:44 +08:00
ayi a6a5d42bea gateway全局异常处理 2026-02-03 16:25:17 +08:00
ayi eaf44e1b4f SaToken接口鉴权 2026-02-03 16:09:47 +08:00
ayi 6da20bdac4 用户登录加载角色到缓存;修改用户角色Key格式 2026-02-02 18:28:33 +08:00
ayi 02010bd9ae 自定义token风格 2026-02-02 17:47:11 +08:00
ayi 660a8430f1 网关整合Sa-Token鉴权 2026-02-02 17:27:09 +08:00
ayi da56185960 添加网关服务,更新包名 2026-02-02 17:01:48 +08:00
ayi 5d8816fb2f 添加网关服务,更新包名 2026-02-02 17:00:42 +08:00
ayi b271f0aa87 nacos服务注册 2026-02-02 16:38:01 +08:00
ayi 50ac15a8f7 nacos动态加载bean 2026-02-02 15:54:44 +08:00
ayi 872be51082 整合nacos配置 2026-02-02 15:33:19 +08:00
ayi da78005ad7 修改数据库连接 2026-02-01 21:12:14 +08:00
ayi f6b3369dc0 添加gitlab-ci.yml 2026-01-31 18:43:51 +08:00
ayi 155376ca6e 项目启动将角色权限写入缓存
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-23 18:09:58 +08:00
ayi 865dfa31f5 登录接口验证码使用guava参数校验 2026-01-23 16:48:14 +08:00
70 changed files with 1296 additions and 186 deletions
+54
View File
@@ -0,0 +1,54 @@
stages:
- sync
sync-to-github:
stage: sync
tags:
- sync
image: ruby:3.3
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG == null'
when: on_success
- when: never
# 关闭浅克隆,拉取完整提交记录
variables:
GIT_DEPTH: 0
script:
# 1. 初始化Git配置,复用提交者信息
- echo "当前提交者信息:$CI_COMMIT_AUTHOR"
- GIT_AUTHOR_NAME=$(echo "$CI_COMMIT_AUTHOR" | cut -d'<' -f1 | xargs)
- GIT_AUTHOR_EMAIL=$(echo "$CI_COMMIT_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1 | xargs)
- git config --global user.name "$GIT_AUTHOR_NAME"
- git config --global user.email "$GIT_AUTHOR_EMAIL"
- git config --global http.sslVerify false
# 核心新增:禁用HTTP2,强制使用HTTP/1.1(解决HTTP2 framing layer帧层错误)
- git config --global http.version HTTP/1.1
- git config --global http.proxy $PROXY_ADDR
- git config --global https.proxy $PROXY_ADDR
# 2. 构造带PAT的GitHub推送地址
- GITHUB_PUSH_URL="https://$GITHUB_PAT@${GITHUB_REPO_URL#https://}"
- echo "同步目标:GitHub仓库 $GITHUB_REPO_URL"
# 3. 安全处理GitHub远程仓库
- git remote rm github || true
- git remote add github "$GITHUB_PUSH_URL"
# 4. 核心修复:基于分离HEAD创建本地同名分支(解决refspec不匹配问题)
- echo "基于分离HEAD创建本地分支 $CI_COMMIT_BRANCH"
- git checkout -b $CI_COMMIT_BRANCH
# 5. 拉取GitHub远程最新代码(避免冲突,新分支拉取失败不影响)
- git fetch github || true
# 6. 单分支精准推送(GitHub自动创建不存在的分支)
- echo "开始同步GitLab分支 $CI_COMMIT_BRANCH 到GitHub..."
- git push github $CI_COMMIT_BRANCH:$CI_COMMIT_BRANCH -f
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- api_failure
+14 -1
View File
@@ -19,6 +19,7 @@
<modules>
<module>xiaoyi-auth</module>
<module>xiaoyi-framework</module>
<module>xiaoyi-gateway</module>
</modules>
<properties>
@@ -41,7 +42,7 @@
<jackson.version>2.16.1</jackson.version>
<mysql-connector-java.version>8.0.29</mysql-connector-java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<druid.version>1.2.21</druid.version>
<druid.version>1.2.27</druid.version>
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<jansi.version>1.18</jansi.version>
<sa-token.version>1.38.0</sa-token.version>
@@ -50,6 +51,7 @@
<commons-lang3.version>3.12.0</commons-lang3.version>
<dypnsapi.version>2.0.0</dypnsapi.version>
<jasypt-starter.version>3.0.5</jasypt-starter.version>
<nacos-config.version>0.3.0-RC</nacos-config.version>
</properties>
<!-- 统一依赖管理 -->
@@ -144,6 +146,11 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
@@ -190,6 +197,12 @@
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>${jasypt-starter.version}</version>
</dependency>
<!-- Nacos配置中心 -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-config-spring-boot-starter</artifactId>
<version>${nacos-config.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
View File
+20
View File
@@ -96,6 +96,26 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Nacos 配置中心 -->
<!-- <dependency> -->
<!-- <groupId>com.alibaba.boot</groupId> -->
<!-- <artifactId>nacos-config-spring-boot-starter</artifactId> -->
<!-- </dependency> -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth;
package top.crushtj.xiaoyi.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -0,0 +1,39 @@
package top.crushtj.xiaoyi.auth.alarm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.crushtj.xiaoyi.auth.alarm.impl.MailAlarmHelper;
import top.crushtj.xiaoyi.auth.alarm.impl.SmsAlarmHelper;
import top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants;
/**
* @author ayi
* @version V1.0
* @title AlarmConfig
* @date 2026/2/2 15:37
* @description 告警配置类
*/
@Configuration
@RefreshScope
public class AlarmConfig {
@Value("${alarm.type}")
private String alarmType;
@Bean
@RefreshScope
public AlarmInterface alarmHelper() {
// 根据配置文件中的告警类型,初始化选择不同的告警实现类
if (StringUtils.equals(XiaoyiAuthConstants.ALARM_TYPE_SMS, alarmType)) {
return new SmsAlarmHelper();
} else if (StringUtils.equals(XiaoyiAuthConstants.ALARM_TYPE_EMAIL, alarmType)) {
return new MailAlarmHelper();
} else {
throw new IllegalArgumentException("错误的告警类型...");
}
}
}
@@ -0,0 +1,20 @@
package top.crushtj.xiaoyi.auth.alarm;
/**
* @author ayi
* @version V1.0
* @title AlarmInterface
* @date 2026/2/2 15:37
* @description 告警接口
*/
public interface AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
boolean send(String message);
}
@@ -0,0 +1,31 @@
package top.crushtj.xiaoyi.auth.alarm.impl;
import lombok.extern.slf4j.Slf4j;
import top.crushtj.xiaoyi.auth.alarm.AlarmInterface;
/**
* @author ayi
* @version V1.0
* @title MailAlarmHelper
* @date 2026/2/2 15:38
* @description 邮件告警通知类
*/
@Slf4j
public class MailAlarmHelper implements AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
@Override
public boolean send(String message) {
log.info("==> 【邮件告警】:{}", message);
// 业务逻辑...
return true;
}
}
@@ -0,0 +1,28 @@
package top.crushtj.xiaoyi.auth.alarm.impl;
import lombok.extern.slf4j.Slf4j;
import top.crushtj.xiaoyi.auth.alarm.AlarmInterface;
/**
* @author ayi
* @version V1.0
* @title SmsAlarmHelper
* @date 2026/2/2 15:39
* @description 短信告警通知类
*/
@Slf4j
public class SmsAlarmHelper implements AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
@Override
public boolean send(String message) {
log.info("==> 【短信告警】:{}", message);
return false;
}
}
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.config;
package top.crushtj.xiaoyi.auth.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.config;
package top.crushtj.xiaoyi.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.config;
package top.crushtj.xiaoyi.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -7,7 +7,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import static top.crushtj.xiaoyishu.auth.constant.ConfigConstants.*;
import static top.crushtj.xiaoyi.auth.constant.ConfigConstants.*;
/**
* @author ayi
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.constant;
package top.crushtj.xiaoyi.auth.constant;
/**
* @author ayi
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.constant;
package top.crushtj.xiaoyi.auth.constant;
/**
*
@@ -26,9 +26,14 @@ public class RedisKeyConstants {
private static final String USER_ROLES_KEY_PREFIX = "user:roles:";
/**
* 书全局 ID 生成器 KEY
* 书全局 ID 生成器 KEY
*/
public static final String XIAOYI_ID_GENERATOR_KEY = "xiaoyishu_id_generator";
public static final String XIAOYI_ID_GENERATOR_KEY = "xiaoyishu.id.generator";
/**
* 角色权限数据 KEY 前缀
*/
public static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
/**
* 构建验证码 KEY
@@ -43,11 +48,21 @@ public class RedisKeyConstants {
/**
* 构建用户角色数据 KEY
*
* @param phone 用户手机号
* @param userId 用户手机号
* @return 用户角色数据 KEY
*/
public static String buildUserRolesKey(String phone) {
return USER_ROLES_KEY_PREFIX + phone;
public static String buildUserRolesKey(Long userId) {
return USER_ROLES_KEY_PREFIX + userId;
}
/**
* 构建角色权限数据 KEY
*
* @param roleKey 角色 ID
* @return 角色权限数据 KEY
*/
public static String buildRolePermissionsKey(String roleKey) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
}
}
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.constant;
package top.crushtj.xiaoyi.auth.constant;
/**
* @author ayi
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.constant;
package top.crushtj.xiaoyi.auth.constant;
/**
* @author ayi
@@ -11,4 +11,7 @@ package top.crushtj.xiaoyishu.auth.constant;
public class XiaoyiAuthConstants {
public static final String NICK_NAME_PREFIX = "咿呀_";
public static final Long XIAOYI_ID_INITIAL_VALUE = 1000000L;
public static final String ALARM_TYPE_SMS = "sms";
public static final String ALARM_TYPE_EMAIL = "email";
}
@@ -0,0 +1,30 @@
package top.crushtj.xiaoyi.auth.controller;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.crushtj.xiaoyi.auth.alarm.AlarmInterface;
@Slf4j
@RestController
public class TestController {
@NacosValue(value = "${rate-limit.api.limit}",autoRefreshed = true)
private Integer limit;
@Resource
private AlarmInterface alarm;
@GetMapping("/test")
public String test() {
return "当前限流阈值为: " + limit;
}
@GetMapping("/alarm")
public String sendAlarm() {
alarm.send("系统出错啦,犬小哈这个月绩效没了,速度上线解决问题!");
return "alarm success";
}
}
@@ -1,24 +1,21 @@
package top.crushtj.xiaoyishu.auth.controller;
package top.crushtj.xiaoyi.auth.controller;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import top.crushtj.framework.biz.operationlog.aspect.ApiOperationLog;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyishu.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyishu.auth.service.UserService;
import top.crushtj.xiaoyi.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyi.auth.service.UserService;
/**
/**
* @author ayi
* @version V1.0
* @title UserController
* @date 2026-01-18 20:40:21
* @description 用户表(User)表控制层
*/
*/
@Slf4j
@RestController
@@ -33,5 +30,12 @@ public class UserController {
public Response<String> loginOrRegister(@RequestBody @Validated UserLoginReqVO userLoginReqVO) {
return userService.loginOrRegister(userLoginReqVO);
}
@PostMapping("/logout")
@ApiOperationLog(description = "用户登出")
public Response<Void> logout(@RequestHeader("userId") String userId) {
// todo 实现用户登出逻辑
return Response.success();
}
}
@@ -1,9 +1,9 @@
package top.crushtj.xiaoyishu.auth.controller;
package top.crushtj.xiaoyi.auth.controller;
import top.crushtj.framework.biz.operationlog.aspect.ApiOperationLog;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyishu.auth.service.VerificationCodeService;
import top.crushtj.xiaoyi.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyi.auth.service.VerificationCodeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.domain.entity;
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.domain.entity;
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.domain.entity;
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.domain.entity;
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.domain.entity;
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -0,0 +1,25 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity;
import java.util.List;
/**
* @author ayi
* @version V1.0
* @title PermissionMapper
* @date 2026-01-19 19:47:28
* @description 权限表(t_permission)表数据库访问层
*/
public interface PermissionMapper extends BaseMapper<PermissionEntity> {
/**
* 查询所有已启用的权限列表
*
* @return 已启用的权限列表
*/
List<PermissionEntity> selectAppEnabledList();
}
@@ -0,0 +1,27 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.crushtj.xiaoyi.auth.domain.entity.RoleEntity;
import java.util.List;
/**
* @author ayi
* @version V1.0
* @title RoleMapper
* @date 2026-01-19 19:48:24
* @description 角色表(t_role)表数据库访问层
*/
public interface RoleMapper extends BaseMapper<RoleEntity> {
/**
* 查询所有启用的角色列表
*
* @return 角色列表
*/
List<RoleEntity> selectEnabledRoleList();
List<String> selectRoleKeyByUserId(Long userId);
}
@@ -0,0 +1,27 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import org.apache.ibatis.annotations.Param;
import top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
/**
* @author ayi
* @version V1.0
* @title RolePermissionRelMapper
* @date 2026-01-19 19:48:32
* @description 用户权限表(t_role_permission_rel)表数据库访问层
*/
public interface RolePermissionRelMapper extends BaseMapper<RolePermissionRelEntity> {
/**
* 根据角色ID查询权限
*
* @param roleIds 角色ID列表
* @return 权限列表
*/
List<RolePermissionRelEntity> selectByRoleIds(@Param("roleIds") List<Long> roleIds);
}
@@ -1,7 +1,7 @@
package top.crushtj.xiaoyishu.auth.domain.mappers;
package top.crushtj.xiaoyi.auth.domain.mappers;
import org.apache.ibatis.annotations.Param;
import top.crushtj.xiaoyishu.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
@@ -1,7 +1,7 @@
package top.crushtj.xiaoyishu.auth.domain.mappers;
package top.crushtj.xiaoyi.auth.domain.mappers;
import top.crushtj.xiaoyishu.auth.domain.entity.UserRoleRelEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.crushtj.xiaoyi.auth.domain.entity.UserRoleRelEntity;
/**
* @author ayi
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyishu.auth.domain.mappers.PermissionMapper">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.PermissionMapper">
<resultMap type="top.crushtj.xiaoyishu.auth.domain.entity.PermissionEntity" id="PermissionMap">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity" id="PermissionMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="parentId" column="parent_id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
@@ -16,5 +16,12 @@
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id="selectAppEnabledList" resultType="top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity">
SELECT id, name, permission_key
FROM t_permission p
WHERE p.status = 1
AND p.is_deleted = 0
AND type = 3
</select>
</mapper>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyishu.auth.domain.mappers.RoleMapper">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.RoleMapper">
<resultMap type="top.crushtj.xiaoyishu.auth.domain.entity.RoleEntity" id="RoleMap">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.RoleEntity" id="RoleMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="roleName" column="role_name" jdbcType="VARCHAR"/>
<result property="roleKey" column="role_key" jdbcType="VARCHAR"/>
@@ -14,4 +14,16 @@
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id="selectEnabledRoleList" resultType="top.crushtj.xiaoyi.auth.domain.entity.RoleEntity">
SELECT id, role_name, role_key
FROM t_role
WHERE status = 1
AND is_deleted = 0
</select>
<select id="selectRoleKeyByUserId" resultType="java.lang.String">
SELECT r.role_key
FROM t_role r
LEFT JOIN t_user_role_rel urr ON r.id = urr.role_id
WHERE urr.user_id = #{userId}
</select>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.RolePermissionRelMapper">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity" id="RolePermissionRelMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="roleId" column="role_id" jdbcType="INTEGER"/>
<result property="permissionId" column="permission_id" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id="selectByRoleIds" resultType="top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity">
SELECT role_id, permission_id FROM t_role_permission_rel
<where>
<if test="roleIds!=null and !roleIds.empty">
AND role_id IN
<foreach collection="roleIds" item="roleId" open="(" separator="," close=")">
#{roleId}
</foreach>
</if>
</where>
</select>
</mapper>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyishu.auth.domain.mappers.UserMapper">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.UserMapper">
<resultMap type="top.crushtj.xiaoyishu.auth.domain.entity.UserEntity" id="UserMap">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.UserEntity" id="UserMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="xiaoyishuId" column="xiaoyishu_id" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
@@ -18,7 +18,7 @@
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id = "selectByPhone" resultType = "top.crushtj.xiaoyishu.auth.domain.entity.UserEntity">
<select id = "selectByPhone" resultType = "top.crushtj.xiaoyi.auth.domain.entity.UserEntity">
SELECT id, password, xiaoyishu_id
FROM t_user
WHERE phone = #{phone}
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyishu.auth.domain.mappers.UserRoleRelMapper">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.UserRoleRelMapper">
<resultMap type="top.crushtj.xiaoyishu.auth.domain.entity.UserRoleRelEntity" id="UserRoleRelMap">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.UserRoleRelEntity" id="UserRoleRelMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="userId" column="user_id" jdbcType="INTEGER"/>
<result property="roleId" column="role_id" jdbcType="INTEGER"/>
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.enums;
package top.crushtj.xiaoyi.auth.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.enums;
package top.crushtj.xiaoyi.auth.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.exception;
package top.crushtj.xiaoyi.auth.exception;
import java.util.Optional;
@@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import top.crushtj.framework.common.exception.BizException;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyi.auth.enums.ResponseCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@@ -91,4 +91,13 @@ public class GlobalExceptionHandler {
log.error("{} request error, ", request.getRequestURI(), e);
return Response.failure(ResponseCodeEnum.SYSTEM_ERROR);
}
@ExceptionHandler({IllegalArgumentException.class})
@ResponseBody
public Response<Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
log.error("{} request error, ", request.getRequestURI(), e);
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
String errorMessage = e.getMessage();
return Response.failure(errorCode,errorMessage);
}
}
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.model.vo.user;
package top.crushtj.xiaoyi.auth.model.vo.user;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.model.vo.verificationcode;
package top.crushtj.xiaoyi.auth.model.vo.verificationcode;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -0,0 +1,123 @@
package top.crushtj.xiaoyi.auth.runner;
import cn.hutool.core.collection.CollUtil;
import com.google.common.collect.Lists;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.crushtj.framework.common.utils.JsonUtils;
import top.crushtj.xiaoyi.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity;
import top.crushtj.xiaoyi.auth.domain.entity.RoleEntity;
import top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity;
import top.crushtj.xiaoyi.auth.domain.mappers.PermissionMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.RoleMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.RolePermissionRelMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
*
* @author ayi
* @version V1.0
* @title PushRolePermissions2RedisRunner
* @date 2026/01/23 16:51
* @description 推送角色权限到Redis
*/
@Slf4j
@Component
public class PushRolePermissions2RedisRunner implements ApplicationRunner {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RoleMapper roleMapper;
@Resource
private RolePermissionRelMapper rolePermissionRelMapper;
@Resource
private PermissionMapper permissionMapper;
public static final String PUSH_PERMISSION_FLAG = "push.permission.flag";
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("==> 开始同步角色权限数据到 Redis 中...");
try {
// 是否能够同步数据: 原子操作,只有在键 PUSH_PERMISSION_FLAG 不存在时,才会设置该键的值为 "1",并设置过期时间为 1 天
boolean canPushed = Boolean.TRUE.equals(redisTemplate.opsForValue()
.setIfAbsent(PUSH_PERMISSION_FLAG, "1", 1, TimeUnit.DAYS));
// 如果无法同步权限数据
if (!canPushed) {
log.warn("==> 角色权限数据已经同步至 Redis 中,不再同步...");
return;
}
// 查询所有已启用角色
List<RoleEntity> roleList = roleMapper.selectEnabledRoleList();
if (CollUtil.isNotEmpty(roleList)) {
// 获取角色ID集合
List<Long> roleIds = roleList.stream()
.map(RoleEntity::getId)
.toList();
//查询角色对应的权限
List<RolePermissionRelEntity> rolePermissionList = rolePermissionRelMapper.selectByRoleIds(roleIds);
// 按角色 ID 分组, 每个角色 ID 对应多个权限 ID
Map<Long, List<Long>> roleIdPermissionIdsMap = rolePermissionList.stream()
.collect(Collectors.groupingBy(RolePermissionRelEntity::getRoleId,
Collectors.mapping(RolePermissionRelEntity::getPermissionId, Collectors.toList())));
// 查询所有启用的权限
List<PermissionEntity> permissionList = permissionMapper.selectAppEnabledList();
// 构建权限 ID 和权限 DO 对象的映射关系
Map<Long, PermissionEntity> permissionIdEntityMap = permissionList.stream()
.collect(Collectors.toMap(PermissionEntity::getId, permissionEntity -> permissionEntity));
// 构建角色 ID 和权限 DO 列表的映射关系
Map<String, List<String>> roleIdPermissionMap = new HashMap<>();
roleList.forEach(role -> {
Long roleId = role.getId();
//获取角色对应的权限id列表
List<Long> permissionIds = roleIdPermissionIdsMap.get(roleId);
if (CollUtil.isNotEmpty(permissionIds)) {
List<String> permissionKeys = Lists.newArrayList();
permissionIds.forEach(permissionId -> {
// 根据权限 ID 获取具体的权限 DO 对象
PermissionEntity permissionDO = permissionIdEntityMap.get(permissionId);
if (Objects.nonNull(permissionDO)) {
permissionKeys.add(permissionDO.getPermissionKey());
}
});
roleIdPermissionMap.put(role.getRoleKey(), permissionKeys);
}
});
// 将角色 ID 和权限 DO 列表的映射关系写入 Redis
roleIdPermissionMap.forEach((roleKey, permission) -> {
String key = RedisKeyConstants.buildRolePermissionsKey(roleKey);
redisTemplate.opsForValue()
.set(key, JsonUtils.toJsonString(permission));
});
}
log.info("==> 角色权限数据同步到 Redis 完成...");
} catch (Exception e) {
log.error("==> 角色权限数据同步到 Redis 失败...", e);
}
}
}
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.cache;
package top.crushtj.xiaoyi.auth.runner.cache;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.PostConstruct;
@@ -6,11 +6,11 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.crushtj.xiaoyishu.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyishu.auth.domain.mappers.UserMapper;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.domain.mappers.UserMapper;
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
import static top.crushtj.xiaoyi.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
/**
* @author ayi
@@ -34,16 +34,18 @@ public class CacheLoader {
*/
@PostConstruct
public void loadUserCache() {
log.info("加载用户自增ID缓存开始...");
log.info("==> 加载用户自增ID缓存开始...");
LambdaQueryWrapper<UserEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(UserEntity::getXiaoyishuId);
queryWrapper.last("limit 1");
UserEntity user = userMapper.selectOne(queryWrapper);
if (user != null){
redisTemplate.opsForValue().set(XIAOYI_ID_GENERATOR_KEY, Long.valueOf(user.getXiaoyishuId()));
}else {
redisTemplate.opsForValue().set(XIAOYI_ID_GENERATOR_KEY, XIAOYI_ID_INITIAL_VALUE);
if (user != null) {
redisTemplate.opsForValue()
.set(XIAOYI_ID_GENERATOR_KEY, Long.valueOf(user.getXiaoyishuId()));
} else {
redisTemplate.opsForValue()
.set(XIAOYI_ID_GENERATOR_KEY, XIAOYI_ID_INITIAL_VALUE);
}
log.info("加载用户自增ID缓存结束...");
log.info("==> 加载用户自增ID缓存结束...");
}
}
@@ -1,9 +1,9 @@
package top.crushtj.xiaoyishu.auth.service;
package top.crushtj.xiaoyi.auth.service;
import com.baomidou.mybatisplus.extension.service.IService;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyishu.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyishu.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.model.vo.user.UserLoginReqVO;
/**
* @author ayi
@@ -1,7 +1,7 @@
package top.crushtj.xiaoyishu.auth.service;
package top.crushtj.xiaoyi.auth.service;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyi.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
/**
*
@@ -1,8 +1,9 @@
package top.crushtj.xiaoyishu.auth.service.impl;
package top.crushtj.xiaoyi.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -16,25 +17,27 @@ import top.crushtj.framework.common.response.Response;
import top.crushtj.framework.common.utils.IdGenerator;
import top.crushtj.framework.common.utils.JsonUtils;
import top.crushtj.framework.common.utils.MaskUtils;
import top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyishu.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyishu.auth.domain.entity.UserRoleRelEntity;
import top.crushtj.xiaoyishu.auth.domain.mappers.UserMapper;
import top.crushtj.xiaoyishu.auth.domain.mappers.UserRoleRelMapper;
import top.crushtj.xiaoyishu.auth.enums.LoginTypeEnum;
import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyishu.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyishu.auth.service.UserService;
import top.crushtj.xiaoyi.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyi.auth.domain.entity.RoleEntity;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.domain.entity.UserRoleRelEntity;
import top.crushtj.xiaoyi.auth.domain.mappers.RoleMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.UserMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.UserRoleRelMapper;
import top.crushtj.xiaoyi.auth.enums.LoginTypeEnum;
import top.crushtj.xiaoyi.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyi.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyi.auth.service.UserService;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyishu.auth.constant.RoleConstants.COMMON_USER_ROLE_ID;
import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.NICK_NAME_PREFIX;
import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
import static top.crushtj.xiaoyi.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyi.auth.constant.RoleConstants.COMMON_USER_ROLE_ID;
import static top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants.NICK_NAME_PREFIX;
import static top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
/**
* @author ayi
@@ -54,6 +57,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
@Resource
private UserRoleRelMapper userRoleRelMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private TransactionTemplate transactionTemplate;
@@ -73,9 +79,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
String verificationCode = userLoginReqVO.getCode();
// 验证码登录
// 校验入参验证码是否为空
if (StringUtils.isBlank(verificationCode)) {
return Response.failure(ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(), "验证码不能为空");
}
Preconditions.checkArgument(StringUtils.isNotBlank(verificationCode), "验证码不能为空");
// 构建验证码 Redis Key
String key = RedisKeyConstants.buildVerificationCodeKey(phone);
@@ -103,6 +107,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
} else {
// 已注册则获取其用户 ID
userId = userEntity.getId();
this.loadUserRole(userId);
}
}
}
@@ -115,6 +120,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
return Response.success(tokenInfo.tokenValue);
}
/**
* 用户注册
*
* @param phone 手机号
* @return 用户ID
*/
private Long registerUser(String phone) {
return transactionTemplate.execute(status -> {
try {
@@ -148,9 +159,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
userRoleRelMapper.insert(userRoleRel);
// 将用户角色信息存储到 Redis
List<Long> roles = new ArrayList<>();
roles.add(COMMON_USER_ROLE_ID);
String userRolesKey = RedisKeyConstants.buildUserRolesKey(phone);
RoleEntity role = roleMapper.selectById(COMMON_USER_ROLE_ID);
List<String> roles = new ArrayList<>(1);
roles.add(role.getRoleKey());
String userRolesKey = RedisKeyConstants.buildUserRolesKey(userId);
redisTemplate.opsForValue()
.set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;
@@ -161,7 +173,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
// 回滚 redis 自增ID
Long decrement = Long.valueOf(Objects.requireNonNull(redisTemplate.opsForValue()
.get(XIAOYI_ID_GENERATOR_KEY))
.toString().trim());
.toString()
.trim());
if (decrement.compareTo(XIAOYI_ID_INITIAL_VALUE) > 0) {
log.error("==> 用户注册异常,回滚redis自增ID: {}", decrement);
redisTemplate.opsForValue()
@@ -171,5 +184,20 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
return null;
});
}
/**
* 加载用户角色信息到缓存
*
* @param userId 用户ID
*/
private void loadUserRole(Long userId) {
String userRolesKey = RedisKeyConstants.buildUserRolesKey(userId);
Boolean hasKey = redisTemplate.hasKey(userRolesKey);
if (!hasKey) {
List<String> roles = roleMapper.selectRoleKeyByUserId(userId);
redisTemplate.opsForValue()
.set(userRolesKey, JsonUtils.toJsonString(roles));
}
}
}
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.service.impl;
package top.crushtj.xiaoyi.auth.service.impl;
import cn.hutool.core.util.RandomUtil;
import jakarta.annotation.Resource;
@@ -9,15 +9,15 @@ import org.springframework.stereotype.Service;
import top.crushtj.framework.common.exception.BizException;
import top.crushtj.framework.common.response.Response;
import top.crushtj.framework.common.utils.MaskUtils;
import top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyishu.auth.service.VerificationCodeService;
import top.crushtj.xiaoyishu.auth.sms.AliyunSmsHelper;
import top.crushtj.xiaoyi.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyi.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyi.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyi.auth.service.VerificationCodeService;
import top.crushtj.xiaoyi.auth.sms.AliyunSmsHelper;
import java.util.concurrent.TimeUnit;
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
import static top.crushtj.xiaoyi.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
/**
* 验证码服务实现类
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.sms;
package top.crushtj.xiaoyi.auth.sms;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.sms;
package top.crushtj.xiaoyi.auth.sms;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth.sms;
package top.crushtj.xiaoyi.auth.sms;
import com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeResponse;
import jakarta.annotation.Resource;
@@ -1,16 +0,0 @@
package top.crushtj.xiaoyishu.auth.domain.mappers;
import top.crushtj.xiaoyishu.auth.domain.entity.PermissionEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author ayi
* @version V1.0
* @title PermissionMapper
* @date 2026-01-19 19:47:28
* @description 权限表(t_permission)表数据库访问层
*/
public interface PermissionMapper extends BaseMapper<PermissionEntity> {
}
@@ -1,16 +0,0 @@
package top.crushtj.xiaoyishu.auth.domain.mappers;
import top.crushtj.xiaoyishu.auth.domain.entity.RoleEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author ayi
* @version V1.0
* @title RoleMapper
* @date 2026-01-19 19:48:24
* @description 角色表(t_role)表数据库访问层
*/
public interface RoleMapper extends BaseMapper<RoleEntity> {
}
@@ -1,16 +0,0 @@
package top.crushtj.xiaoyishu.auth.domain.mappers;
import top.crushtj.xiaoyishu.auth.domain.entity.RolePermissionRelEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author ayi
* @version V1.0
* @title RolePermissionRelMapper
* @date 2026-01-19 19:48:32
* @description 用户权限表(t_role_permission_rel)表数据库访问层
*/
public interface RolePermissionRelMapper extends BaseMapper<RolePermissionRelEntity> {
}
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyishu.auth.domain.mappers.RolePermissionRelMapper">
<resultMap type="top.crushtj.xiaoyishu.auth.domain.entity.RolePermissionRelEntity" id="RolePermissionRelMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="roleId" column="role_id" jdbcType="INTEGER"/>
<result property="permissionId" column="permission_id" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
</mapper>
@@ -1,14 +1,10 @@
server:
servlet:
context-path: /xiaoyishu-auth
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接信息
url: ENC(+rm/FjvaL/fDxkZu/G9zxQnSO2M60VFodOuZzxqlxzUyNN2ooNjH9mtVVZiqlqMm8EHDCBKYD/rI4e1VdaDlYtwL+WxdW2K6rujuUm1R0Mkjl1kphthDaiAK5tnPvtfffL4aVpBbA7oOUdsX0tCURzWvpjSFhDHlxtb4b+Ezx6JeHJoZDEJ0bErjDETLdjbIGDNdCsTCvz2wp1S5+AdW39S5C8kj4PNLIXkbckzKdhHm9ATLrixdveyBLmuBkDwLsE9XBmCFm0ipST5/SVg8WA==)
username: ENC(B/DozYPqx155j/Gdh3QkCX/wGmuzLf1jumsbb1kF4GY6EjrlRdAatc8BlhrrR2J6)
password: ENC(JwQdlswsrwmGE6eplp6Jk9Vp5s6zlFgra6jLN5fKBt8qOyqQHUHCJM6JcKp5g0t8B/or2wTH18ohR/d41oyV9w==)
url: ENC(oyIjmfAXMQ/z3uHl88N68RYJ96HJhdLkXMBBpE1wq9kSbYuTlxwZV/F7Qxa6tDAu+MD6I9swGqmYl2ormW/bzxPQdk+rrQANSQVA+ekyPGd97I5RGPfLLGQCw6FExrbhh9uzKGvekSmsIC8h91dTL2sMD8dYHbuRpW9J5knEyHSCZ90Bep1ZOlu2mWNQfdbaqmbafs6Bhjj9Iu4m9SblAMomAbDVT1oREPzjtMX640ro+Ou/YGVYPuLj3LlwVooauGCoswmHK0cA2TCD0RqaIg==)
username: ENC(Hf/tJoMig8FWFHopB7CMlQFuBhu61JbpA64JQWvqrmL+fTtGi6QNuI+sHBNY43nY)
password: ENC(l2k707lyMR9ACwqEHl/d3VOojwmvIyRsH7leB9fa9ppOtbKBae8YnVVuEMcRhEqMTTvcqZ4Otd/TotqZMerL/w==)
druid: # Druid 连接池
initial-size: 5 # 初始化连接池大小
min-idle: 5 # 最小连接池数量
@@ -43,7 +39,7 @@ spring:
data:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: ENC(W8nbSo0vMjCenHHLHyhvdhW2wH3oI54/FW+LQp8H9lx6xQH2Tm56nGSgNom6xsK7) # Redis 服务器地址
host: ENC(C1TWXF+/HzWQBF25uXCdy/0fHoRDXdCW72+NKCIJURg4l3IDnJzl278KmFhfsusX) # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: ENC(iK/k0IGPflACqYMUwX4N/sGvCVuysYywLcAO+Ikeqk326V8hCr8dgEGzkiEIwWOo) # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
@@ -82,4 +78,20 @@ jasypt:
key-obtention-iterations: 1000
string-output-type: base64
provider-name: SunJCE
iv-generator-classname: org.jasypt.iv.RandomIvGenerator
iv-generator-classname: org.jasypt.iv.RandomIvGenerator
#nacos:
# config: # Nacos 配置中心
# access-key: # 身份验证
# secret-key: # 身份验证
# data-id: xiaoyishu-auth # 指定要加载的配置数据的 Data Id
# group: DEFAULT_GROUP # 指定配置数据所属的组
# type: yaml # 指定配置数据的格式
# server-addr: http://127.0.0.1:8848/ # 指定 Nacos 配置中心的服务器地址
# auto-refresh: true # 是否自动刷新配置
# remote-first: true # 是否优先使用远程配置
# bootstrap:
# enable: true # 启动时,预热配置
rate-limit:
api:
limit: 100 # 接口限流阈值
@@ -10,9 +10,11 @@ logging:
############## Sa-Token 配置 ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
token-name: Authorization
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# timeout: 60
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
@@ -20,6 +22,9 @@ sa-token:
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: uuid
token-style: random-128
# 是否输出操作日志
is-log: true
is-log: true
alarm:
type: sms # 告警方式,sms-短信,email-邮件
@@ -0,0 +1,19 @@
spring:
application:
name: xiaoyishu-auth # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
config:
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
group: DEFAULT_GROUP # 所属组
namespace: xiaoyishu # 命名空间
file-extension: yaml # 配置文件格式
refresh-enabled: true # 是否开启动态刷新
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: xiaoyishu # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth;
package top.crushtj.xiaoyi.auth;
import com.alibaba.druid.filter.config.ConfigTools;
import lombok.SneakyThrows;
@@ -64,23 +64,46 @@ public class EncryptTest {
// 待加密的原始值
String encry1 = ""; // 待加密的原始值1
String encry2 = ""; // 待加密的原始值2
String encry3 = ""; // 待加密的原始值3
try {
String cipherAccessKeyId = encryptor.encrypt(encry1);
System.out.println("字段1加密成功,密文:" + cipherAccessKeyId);
String decryptAccessKeyId = encryptor.decrypt(cipherAccessKeyId);
System.out.println("字段1解密成功,明文:" + decryptAccessKeyId);
String cipherAccessKeySecret = encryptor.encrypt(encry2);
System.out.println("字段2加密成功,密文:" + cipherAccessKeySecret);
String decryptAccessKeySecret = encryptor.decrypt(cipherAccessKeySecret);
System.out.println("字段2解密成功,明文:" + decryptAccessKeySecret);
String cipherText1 = encryptor.encrypt(encry1);
System.out.println("字段1加密成功,密文:" + cipherText1);
String cipherText2 = encryptor.encrypt(encry2);
System.out.println("字段2加密成功,密文:" + cipherText2);
String cipherText3 = encryptor.encrypt(encry3);
System.out.println("字段2加密成功,密文:" + cipherText3);
} catch (Exception e) {
e.printStackTrace();
System.out.println("加密失败原因:" + e.getMessage());
}
}
@Test
public void dencrypt(){
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
String cipherText1 = "";//密文
String cipherText2 = "";//密文
String cipherText3 = "";//密文
String password = "";//加密密码
encryptor.setPassword(password); // AES-256要求密钥至少32位
encryptor.setAlgorithm("PBEWithHMACSHA512AndAES_256"); // JDK17原生支持的算法
encryptor.setKeyObtentionIterations(1000); // 迭代次数固定值
encryptor.setStringOutputType("base64"); // 输出格式固定
encryptor.setProviderName("SunJCE"); // 加密提供者JDK17默认
encryptor.setIvGenerator(new RandomIvGenerator());
try {
String text1 = encryptor.decrypt(cipherText1);
System.out.println("字段1解密成功,明文:" + text1);
String text2 = encryptor.decrypt(cipherText2);
System.out.println("字段2解密成功,明文:" + text2);
String text3 = encryptor.decrypt(cipherText3);
System.out.println("字段2解密成功,明文:" + text3);
} catch (Exception e) {
e.printStackTrace();
System.out.println("加密失败原因:" + e.getMessage());
}// AES必须的IV生成器
}
}
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth;
package top.crushtj.xiaoyi.auth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@@ -1,4 +1,4 @@
package top.crushtj.xiaoyishu.auth;
package top.crushtj.xiaoyi.auth;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
+93
View File
@@ -0,0 +1,93 @@
<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>top.crushtj</groupId>
<artifactId>xiaoyishu</artifactId>
<version>${revision}</version>
</parent>
<artifactId>xiaoyi-gateway</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>网关服务</description>
<dependencies>
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jasypt 加密工具 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson 组件 -->
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-spring-boot-starter-jackson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,20 @@
package top.crushtj.xiaoyi.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author ayi
* @version V1.0
* @title XiaoyiGatewayApplication
* @date 2026/2/2 16:46
* @description 网关启动类
*/
@SpringBootApplication
public class XiaoyiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(XiaoyiGatewayApplication.class, args);
}
}
@@ -0,0 +1,55 @@
package top.crushtj.xiaoyi.gateway.auth;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ayi
* @version V1.0
* @title SaTokenConfigure
* @date 2026/2/2 17:16
* @description SaToken配置类
*/
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验
SaRouter.match("/**") // 拦截所有路由
.notMatch("/auth/user/login") // 排除登录接口
.notMatch("/auth/verification/code/send") // 排除验证码发送接口
.check(r -> StpUtil.checkLogin()) // 校验是否登录
;
// SaRouter.match("/auth/user/logout", r -> StpUtil.checkPermission("user"));
// 权限认证 -- 不同模块, 校验不同权限
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// 更多匹配 ... */
})
.setError(e -> {
if (e instanceof NotLoginException) { // 未登录异常
throw new NotLoginException(e.getMessage(), null, null);
} else if (e instanceof NotPermissionException || e instanceof NotRoleException) { // 权限不足,或不具备角色,统一抛出权限不足异常
throw new NotPermissionException(e.getMessage());
} else { // 其他异常,则抛出一个运行时异常
throw new RuntimeException(e.getMessage());
}
});
}
}
@@ -0,0 +1,107 @@
package top.crushtj.xiaoyi.gateway.auth;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.collection.CollUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.crushtj.xiaoyi.gateway.constant.RedisKeyConstants;
import java.util.List;
import java.util.Objects;
/**
* @author ayi
* @version V1.0
* @title StpInterfaceImpl
* @date 2026/2/2 17:14
* @description 自定义权限验证接口
*/
@Slf4j
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private ObjectMapper objectMapper;
@SneakyThrows
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
log.info("## 获取用户权限列表, loginId: {}", loginId);
// 构建 用户-角色 Redis Key
String userRolesKey = RedisKeyConstants.buildUserRoleKey(Long.valueOf(loginId.toString()));
// 根据用户 ID ,从 Redis 中获取该用户的角色集合
String useRolesValue = redisTemplate.opsForValue()
.get(userRolesKey);
if (StringUtils.isBlank(useRolesValue)) {
return null;
}
// 将 JSON 字符串转换为 List<String> 角色集合
List<String> userRoleKeys = objectMapper.readValue(useRolesValue, new TypeReference<>() {
});
if (CollUtil.isNotEmpty(userRoleKeys)) {
// 查询这些角色对应的权限
// 构建 角色-权限 Redis Key 集合
List<String> rolePermissionsKeys = userRoleKeys.stream()
.map(RedisKeyConstants::buildRolePermissionsKey)
.toList();
// 通过 key 集合批量查询权限,提升查询性能。
List<String> rolePermissionsValues = Objects.requireNonNull(redisTemplate.opsForValue()
.multiGet(rolePermissionsKeys))
.stream()
.filter(Objects::nonNull)
.toList();
if (CollUtil.isNotEmpty(rolePermissionsValues)) {
List<String> permissions = Lists.newArrayList();
// 遍历所有角色的权限集合,统一添加到 permissions 集合中
rolePermissionsValues.forEach(jsonValue -> {
try {
// 将 JSON 字符串转换为 List<String> 权限集合
List<String> rolePermissions = objectMapper.readValue(jsonValue, new TypeReference<>() {
});
permissions.addAll(rolePermissions);
} catch (JsonProcessingException e) {
log.error("==> JSON 解析错误: ", e);
}
});
// 返回此用户所拥有的权限
return permissions;
}
}
return null;
}
@SneakyThrows
@Override
public List<String> getRoleList(Object loginId, String loginType) {
log.info("## 获取用户角色列表, loginId: {}", loginId);
String userRolesKey = RedisKeyConstants.buildUserRoleKey(Long.valueOf(loginId.toString()));
String useRolesValue = redisTemplate.opsForValue()
.get(userRolesKey);
if (StringUtils.isBlank(useRolesValue)) {
return null;
}
// 将 JSON 字符串转换为 List<String> 集合
return objectMapper.readValue(useRolesValue, new TypeReference<>() {
});
}
}
@@ -0,0 +1,41 @@
package top.crushtj.xiaoyi.gateway.config;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.NonNull;
/**
*
* @author ayi
* @version V1.0
* @title RedisTemplateConfig
* @description Redis 配置类
* @date 2026/01/12 19:13
*/
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(@NonNull RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@@ -0,0 +1,42 @@
package top.crushtj.xiaoyi.gateway.constant;
/**
* @author ayi
* @version V1.0
* @title RedisKeyConstants
* @date 2026/2/3 15:24
* @description Redis key 常量
*/
public class RedisKeyConstants {
/**
* 用户对应角色集合 KEY 前缀
*/
private static final String USER_ROLES_KEY_PREFIX = "user:roles:";
/**
* 角色对应的权限集合 KEY 前缀
*/
private static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
/**
* 构建角色对应的权限集合 KEY
*
* @param roleKey 角色 key
* @return 角色对应的权限集合 KEY
*/
public static String buildRolePermissionsKey(String roleKey) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
}
/**
* 构建用户-角色 KEY
*
* @param userId 用户 ID
* @return 用户-角色 KEY
*/
public static String buildUserRoleKey(Long userId) {
return USER_ROLES_KEY_PREFIX + userId;
}
}
@@ -0,0 +1,29 @@
package top.crushtj.xiaoyi.gateway.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import top.crushtj.framework.common.exception.BaseExceptionInterface;
/**
* @author ayi
* @version V1.0
* @title ResponseCodeEnum
* @date 2026/2/3 16:15
* @description 异常状态吗枚举
*/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 -----------
SYSTEM_ERROR("500", "系统繁忙,请稍后再试"),
UNAUTHORIZED("401", "没有权限"),
// ----------- 业务异常状态码 -----------
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMessage;
}
@@ -0,0 +1,78 @@
package top.crushtj.xiaoyi.gateway.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.SaTokenException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.gateway.enums.ResponseCodeEnum;
/**
* @author ayi
* @version V1.0
* @title GlobalExceptionHandler
* @date 2026/2/3 16:17
* @description 全局异常处理
*/
@Slf4j
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Resource
private ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 获取响应对象
ServerHttpResponse response = exchange.getResponse();
log.error("==> 全局异常捕获: ", ex);
// 响参
Response<?> result;
// 根据捕获的异常类型,设置不同的响应状态码和响应消息
if (ex instanceof SaTokenException) { // Sa-Token 异常
// 权限认证失败时,设置 401 状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
if (ex instanceof NotLoginException){
// 构建响应结果
result = Response.failure(ResponseCodeEnum.UNAUTHORIZED.getErrorCode(),
ex.getMessage());
}else if (ex instanceof NotPermissionException) {
result = Response.failure(ResponseCodeEnum.UNAUTHORIZED.getErrorCode(),
ResponseCodeEnum.UNAUTHORIZED.getErrorMessage());
} else {
result = Response.failure(ResponseCodeEnum.SYSTEM_ERROR);
}
} else { // 其他异常,则统一提示 “系统繁忙” 错误
result = Response.failure(ResponseCodeEnum.SYSTEM_ERROR);
}
// 设置响应头的内容类型为 application/json;charset=UTF-8,表示响应体为 JSON 格式
response.getHeaders()
.setContentType(MediaType.APPLICATION_JSON_UTF8);
// 设置 body 响应体
return response.writeWith(Mono.fromSupplier(() -> { // 使用 Mono.fromSupplier 创建响应体
DataBufferFactory bufferFactory = response.bufferFactory();
try {
// 使用 ObjectMapper 将 result 对象转换为 JSON 字节数组
return bufferFactory.wrap(objectMapper.writeValueAsBytes(result));
} catch (Exception e) {
// 如果转换过程中出现异常,则返回空字节数组
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
@@ -0,0 +1,40 @@
package top.crushtj.xiaoyi.gateway.filter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author ayi
* @version V1.0
* @title AddUserId2HeaderFilter
* @date 2026/2/4 15:28
* @description 添加用户ID到请求头过滤器
*/
@Slf4j
@Component
public class AddUserId2HeaderFilter implements GlobalFilter {
private static final String HEADER_USER_ID = "userId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Long userId = null;
try {
userId = StpUtil.getLoginIdAsLong();
} catch (Exception e) {
return chain.filter(exchange);
}
Long finalUserId = userId;
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder.header(HEADER_USER_ID, String.valueOf(finalUserId)))
.build();
return chain.filter(newExchange);
}
}
@@ -0,0 +1,54 @@
server:
port: 8000 # 指定启动端口
spring:
cloud:
gateway:
routes:
- id: auth
uri: lb://xiaoyishu-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
data:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: ENC(C1TWXF+/HzWQBF25uXCdy/0fHoRDXdCW72+NKCIJURg4l3IDnJzl278KmFhfsusX) # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: ENC(iK/k0IGPflACqYMUwX4N/sGvCVuysYywLcAO+Ikeqk326V8hCr8dgEGzkiEIwWOo) # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
############## Sa-Token 配置 ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# timeout: 60
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true
jasypt:
encryptor:
password:
algorithm: PBEWithHMACSHA512AndAES_256
key-obtention-iterations: 1000
string-output-type: base64
provider-name: SunJCE
iv-generator-classname: org.jasypt.iv.RandomIvGenerator
@@ -0,0 +1,12 @@
spring:
application:
name: xiaoyi-gateway # 应用名称
profiles:
active: dev
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: xiaoyishu # 命名空间
server-addr: 127.0.0.1:8848 # Nacos 服务器地址