RuoYi-Cloud 微服务安全认证体系深度解析
1. 整体架构概览
RuoYi-Cloud 的安全认证体系由以下几个核心组件构成:
- 网关服务(ruoyi-gateway):统一认证入口
- 认证服务(ruoyi-auth):处理用户登录认证
- 公共安全模块(ruoyi-common-security):提供安全认证核心逻辑
- Redis 缓存:存储用户登录信息
2. 安全认证技术栈
项目采用 JWT + Redis 的无状态认证机制,使用 JWT 进行令牌生成和验证,通过 Redis 缓存用户登录信息,实现分布式环境下的安全认证。
权限控制方式:项目使用自定义注解(@RequiresLogin、@RequiresRoles、@RequiresPermissions)结合 AOP 切面(PreAuthorizeAspect)实现方法级别的权限控制。
3. 请求处理完整流程
3.1 请求进入网关
当一个请求进入系统时,首先会经过网关服务。网关中配置了 AuthFilter 过滤器进行认证处理:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpRequest.Builder mutate = request.mutate();String url = request.getURI().getPath();// 跳过不需要验证的路径if (StringUtils.matches(url, ignoreWhite.getWhites())) {return chain.filter(exchange);}// ... 其他认证逻辑
}
3.2 白名单检查
网关首先检查请求路径是否在白名单中。白名单配置在 IgnoreWhiteProperties中:
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {private List<String> whites = new ArrayList<>();// ...
}
对于 /login、/logout 等路径,网关会直接放行,不进行认证检查。
3.3 JWT 令牌验证
对于不在白名单中的请求,网关会进行 JWT 令牌验证:
String token = getToken(request);
if (StringUtils.isEmpty(token)) {return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
3.4 Redis 登录状态验证
验证 JWT 令牌有效后,网关会进一步检查 Redis 中是否存在对应的登录信息:
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin) {return unauthorizedResponse(exchange, "登录状态已过期");
}
3.5 用户信息提取与传递
认证通过后,网关从 JWT 令牌中提取用户信息,并将其设置到请求头中传递给下游服务:
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {return unauthorizedResponse(exchange, "令牌验证失败");
}// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
3.6 请求转发到业务服务
网关完成认证后,将请求转发到对应的业务服务。
3.7 业务服务拦截器处理
业务服务中配置了 HeaderInterceptor拦截器,用于处理从网关传递过来的用户信息:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));String token = SecurityUtils.getToken();if (StringUtils.isNotEmpty(token)) {LoginUser loginUser = AuthUtil.getLoginUser(token);if (StringUtils.isNotNull(loginUser)) {AuthUtil.verifyLoginUserExpire(loginUser);SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);}}return true;
}
3.8 安全上下文设置
HeaderInterceptor 将用户信息存储在 SecurityContextHolder中,这是一个基于TransmittableThreadLocal的线程上下文:
private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
3.9 业务方法权限验证
在业务方法执行前,如果方法上有权限注解(如 @RequiresLogin、@RequiresRoles、@RequiresPermissions) 、PreAuthorizeAspect 切面会进行权限验证:
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {// 注解鉴权MethodSignature signature = (MethodSignature) joinPoint.getSignature();checkMethodAnnotation(signature.getMethod());// 执行原有逻辑return joinPoint.proceed();
}
3.10 权限验证逻辑
权限验证通过 AuthUtil 和 AuthLogic 实现:
public void checkMethodAnnotation(Method method) {// 校验 @RequiresLogin 注解RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);if (requiresLogin != null) {AuthUtil.checkLogin();}// 校验 @RequiresRoles 注解RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);if (requiresRoles != null) {AuthUtil.checkRole(requiresRoles);}// 校验 @RequiresPermissions 注解RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);if (requiresPermissions != null) {AuthUtil.checkPermi(requiresPermissions);}
}
3.11 业务逻辑执行
通过所有认证和权限检查后,业务方法开始执行。在业务逻辑中,可以通过 SecurityContextHolder获取当前用户信息:
String username = SecurityContextHolder.getUserName();
Long userId = SecurityContextHolder.getUserId();
3.12 请求完成清理
请求处理完成后,HeaderInterceptor 会清理线程上下文:
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {SecurityContextHolder.remove();
}
4. 典型请求流程示例
4.1 登录请求流程
- 用户发起
/login请求 - 请求到达网关,由于
/login在白名单中,直接放行 - 请求转发到认证服务 ruoyi-auth
TokenController处理登录请求SysLoginService验证用户名密码- 验证通过后,
TokenService生成 JWT 令牌并存储用户信息到 Redis - 返回 JWT 令牌给客户端
4.2 业务请求流程
- 用户携带 JWT 令牌发起业务请求(如
/user/info) - 请求到达网关
- 网关检查路径不在白名单中,进行认证处理
- 网关验证 JWT 令牌有效性
- 网关检查 Redis 中是否存在对应的登录信息
- 认证通过后,将用户信息添加到请求头中
- 请求转发到用户服务
- 用户服务的
HeaderInterceptor处理请求头中的用户信息 - 如果业务方法上有权限注解,
PreAuthorizeAspect进行权限验证 - 业务方法执行,通过
SecurityContextHolder获取用户信息 - 返回业务结果给客户端
5. 安全特性
5.1 无状态认证
使用 JWT 实现无状态认证,服务端不需要存储会话信息,便于水平扩展。
5.2 Redis 缓存增强安全性
虽然 JWT 本身是无状态的,但通过 Redis 存储用户登录信息,可以实现登录状态的管理和强制下线等功能。
5.3 双重验证机制
同时验证 JWT 令牌的有效性和 Redis 中的登录状态,提高安全性。
5.4 细粒度权限控制
支持基于角色和权限的细粒度访问控制,通过注解方式实现方法级别的权限验证。
5.5 线程上下文传递
通过 TransmittableThreadLocal 实现用户信息在线程间的传递,保证在异步环境下也能正确获取用户信息。
结论
RuoYi-Cloud 的安全认证体系通过网关统一认证和微服务内部权限验证的方式,构建了一个完整、安全、可扩展的认证解决方案。整个流程从请求进入网关开始,经过令牌验证、权限检查,到最后的业务处理和上下文清理,形成了一个闭环的安全保障体系。这种设计既保证了系统的安全性,又具有良好的可维护性和扩展性,为微服务架构下的安全认证提供了优秀的实践范例。
