feat(gateway): 新增网关服务及权限认证功能

- 新增网关服务模块 han-note-gateway,包含基础配置和启动类
- 实现全局过滤器 AddUserId2HeaderFilter,自动将用户ID添加到请求头(目前有问题)
- 配置 Sa-Token 权限认证,支持 JWT 格式的 Token 解析和鉴权
- 新增全局异常处理器 GlobalExceptionHandler,统一处理未登录和权限不足异常
- 实现 StpInterfaceImpl 接口,从 Redis 获取用户角色和权限信息- 配置 RedisTemplate 支持 JSON 序列化,用于存储用户角色和权限数据
- 在 auth 服务中增加登出接口,支持用户退出登录(待完成)
- 引入 Nacos 配置中心和注册中心依赖,支持配置动态刷新和服务发现
- 更新 Redis Key 构造方式,使用 userId 和 roleKey 替代 email 和 roleId
- 新增告警模块,支持邮件和短信告警方式的配置与切换
-优化角色权限同步逻辑,使用角色 Key 替代角色 ID 存储权限信息
- 添加 bootstrap.yml 配置文件,支持从 Nacos 读取配置
This commit is contained in:
Hanserwei
2025-10-02 21:46:05 +08:00
parent eb9f887ac3
commit 4c6a08438a
27 changed files with 668 additions and 26 deletions

View File

@@ -0,0 +1,29 @@
package com.hanserwei.hannote.auth.alarm;
import com.hanserwei.hannote.auth.alarm.impl.MailAlarmHelper;
import com.hanserwei.hannote.auth.alarm.impl.SmsAlarmHelper;
import org.apache.commons.lang3.Strings;
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;
@Configuration
@RefreshScope
public class AlarmConfig {
@Value("${alarm.type}")
private String alarmType;
@Bean
public AlarmInterface alarmHelper() {
// 根据配置文件中的告警类型,初始化选择不同的告警实现类
if (Strings.CS.equals("sms", alarmType)) {
return new SmsAlarmHelper();
} else if (Strings.CS.equals("mail", alarmType)) {
return new MailAlarmHelper();
} else {
throw new IllegalArgumentException("错误的告警类型...");
}
}
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.auth.alarm;
public interface AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
boolean send(String message);
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.hannote.auth.alarm.impl;
import com.hanserwei.hannote.auth.alarm.AlarmInterface;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MailAlarmHelper implements AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 响应
*/
@Override
public boolean send(String message) {
log.info("==> 【邮件告警】:{}", message);
// 业务逻辑...
return true;
}
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.hannote.auth.alarm.impl;
import com.hanserwei.hannote.auth.alarm.AlarmInterface;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SmsAlarmHelper implements AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 响应
*/
@Override
public boolean send(String message) {
log.info("==> 【短信告警】:{}", message);
// 业务逻辑...
return true;
}
}

View File

@@ -35,20 +35,20 @@ public class RedisKeyConstants {
/**
* 构建用户-角色 Key
*
* @param email 邮箱
* @param userId 邮箱
* @return 用户角色key
*/
public static String buildUserRoleKey(String email) {
return USER_ROLES_KEY_PREFIX + email;
public static String buildUserRoleKey(Long userId) {
return USER_ROLES_KEY_PREFIX + userId;
}
/**
* 构建角色对应的权限集合 KEY
*
* @param roleId 角色ID
* @param roleKey 角色ID
* @return 角色权限集合key
*/
public static String buildRolePermissionsKey(Long roleId) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleId;
public static String buildRolePermissionsKey(String roleKey) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
}
}

View File

@@ -8,10 +8,7 @@ import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
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.*;
@RestController
@RequestMapping("/user")
@@ -27,4 +24,13 @@ public class UserController {
public Response<String> loginAndRegister(@Validated @RequestBody UserLoginReqVO userLoginReqVO) {
return userService.loginAndRegister(userLoginReqVO);
}
@PostMapping("/logout")
@ApiOperationLog(description = "账号登出")
public Response<?> logout(@RequestHeader("userId") String userId) {
log.info("==> 网关透传过来的用户 ID: {}", userId);
// todo 账号退出登录逻辑待实现
return Response.success();
}
}

View File

@@ -10,7 +10,9 @@ public interface RoleDOMapper extends BaseMapper<RoleDO> {
/**
* 查询所有被启用的角色
*
* @return
* @return 角色列表
*/
List<RoleDO> selectEnabledList();
RoleDO selectByPrimaryKey(Long commonUserRoleId);
}

View File

@@ -20,7 +20,6 @@ import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -73,29 +72,29 @@ public class PushRolePermissions2RedisRunner implements ApplicationRunner {
);
// 组织 角色ID-权限 关系
Map<Long, List<PermissionDO>> roleIdPermissionDOMap = Maps.newHashMap();
Map<String, List<String>> roleKeyPermissionMap = Maps.newHashMap();
// 循环所有角色
roleDOS.forEach(roleDO -> {
// 当前角色 roleKey
String roleKey = roleDO.getRoleKey();
// 当前角色 ID
Long roleId = roleDO.getId();
// 当前角色 ID 对应的权限 ID 集合
List<Long> permissionIds = roleIdPermissionIdsMap.get(roleId);
if (CollUtil.isNotEmpty(permissionIds)) {
List<PermissionDO> perDOS = Lists.newArrayList();
List<String> permissionKeys = Lists.newArrayList();
permissionIds.forEach(permissionId -> {
// 根据权限 ID 获取具体的权限 DO 对象
PermissionDO permissionDO = permissionIdDOMap.get(permissionId);
if (Objects.nonNull(permissionDO)) {
perDOS.add(permissionDO);
}
permissionKeys.add(permissionDO.getPermissionKey());
});
roleIdPermissionDOMap.put(roleId, perDOS);
roleKeyPermissionMap.put(roleKey, permissionKeys);
}
});
// 同步至 Redis 中,方便后续网关查询鉴权使用
roleIdPermissionDOMap.forEach((roleId, permissions) -> {
roleKeyPermissionMap.forEach((roleId, permissions) -> {
String key = RedisKeyConstants.buildRolePermissionsKey(roleId);
redisTemplate.opsForValue().set(key, JsonUtils.toJsonString(permissions));
});

View File

@@ -6,7 +6,6 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.hanserwei.framework.common.enums.DeletedEnum;
import com.hanserwei.framework.common.enums.StatusEnum;
import com.hanserwei.framework.common.exception.ApiException;
@@ -14,8 +13,10 @@ import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.auth.constant.RedisKeyConstants;
import com.hanserwei.hannote.auth.constant.RoleConstants;
import com.hanserwei.hannote.auth.domain.dataobject.RoleDO;
import com.hanserwei.hannote.auth.domain.dataobject.UserDO;
import com.hanserwei.hannote.auth.domain.dataobject.UserRoleDO;
import com.hanserwei.hannote.auth.domain.mapper.RoleDOMapper;
import com.hanserwei.hannote.auth.domain.mapper.UserDOMapper;
import com.hanserwei.hannote.auth.domain.mapper.UserRoleDOMapper;
import com.hanserwei.hannote.auth.enums.LoginTypeEnum;
@@ -30,6 +31,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -42,6 +44,7 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
private final RedisTemplate<String, Object> redisTemplate;
private final UserRoleDOMapper userRoleDOMapper;
private final TransactionTemplate transactionTemplate;
private final RoleDOMapper roleDOMapper;
@Override
public Response<String> loginAndRegister(UserLoginReqVO reqVO) {
@@ -122,10 +125,13 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
.build();
userRoleDOMapper.insert(userRoleDO);
// 将该用户的角色 ID 存入 Redis 中
List<Long> roles = Lists.newArrayList();
roles.add(RoleConstants.COMMON_USER_ROLE_ID);
String userRolesKey = RedisKeyConstants.buildUserRoleKey(email);
RoleDO roleDO = roleDOMapper.selectByPrimaryKey(RoleConstants.COMMON_USER_ROLE_ID);
// 将该用户的角色 ID 存入 Redis 中,指定初始容量为 1这样可以减少在扩容时的性能开销
List<String> roles = new ArrayList<>(1);
roles.add(roleDO.getRoleKey());
String userRolesKey = RedisKeyConstants.buildUserRoleKey(userId);
redisTemplate.opsForValue().set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;

View File

@@ -47,7 +47,9 @@ logging:
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
token-name: Authorization
# token前缀
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
@@ -57,6 +59,8 @@ sa-token:
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: true
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: simple-uuid
token-style: random-128
# 是否输出操作日志
is-log: true
alarm:
type: mail # 告警类型

View File

@@ -0,0 +1,19 @@
spring:
profiles:
active: dev # 激活的环境
application:
name: han-note-auth # 必须在 bootstrap 阶段就设置应用名
cloud:
nacos:
config:
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
file-extension: yaml # 配置文件格式
refresh-enabled: true # 是否开启动态刷新
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址

View File

@@ -25,4 +25,13 @@
where status = 0
and is_deleted = 0;
</select>
<select id="selectByPrimaryKey" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from t_role
where id = #{id,jdbcType=BIGINT}
and is_deleted = 0
and status = 0;
</select>
</mapper>