初始化
This commit is contained in:
32
mokee-gateway-auth/pom.xml
Normal file
32
mokee-gateway-auth/pom.xml
Normal 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.mokee</groupId>
|
||||
<artifactId>mokee-gateway</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>mokee-gateway-auth</artifactId>
|
||||
<name>mokee-gateway-auth</name>
|
||||
<description>认证用户服务 — 登录/登出/Token/用户/角色/菜单</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.mokee</groupId>
|
||||
<artifactId>mokee-gateway-common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.mokee.auth;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 认证用户服务 — 启动类
|
||||
* <p>
|
||||
* 负责用户登录/登出、Token 管理、用户信息查询、菜单路由、系统切换等
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.mokee.auth", "com.mokee.common"})
|
||||
@MapperScan("com.mokee.auth.mapper")
|
||||
public class AuthApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AuthApplication.class, args);
|
||||
System.out.println("========================================");
|
||||
System.out.println(" 认证用户服务启动成功! 端口: 9001");
|
||||
System.out.println(" Knife4j 文档: http://localhost:9001/doc.html");
|
||||
System.out.println("========================================");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.mokee.auth.controller;
|
||||
|
||||
import com.mokee.auth.service.AuthService;
|
||||
import com.mokee.common.Result;
|
||||
import com.mokee.common.dto.request.LoginRequest;
|
||||
import com.mokee.common.dto.response.LoginResponse;
|
||||
import com.mokee.common.dto.response.MenuRouterResponse;
|
||||
import com.mokee.common.dto.response.MenuRouterVO;
|
||||
import com.mokee.common.dto.response.SystemVO;
|
||||
import com.mokee.common.dto.response.UserInfoVO;
|
||||
import com.mokee.common.utils.IpUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* <p>
|
||||
* 提供登录/登出/Token校验/用户信息/菜单路由/系统切换/密码修改等接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/zgapi/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* <p>
|
||||
* 校验用户名密码,成功后返回 JWT Token 及用户信息
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest req, HttpServletRequest request) {
|
||||
String clientIp = IpUtil.getClientIp(request);
|
||||
String userAgent = request.getHeader("User-Agent");
|
||||
log.info("用户登录请求 - username: {}, ip: {}", req.getUsername(), clientIp);
|
||||
LoginResponse response = authService.login(req, clientIp, userAgent);
|
||||
return Result.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
* <p>
|
||||
* 清除 Redis 中的登录会话,使 Token 失效
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public Result<?> logout(@RequestHeader("Authorization") String authHeader) {
|
||||
String token = extractToken(authHeader);
|
||||
log.info("用户登出请求");
|
||||
authService.logout(token);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*/
|
||||
@GetMapping("/user/info")
|
||||
public Result<UserInfoVO> getUserInfo(@RequestHeader("Authorization") String authHeader) {
|
||||
String token = extractToken(authHeader);
|
||||
UserInfoVO userInfo = authService.getUserInfo(token);
|
||||
return Result.ok(userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前系统的菜单路由树和权限列表
|
||||
* <p>
|
||||
* 根据用户角色权限,返回前端动态路由所需的树形菜单数据 + 该系统所有权限标识
|
||||
*/
|
||||
@GetMapping("/menu/router")
|
||||
public Result<MenuRouterResponse> getMenuRouter(@RequestHeader("Authorization") String authHeader,
|
||||
@RequestParam String systemCode) {
|
||||
String token = extractToken(authHeader);
|
||||
MenuRouterResponse menuRouter = authService.getMenuRouter(token, systemCode);
|
||||
return Result.ok(menuRouter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可切换的业务系统列表
|
||||
*/
|
||||
@GetMapping("/system/list")
|
||||
public Result<List<SystemVO>> getSystemList(@RequestHeader("Authorization") String authHeader) {
|
||||
String token = extractToken(authHeader);
|
||||
List<SystemVO> systemList = authService.getSystemList(token);
|
||||
return Result.ok(systemList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换当前业务系统
|
||||
* <p>
|
||||
* 切换后重新计算角色和权限,并更新 Redis 会话
|
||||
*/
|
||||
@PostMapping("/system/select")
|
||||
public Result<?> selectSystem(@RequestHeader("Authorization") String authHeader,
|
||||
@RequestParam String systemId) {
|
||||
String token = extractToken(authHeader);
|
||||
authService.selectSystem(token, systemId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* <p>
|
||||
* 需验证旧密码,新密码 BCrypt 加密后存储
|
||||
*/
|
||||
@PutMapping("/password/update")
|
||||
public Result<?> updatePassword(@RequestHeader("Authorization") String authHeader,
|
||||
@RequestParam String oldPwd,
|
||||
@RequestParam String newPwd) {
|
||||
String token = extractToken(authHeader);
|
||||
authService.updatePassword(token, oldPwd, newPwd);
|
||||
return Result.ok("密码修改成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Authorization 头中提取 Bearer Token
|
||||
*/
|
||||
private String extractToken(String authHeader) {
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
return authHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysLoginLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 登录日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysLoginLogMapper extends BaseMapper<SysLoginLog> {
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysMenu;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysMenuMapper extends BaseMapper<SysMenu> {
|
||||
|
||||
/**
|
||||
* 查询多个角色拥有的菜单(去重)
|
||||
*
|
||||
* @param roleIds 角色ID列表
|
||||
* @param systemId 系统ID
|
||||
* @return 去重后的菜单列表
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT DISTINCT m.* FROM sys_menu m " +
|
||||
"INNER JOIN sys_role_menu rm ON m.id = rm.menu_id " +
|
||||
"WHERE rm.role_id IN " +
|
||||
"<foreach collection='roleIds' item='roleId' open='(' separator=',' close=')'>#{roleId}</foreach> " +
|
||||
"AND m.system_id = #{systemId} " +
|
||||
"AND m.status = 1 " +
|
||||
"ORDER BY m.sort_order ASC" +
|
||||
"</script>")
|
||||
List<SysMenu> selectDistinctByRoleIds(@Param("roleIds") List<String> roleIds, @Param("systemId") String systemId);
|
||||
|
||||
/**
|
||||
* 查询多个角色的权限标识列表
|
||||
*
|
||||
* @param roleIds 角色ID列表
|
||||
* @return 权限标识列表(已去重,过滤空值)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT DISTINCT m.permission FROM sys_menu m " +
|
||||
"INNER JOIN sys_role_menu rm ON m.id = rm.menu_id " +
|
||||
"WHERE rm.role_id IN " +
|
||||
"<foreach collection='roleIds' item='roleId' open='(' separator=',' close=')'>#{roleId}</foreach> " +
|
||||
"AND m.permission IS NOT NULL AND m.permission != '' " +
|
||||
"AND m.status = 1" +
|
||||
"</script>")
|
||||
List<String> selectPermissionsByRoleIds(@Param("roleIds") List<String> roleIds);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysRole;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysRoleMapper extends BaseMapper<SysRole> {
|
||||
|
||||
/**
|
||||
* 查询用户在指定系统中的角色列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param systemId 系统ID
|
||||
* @return 角色列表
|
||||
*/
|
||||
@Select("SELECT r.* FROM sys_role r " +
|
||||
"INNER JOIN sys_user_role ur ON r.id = ur.role_id " +
|
||||
"WHERE ur.user_id = #{userId} AND r.system_id = #{systemId} " +
|
||||
"AND r.status = 1 AND r.is_deleted = 0")
|
||||
List<SysRole> selectListByUserIdAndSystemId(@Param("userId") String userId, @Param("systemId") String systemId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysSystem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统信息 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysSystemMapper extends BaseMapper<SysSystem> {
|
||||
|
||||
/**
|
||||
* 查询用户绑定的启用系统列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 系统列表
|
||||
*/
|
||||
@Select("SELECT s.* FROM sys_system s " +
|
||||
"INNER JOIN sys_user_system us ON s.id = us.system_id " +
|
||||
"WHERE us.user_id = #{userId} " +
|
||||
"AND s.status = 1 AND s.is_deleted = 0 " +
|
||||
"ORDER BY s.sort_order ASC")
|
||||
List<SysSystem> selectListByUserId(@Param("userId") String userId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysUserMapper extends BaseMapper<SysUser> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysUserRole;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户角色关联 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysUserRoleMapper extends BaseMapper<SysUserRole> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mokee.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mokee.common.entity.SysUserSystem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户系统关联 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysUserSystemMapper extends BaseMapper<SysUserSystem> {
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.mokee.auth.service;
|
||||
|
||||
import com.mokee.common.dto.request.LoginRequest;
|
||||
import com.mokee.common.dto.response.LoginResponse;
|
||||
import com.mokee.common.dto.response.MenuRouterResponse;
|
||||
import com.mokee.common.dto.response.MenuRouterVO;
|
||||
import com.mokee.common.dto.response.SystemVO;
|
||||
import com.mokee.common.dto.response.UserInfoVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
* <p>
|
||||
* 定义登录、登出、用户信息、菜单路由、系统切换等核心认证方法
|
||||
*/
|
||||
public interface AuthService {
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param req 登录请求(用户名、密码、可选系统编码)
|
||||
* @param ip 客户端 IP
|
||||
* @param userAgent 浏览器 User-Agent
|
||||
* @return 登录响应(Token + 用户信息 + 系统列表)
|
||||
*/
|
||||
LoginResponse login(LoginRequest req, String ip, String userAgent);
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
* <p>
|
||||
* 清除 Redis 中的登录会话
|
||||
*
|
||||
* @param token JWT Token
|
||||
*/
|
||||
void logout(String token);
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 用户信息 VO
|
||||
*/
|
||||
UserInfoVO getUserInfo(String token);
|
||||
|
||||
/**
|
||||
* 获取指定系统的菜单路由树和权限列表
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @param systemCode 系统编码
|
||||
* @return 菜单路由响应(含菜单树 + 权限列表)
|
||||
*/
|
||||
MenuRouterResponse getMenuRouter(String token, String systemCode);
|
||||
|
||||
/**
|
||||
* 获取当前用户可切换的业务系统列表
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 系统列表
|
||||
*/
|
||||
List<SystemVO> getSystemList(String token);
|
||||
|
||||
/**
|
||||
* 切换当前业务系统
|
||||
* <p>
|
||||
* 切换后更新 Redis 会话中的 currentSystem 信息
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @param systemId 目标系统 ID
|
||||
*/
|
||||
void selectSystem(String token, String systemId);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* <p>
|
||||
* 验证旧密码后使用 BCrypt 加密新密码并更新
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @param oldPwd 旧密码
|
||||
* @param newPwd 新密码
|
||||
*/
|
||||
void updatePassword(String token, String oldPwd, String newPwd);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.mokee.auth.service;
|
||||
|
||||
import com.mokee.auth.mapper.SysLoginLogMapper;
|
||||
import com.mokee.common.entity.SysLoginLog;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 异步日志写入服务
|
||||
* <p>
|
||||
* 将登录日志的写入从主业务线程剥离,避免同步数据库写入阻塞登录接口响应。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LogAsyncService {
|
||||
|
||||
private final SysLoginLogMapper sysLoginLogMapper;
|
||||
|
||||
/**
|
||||
* 异步写入登录日志
|
||||
*/
|
||||
@Async("logTaskExecutor")
|
||||
public void saveLoginLog(SysLoginLog logEntity) {
|
||||
try {
|
||||
sysLoginLogMapper.insert(logEntity);
|
||||
} catch (Exception e) {
|
||||
log.error("异步写入登录日志失败 - username: {}", logEntity.getUsername(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
package com.mokee.auth.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.mokee.auth.mapper.*;
|
||||
import com.mokee.auth.service.AuthService;
|
||||
import com.mokee.auth.service.LogAsyncService;
|
||||
import com.mokee.common.dto.request.LoginRequest;
|
||||
import com.mokee.common.dto.response.*;
|
||||
import com.mokee.common.entity.*;
|
||||
import com.mokee.common.enums.StatusEnum;
|
||||
import com.mokee.common.exception.BusinessException;
|
||||
import com.mokee.common.exception.ErrorCode;
|
||||
import com.mokee.common.utils.JwtUtil;
|
||||
import com.mokee.common.utils.RedisUtil;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 认证服务实现类
|
||||
* <p>
|
||||
* 完整登录流程:限流 → 锁定检查 → 用户验证 → 密码校验 → 锁定/成功处理 → 系统查询 → 角色查询 → Token生成 → Redis缓存
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final SysRoleMapper sysRoleMapper;
|
||||
private final SysMenuMapper sysMenuMapper;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final SysUserSystemMapper sysUserSystemMapper;
|
||||
private final SysSystemMapper sysSystemMapper;
|
||||
private final SysLoginLogMapper sysLoginLogMapper;
|
||||
private final LogAsyncService logAsyncService;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
/** 登录失败最大次数 */
|
||||
private static final int MAX_FAIL_COUNT = 5;
|
||||
/** 锁定时间(分钟) */
|
||||
private static final int LOCK_MINUTES = 5;
|
||||
/** 限流每分钟最大请求数 */
|
||||
private static final int RATE_LIMIT_MAX = 10;
|
||||
/** Token 缓存时间(小时) */
|
||||
private static final int TOKEN_TTL_HOURS = 2;
|
||||
/** Token 续期阈值(秒),小于此值自动续期 */
|
||||
private static final long RENEW_THRESHOLD_SECONDS = 1800;
|
||||
/** 本地缓存 TTL(分钟),用 Caffeine 避免跨子网 DB/Redis 查询 */
|
||||
private static final int CACHE_TTL_MINUTES = 3;
|
||||
|
||||
// Caffeine 本地缓存(微秒级命中,不走网络)
|
||||
private Cache<String, UserInfoVO> userInfoCache;
|
||||
private Cache<String, List<SystemVO>> systemListCache;
|
||||
private Cache<String, MenuRouterResponse> menuRouterCache;
|
||||
|
||||
@PostConstruct
|
||||
public void initCaches() {
|
||||
userInfoCache = Caffeine.newBuilder()
|
||||
.maximumSize(500).expireAfterWrite(Duration.ofMinutes(CACHE_TTL_MINUTES)).build();
|
||||
systemListCache = Caffeine.newBuilder()
|
||||
.maximumSize(500).expireAfterWrite(Duration.ofMinutes(CACHE_TTL_MINUTES)).build();
|
||||
menuRouterCache = Caffeine.newBuilder()
|
||||
.maximumSize(500).expireAfterWrite(Duration.ofMinutes(CACHE_TTL_MINUTES)).build();
|
||||
}
|
||||
|
||||
// ==================== 登录 ====================
|
||||
|
||||
@Override
|
||||
public LoginResponse login(LoginRequest req, String ip, String userAgent) {
|
||||
// 1. 限流检查:按 IP 限制登录频率,每分钟最多 10 次
|
||||
String rateKey = RedisUtil.key("rate:login:" + ip);
|
||||
Long rateCount = redisUtil.incr(rateKey);
|
||||
if (rateCount == 1) {
|
||||
redisUtil.expire(rateKey, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
if (rateCount > RATE_LIMIT_MAX) {
|
||||
log.warn("登录限流触发 - ip: {}, count: {}", ip, rateCount);
|
||||
throw new BusinessException(ErrorCode.RATE_LIMITED);
|
||||
}
|
||||
|
||||
// 2. 锁定检查:检查 Redis 中是否存在账号锁定标记
|
||||
String lockKey = RedisUtil.key("lock:login:" + req.getUsername());
|
||||
if (redisUtil.exists(lockKey)) {
|
||||
log.warn("账号锁定中(Redis标记) - username: {}", req.getUsername());
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_LOCKED);
|
||||
}
|
||||
|
||||
// 3. 查询用户
|
||||
SysUser user = sysUserMapper.selectOne(
|
||||
new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, req.getUsername())
|
||||
);
|
||||
if (user == null) {
|
||||
log.warn("用户不存在 - username: {}", req.getUsername());
|
||||
throw new BusinessException(ErrorCode.USERNAME_PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
// 4. 状态校验:禁用/锁定的用户拒绝登录
|
||||
if (user.getStatus() == null || user.getStatus().equals(StatusEnum.DISABLED.getCode())) {
|
||||
log.warn("用户已禁用 - username: {}", req.getUsername());
|
||||
throw new BusinessException(ErrorCode.USER_DISABLED);
|
||||
}
|
||||
if (user.getStatus().equals(StatusEnum.LOCKED.getCode())
|
||||
&& user.getLockUntil() != null
|
||||
&& user.getLockUntil().isAfter(LocalDateTime.now())) {
|
||||
log.warn("账号已被锁定 - username: {}, lockUntil: {}", req.getUsername(), user.getLockUntil());
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_LOCKED);
|
||||
}
|
||||
|
||||
// 5. 密码校验:使用 BCrypt 比对
|
||||
boolean passwordMatch = BCrypt.checkpw(req.getPassword(), user.getPassword());
|
||||
if (!passwordMatch) {
|
||||
return handleLoginFailure(user, ip, userAgent, "密码错误");
|
||||
}
|
||||
|
||||
// 6. 登录成功处理
|
||||
return handleLoginSuccess(user, req.getSystemCode(), ip, userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录失败:累计失败次数、超限锁定账号、记录失败日志
|
||||
*/
|
||||
private LoginResponse handleLoginFailure(SysUser user, String ip, String userAgent, String failReason) {
|
||||
int failCount = (user.getLoginFailCount() == null ? 0 : user.getLoginFailCount()) + 1;
|
||||
user.setLoginFailCount(failCount);
|
||||
|
||||
// 连续失败 >= 5 次,锁定账号 30 分钟
|
||||
if (failCount >= MAX_FAIL_COUNT) {
|
||||
user.setStatus(StatusEnum.LOCKED.getCode());
|
||||
LocalDateTime lockUntil = LocalDateTime.now().plusMinutes(LOCK_MINUTES);
|
||||
user.setLockUntil(lockUntil);
|
||||
|
||||
// 在 Redis 中设置锁定标记,30 分钟后自动过期
|
||||
redisUtil.set(RedisUtil.key("lock:login:" + user.getUsername()), "1", LOCK_MINUTES, TimeUnit.MINUTES);
|
||||
log.warn("账号已被锁定 - username: {}, lockUntil: {}", user.getUsername(), lockUntil);
|
||||
}
|
||||
sysUserMapper.updateById(user);
|
||||
|
||||
// 记录登录失败日志
|
||||
saveLoginLog(user.getId(), user.getUsername(), user.getRealName(), null, ip, userAgent, 0, failReason);
|
||||
|
||||
throw new BusinessException(ErrorCode.USERNAME_PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录成功:重置失败计数、更新登录信息、查询系统/角色、生成Token并缓存Redis
|
||||
*/
|
||||
private LoginResponse handleLoginSuccess(SysUser user, String systemCode, String ip, String userAgent) {
|
||||
// 重置失败次数并更新登录信息
|
||||
user.setLoginFailCount(0);
|
||||
user.setLastLoginIp(ip);
|
||||
user.setLastLoginTime(LocalDateTime.now());
|
||||
sysUserMapper.updateById(user);
|
||||
|
||||
// 查询用户绑定的业务系统列表
|
||||
List<SysSystem> boundSystems = sysSystemMapper.selectListByUserId(user.getId());
|
||||
if (boundSystems.isEmpty()) {
|
||||
log.warn("用户未绑定任何业务系统 - userId: {}", user.getId());
|
||||
throw new BusinessException(ErrorCode.SYSTEM_DISABLED);
|
||||
}
|
||||
|
||||
// 确定当前系统:优先使用请求指定的 systemCode,其次取唯一边界系统
|
||||
SysSystem currentSystem = null;
|
||||
if (StrUtil.isNotBlank(systemCode)) {
|
||||
currentSystem = boundSystems.stream()
|
||||
.filter(s -> systemCode.equals(s.getSystemCode()))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
if (currentSystem == null && boundSystems.size() == 1) {
|
||||
currentSystem = boundSystems.get(0);
|
||||
}
|
||||
|
||||
// 查询用户在当前系统中的角色及权限
|
||||
List<RoleInfo> roleInfoList = new ArrayList<>();
|
||||
List<String> permissionList = new ArrayList<>();
|
||||
if (currentSystem != null) {
|
||||
List<SysRole> roles = sysRoleMapper.selectListByUserIdAndSystemId(user.getId(), currentSystem.getId());
|
||||
roleInfoList = roles.stream()
|
||||
.map(r -> new RoleInfo(r.getId(), r.getRoleName(), r.getRoleCode()))
|
||||
.collect(Collectors.toList());
|
||||
if (!roles.isEmpty()) {
|
||||
List<String> roleIds = roles.stream().map(SysRole::getId).collect(Collectors.toList());
|
||||
permissionList = sysMenuMapper.selectPermissionsByRoleIds(roleIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 JWT Token(JwtUtil 内部自动生成 UUID 作为 tokenId)
|
||||
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
Claims tokenClaims = jwtUtil.validateToken(token);
|
||||
String actualTokenId = jwtUtil.getTokenId(tokenClaims);
|
||||
|
||||
// 构建 TokenInfo 并缓存到 Redis(TTL 2小时)
|
||||
TokenInfo tokenInfo = new TokenInfo();
|
||||
tokenInfo.setUserId(user.getId());
|
||||
tokenInfo.setUsername(user.getUsername());
|
||||
tokenInfo.setRealName(user.getRealName());
|
||||
tokenInfo.setEmail(user.getEmail());
|
||||
tokenInfo.setPhone(user.getPhone());
|
||||
tokenInfo.setAvatar(user.getAvatar());
|
||||
tokenInfo.setPost(user.getPost());
|
||||
tokenInfo.setRoles(roleInfoList);
|
||||
tokenInfo.setPermissions(permissionList);
|
||||
if (currentSystem != null) {
|
||||
tokenInfo.setCurrentSystemId(currentSystem.getId());
|
||||
tokenInfo.setCurrentSystemCode(currentSystem.getSystemCode());
|
||||
tokenInfo.setCurrentSystemName(currentSystem.getSystemName());
|
||||
}
|
||||
|
||||
redisUtil.set(RedisUtil.key("auth:token:" + actualTokenId), JSONUtil.toJsonStr(tokenInfo), TOKEN_TTL_HOURS, TimeUnit.HOURS);
|
||||
// 反向索引:userId → tokenId,用于用户信息更新时同步刷新 Redis
|
||||
redisUtil.set(RedisUtil.key("auth:user_session:" + user.getId()), actualTokenId, TOKEN_TTL_HOURS, TimeUnit.HOURS);
|
||||
|
||||
// 记录登录成功日志
|
||||
saveLoginLog(user.getId(), user.getUsername(), user.getRealName(),
|
||||
currentSystem != null ? currentSystem.getId() : null,
|
||||
ip, userAgent, 1, null);
|
||||
|
||||
// 登录成功后刷新本地缓存(下次查询自动重建)
|
||||
userInfoCache.invalidate(user.getId());
|
||||
systemListCache.invalidate(user.getId());
|
||||
if (currentSystem != null) {
|
||||
menuRouterCache.invalidate(currentSystem.getId() + ":" + user.getId());
|
||||
}
|
||||
|
||||
// 构建响应对象
|
||||
UserInfoVO userInfoVO = new UserInfoVO();
|
||||
userInfoVO.setUserId(user.getId());
|
||||
userInfoVO.setUsername(user.getUsername());
|
||||
userInfoVO.setRealName(user.getRealName());
|
||||
userInfoVO.setEmail(user.getEmail());
|
||||
userInfoVO.setPhone(user.getPhone());
|
||||
userInfoVO.setAvatar(user.getAvatar());
|
||||
userInfoVO.setPost(user.getPost());
|
||||
|
||||
List<SystemVO> systemVOList = boundSystems.stream()
|
||||
.map(s -> new SystemVO(s.getId(), s.getSystemCode(), s.getSystemName(),
|
||||
s.getFrontUrl(), s.getIcon(), s.getDescription()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
SystemVO currentSystemVO = null;
|
||||
if (currentSystem != null) {
|
||||
currentSystemVO = new SystemVO(currentSystem.getId(), currentSystem.getSystemCode(),
|
||||
currentSystem.getSystemName(), currentSystem.getFrontUrl(),
|
||||
currentSystem.getIcon(), currentSystem.getDescription());
|
||||
}
|
||||
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setToken(token);
|
||||
response.setUserInfo(userInfoVO);
|
||||
response.setSystemList(systemVOList);
|
||||
response.setNeedSelectSystem(currentSystem == null && boundSystems.size() > 1);
|
||||
response.setCurrentSystem(currentSystemVO);
|
||||
// 判断是否为平台管理员(拥有 super_admin 角色的用户可以进入后台管理)
|
||||
boolean isAdmin = roleInfoList.stream().anyMatch(r -> "super_admin".equals(r.getRoleCode()));
|
||||
response.setIsAdmin(isAdmin);
|
||||
response.setPermissions(permissionList);
|
||||
|
||||
log.info("用户登录成功 - username: {}, ip: {}, tokenId: {}", user.getUsername(), ip, actualTokenId);
|
||||
return response;
|
||||
}
|
||||
|
||||
// ==================== 登出 ====================
|
||||
|
||||
@Override
|
||||
public void logout(String token) {
|
||||
try {
|
||||
Claims claims = jwtUtil.validateToken(token);
|
||||
String tokenId = jwtUtil.getTokenId(claims);
|
||||
String userId = jwtUtil.getUserId(claims);
|
||||
redisUtil.delete(RedisUtil.key(RedisUtil.key("auth:token:" + tokenId)));
|
||||
redisUtil.delete(RedisUtil.key("auth:user_session:" + userId));
|
||||
log.info("用户登出成功 - tokenId: {}, userId: {}", tokenId, userId);
|
||||
} catch (Exception e) {
|
||||
// Token 已过期或无效,忽略即可,登出本身就是为了清除会话
|
||||
log.warn("登出时 Token 无效,忽略 - {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 获取用户信息 ====================
|
||||
|
||||
@Override
|
||||
public UserInfoVO getUserInfo(String token) {
|
||||
TokenInfo tokenInfo = getTokenInfoFromRedis(token);
|
||||
String userId = tokenInfo.getUserId();
|
||||
|
||||
// 优先读本地缓存(微秒级)
|
||||
UserInfoVO cached = userInfoCache.getIfPresent(userId);
|
||||
if (cached != null) {
|
||||
if ("gateway".equals(tokenInfo.getCurrentSystemCode())) {
|
||||
cached.setPermissions(tokenInfo.getPermissions());
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 缓存未命中,查 DB 构建
|
||||
UserInfoVO vo = new UserInfoVO();
|
||||
vo.setUserId(tokenInfo.getUserId());
|
||||
vo.setUsername(tokenInfo.getUsername());
|
||||
vo.setRealName(tokenInfo.getRealName());
|
||||
vo.setEmail(tokenInfo.getEmail());
|
||||
vo.setPhone(tokenInfo.getPhone());
|
||||
vo.setAvatar(tokenInfo.getAvatar());
|
||||
vo.setPost(tokenInfo.getPost());
|
||||
if ("gateway".equals(tokenInfo.getCurrentSystemCode())) {
|
||||
vo.setPermissions(tokenInfo.getPermissions());
|
||||
}
|
||||
SysUser user = sysUserMapper.selectById(userId);
|
||||
if (user != null) {
|
||||
vo.setCreatedAt(user.getCreatedAt() != null ? user.getCreatedAt().toString() : null);
|
||||
vo.setLastLoginTime(user.getLastLoginTime() != null ? user.getLastLoginTime().toString() : null);
|
||||
}
|
||||
|
||||
userInfoCache.put(userId, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ==================== 获取菜单路由 ====================
|
||||
|
||||
@Override
|
||||
public MenuRouterResponse getMenuRouter(String token, String systemCode) {
|
||||
TokenInfo tokenInfo = getTokenInfoFromRedis(token);
|
||||
String userId = tokenInfo.getUserId();
|
||||
|
||||
// 查询指定系统
|
||||
SysSystem system = sysSystemMapper.selectOne(
|
||||
new LambdaQueryWrapper<SysSystem>().eq(SysSystem::getSystemCode, systemCode)
|
||||
);
|
||||
if (system == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
String systemId = system.getId();
|
||||
String cacheKey = systemId + ":" + userId;
|
||||
|
||||
// 优先读本地缓存(微秒级)
|
||||
MenuRouterResponse cached = menuRouterCache.getIfPresent(cacheKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 缓存未命中,查 DB 构建
|
||||
List<SysRole> userRoles = sysRoleMapper.selectListByUserIdAndSystemId(userId, systemId);
|
||||
if (userRoles.isEmpty()) {
|
||||
return new MenuRouterResponse(Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
List<String> roleIds = userRoles.stream().map(SysRole::getId).collect(Collectors.toList());
|
||||
List<String> permissions = sysMenuMapper.selectPermissionsByRoleIds(roleIds);
|
||||
List<SysMenu> menus = sysMenuMapper.selectDistinctByRoleIds(roleIds, systemId);
|
||||
List<SysMenu> routerMenus = menus.stream()
|
||||
.filter(m -> m.getStatus() == 1)
|
||||
.filter(m -> m.getMenuType() == 1 || m.getMenuType() == 2)
|
||||
.sorted(Comparator.comparingInt(m -> m.getSortOrder() != null ? m.getSortOrder() : 0))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<MenuRouterVO> menuTree = buildMenuTree(routerMenus, "0");
|
||||
MenuRouterResponse response = new MenuRouterResponse(menuTree, permissions);
|
||||
menuRouterCache.put(cacheKey, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建菜单路由树
|
||||
*
|
||||
* @param allMenus 所有菜单列表
|
||||
* @param parentId 当前父节点 ID
|
||||
* @return 树形路由节点列表
|
||||
*/
|
||||
private List<MenuRouterVO> buildMenuTree(List<SysMenu> allMenus, String parentId) {
|
||||
List<MenuRouterVO> list = new ArrayList<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
if (menu.getParentId().equals(parentId)) {
|
||||
MenuRouterVO vo = new MenuRouterVO();
|
||||
vo.setId(menu.getId());
|
||||
vo.setMenuName(menu.getMenuName());
|
||||
vo.setMenuType(menu.getMenuType());
|
||||
vo.setIcon(menu.getIcon());
|
||||
vo.setPermission(menu.getPermission());
|
||||
vo.setPath(menu.getMenuPath());
|
||||
vo.setName(menu.getMenuName());
|
||||
vo.setComponent(menu.getComponent());
|
||||
vo.setSortOrder(menu.getSortOrder());
|
||||
vo.setStatus(menu.getStatus());
|
||||
vo.setMeta(new MenuMetaVO(menu.getMenuName(), menu.getIcon()));
|
||||
// 递归构建子菜单
|
||||
List<MenuRouterVO> children = buildMenuTree(allMenus, menu.getId());
|
||||
vo.setChildren(children.isEmpty() ? null : children);
|
||||
list.add(vo);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ==================== 获取系统列表 ====================
|
||||
|
||||
@Override
|
||||
public List<SystemVO> getSystemList(String token) {
|
||||
TokenInfo tokenInfo = getTokenInfoFromRedis(token);
|
||||
String userId = tokenInfo.getUserId();
|
||||
|
||||
// 优先读本地缓存(微秒级)
|
||||
List<SystemVO> cached = systemListCache.getIfPresent(userId);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 缓存未命中,查 DB
|
||||
List<SysSystem> systems = sysSystemMapper.selectListByUserId(userId);
|
||||
List<SystemVO> result = systems.stream()
|
||||
.map(s -> new SystemVO(s.getId(), s.getSystemCode(), s.getSystemName(),
|
||||
s.getFrontUrl(), s.getIcon(), s.getDescription()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
systemListCache.put(userId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== 切换系统 ====================
|
||||
|
||||
@Override
|
||||
public void selectSystem(String token, String systemId) {
|
||||
Claims claims = jwtUtil.validateToken(token);
|
||||
String tokenId = jwtUtil.getTokenId(claims);
|
||||
String userId = jwtUtil.getUserId(claims);
|
||||
|
||||
// 验证该系统是否属于用户的绑定系统
|
||||
List<SysSystem> userSystems = sysSystemMapper.selectListByUserId(userId);
|
||||
boolean hasSystem = userSystems.stream().anyMatch(s -> s.getId().equals(systemId));
|
||||
if (!hasSystem) {
|
||||
throw new BusinessException(ErrorCode.FORBIDDEN);
|
||||
}
|
||||
|
||||
// 获取当前 Redis 中的 TokenInfo
|
||||
String redisKey = RedisUtil.key("auth:token:" + tokenId);
|
||||
String jsonStr = redisUtil.get(redisKey);
|
||||
if (jsonStr == null) {
|
||||
throw new BusinessException(ErrorCode.TOKEN_EXPIRED);
|
||||
}
|
||||
TokenInfo tokenInfo = JSONUtil.toBean(jsonStr, TokenInfo.class);
|
||||
|
||||
// 查询目标系统信息
|
||||
SysSystem targetSystem = sysSystemMapper.selectById(systemId);
|
||||
if (targetSystem == null || targetSystem.getStatus() == 0) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_DISABLED);
|
||||
}
|
||||
|
||||
// 更新 TokenInfo 中的当前系统信息
|
||||
tokenInfo.setCurrentSystemId(targetSystem.getId());
|
||||
tokenInfo.setCurrentSystemCode(targetSystem.getSystemCode());
|
||||
tokenInfo.setCurrentSystemName(targetSystem.getSystemName());
|
||||
|
||||
// 重新查询该系统中的角色和权限
|
||||
List<SysRole> roles = sysRoleMapper.selectListByUserIdAndSystemId(userId, systemId);
|
||||
tokenInfo.setRoles(roles.stream()
|
||||
.map(r -> new RoleInfo(r.getId(), r.getRoleName(), r.getRoleCode()))
|
||||
.collect(Collectors.toList()));
|
||||
if (!roles.isEmpty()) {
|
||||
List<String> roleIds = roles.stream().map(SysRole::getId).collect(Collectors.toList());
|
||||
tokenInfo.setPermissions(sysMenuMapper.selectPermissionsByRoleIds(roleIds));
|
||||
} else {
|
||||
tokenInfo.setPermissions(Collections.emptyList());
|
||||
}
|
||||
|
||||
// 更新 Redis 缓存,保持原有 TTL
|
||||
Long ttl = redisUtil.getExpire(redisKey);
|
||||
if (ttl != null && ttl > 0) {
|
||||
redisUtil.set(redisKey, JSONUtil.toJsonStr(tokenInfo), ttl, TimeUnit.SECONDS);
|
||||
} else {
|
||||
redisUtil.set(redisKey, JSONUtil.toJsonStr(tokenInfo), TOKEN_TTL_HOURS, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
// 刷新缓存:切换系统后下次查询自动重建
|
||||
menuRouterCache.invalidate(systemId + ":" + userId);
|
||||
|
||||
// 更新最近一条登录日志的选择系统字段
|
||||
try {
|
||||
SysLoginLog latestLog = sysLoginLogMapper.selectOne(
|
||||
new LambdaQueryWrapper<SysLoginLog>()
|
||||
.eq(SysLoginLog::getUserId, userId)
|
||||
.eq(SysLoginLog::getStatus, 1)
|
||||
.orderByDesc(SysLoginLog::getCreatedAt)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (latestLog != null) {
|
||||
latestLog.setSystemId(systemId);
|
||||
sysLoginLogMapper.updateById(latestLog);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("更新登录日志系统ID失败 - userId: {}, systemId: {}", userId, systemId, e);
|
||||
}
|
||||
|
||||
log.info("用户切换系统 - userId: {}, systemId: {}, systemName: {}", userId, systemId, targetSystem.getSystemName());
|
||||
}
|
||||
|
||||
// ==================== 修改密码 ====================
|
||||
|
||||
@Override
|
||||
public void updatePassword(String token, String oldPwd, String newPwd) {
|
||||
Claims claims = jwtUtil.validateToken(token);
|
||||
String userId = jwtUtil.getUserId(claims);
|
||||
|
||||
SysUser user = sysUserMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if (!BCrypt.checkpw(oldPwd, user.getPassword())) {
|
||||
throw new BusinessException(400, "旧密码错误");
|
||||
}
|
||||
|
||||
// BCrypt 加密新密码并更新
|
||||
user.setPassword(BCrypt.hashpw(newPwd));
|
||||
sysUserMapper.updateById(user);
|
||||
|
||||
log.info("用户密码修改成功 - userId: {}", userId);
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 从 Redis 中获取 TokenInfo 并进行续期检查
|
||||
* <p>
|
||||
* 如果 Token 剩余有效时间不足 30 分钟,自动续期为 2 小时
|
||||
*/
|
||||
private TokenInfo getTokenInfoFromRedis(String token) {
|
||||
Claims claims = jwtUtil.validateToken(token);
|
||||
String tokenId = jwtUtil.getTokenId(claims);
|
||||
String redisKey = RedisUtil.key("auth:token:" + tokenId);
|
||||
|
||||
String jsonStr = redisUtil.get(redisKey);
|
||||
if (jsonStr == null) {
|
||||
throw new BusinessException(ErrorCode.TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
// 每次请求都刷新 TTL(GET + EXPIRE 共 2 次 Redis 往返,省掉 TTL 检查)
|
||||
redisUtil.expire(redisKey, TOKEN_TTL_HOURS, TimeUnit.HOURS);
|
||||
|
||||
return JSONUtil.toBean(jsonStr, TokenInfo.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存登录日志到数据库
|
||||
*/
|
||||
private void saveLoginLog(String userId, String username, String realName, String systemId,
|
||||
String ip, String userAgent, int status, String failReason) {
|
||||
SysLoginLog loginLog = SysLoginLog.builder()
|
||||
.userId(userId)
|
||||
.username(username)
|
||||
.realName(realName)
|
||||
.systemId(systemId)
|
||||
.loginIp(ip)
|
||||
.userAgent(userAgent)
|
||||
.browser(com.mokee.common.utils.UserAgentUtil.parseBrowser(userAgent))
|
||||
.os(com.mokee.common.utils.UserAgentUtil.parseOs(userAgent))
|
||||
.status(status)
|
||||
.failReason(failReason)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
logAsyncService.saveLoginLog(loginLog);
|
||||
}
|
||||
}
|
||||
40
mokee-gateway-auth/src/main/resources/application-prod.yml
Normal file
40
mokee-gateway-auth/src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
app:
|
||||
env-prefix: "[PROD]"
|
||||
|
||||
spring:
|
||||
mail:
|
||||
host: smtp.qiye.aliyun.com
|
||||
port: 465
|
||||
username: admin@zgitm.com
|
||||
password: mokee2016.
|
||||
properties:
|
||||
mail.smtp.auth: true
|
||||
mail.smtp.ssl.enable: true
|
||||
mail.smtp.socketFactory.port: 465
|
||||
mail.smtp.socketFactory.class: javax.net.ssl.SSLSocketFactory
|
||||
datasource:
|
||||
url: jdbc:mysql://db1.prod.zgitm.com:3306/mokeegateway?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: mokeegateway
|
||||
password: mokee.
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
data:
|
||||
redis:
|
||||
host: redis.db1.zgitm.com
|
||||
port: 6379
|
||||
password: mokee2016.
|
||||
database: 1
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.mokee: INFO
|
||||
org.springframework.web.servlet.DispatcherServlet: DEBUG
|
||||
39
mokee-gateway-auth/src/main/resources/application-qa.yml
Normal file
39
mokee-gateway-auth/src/main/resources/application-qa.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
app:
|
||||
env-prefix: "[QA]"
|
||||
|
||||
spring:
|
||||
mail:
|
||||
host: smtp.qiye.aliyun.com
|
||||
port: 465
|
||||
username: admin@zgitm.com
|
||||
password: mokee2016.
|
||||
properties:
|
||||
mail.smtp.auth: true
|
||||
mail.smtp.ssl.enable: true
|
||||
mail.smtp.socketFactory.port: 465
|
||||
mail.smtp.socketFactory.class: javax.net.ssl.SSLSocketFactory
|
||||
datasource:
|
||||
url: jdbc:mysql://db1.qa.zgitm.com:3306/mokeegatewayqa?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: mokeegatewayqa
|
||||
password: mokee.
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
data:
|
||||
redis:
|
||||
host: redis.db2.zgitm.com
|
||||
port: 6379
|
||||
password: mokee2016.
|
||||
database: 2
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.mokee: INFO
|
||||
30
mokee-gateway-auth/src/main/resources/application.yml
Normal file
30
mokee-gateway-auth/src/main/resources/application.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
server:
|
||||
port: 9001
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: mokee-gateway-auth
|
||||
profiles:
|
||||
active: @spring.profiles.active@
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
type-aliases-package: com.mokee.common.entity
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
jwt:
|
||||
secret: mokee-gateway-jwt-secret-key-2024-must-be-256-bits!!
|
||||
expiration: 7200
|
||||
|
||||
knife4j:
|
||||
enable: true
|
||||
openapi:
|
||||
title: 认证用户服务
|
||||
description: 登录/登出/Token/用户/角色/菜单
|
||||
version: 1.0.0
|
||||
Reference in New Issue
Block a user