调用过程是:ruoyi-auth —> ruoyi-system
登录请求 127.0.0.1:8080/auth/login
是特殊的请求。经过的过滤器有:
AuthFilter(order=-200)
XssFilter(order=-100)
CacheRequestFilter(order=1)
ValidateCodeFilter(order=2)
StripPrefix(order=3)
AuthFilter 认证过滤器对需要排除的 uri 地址,不检查是否有 token 直接放行(需要排除的配置可以在nacos中配置)。默认配置如下:
# 安全配置
security:
# 验证码
captcha:
enabled: true
type: math
# 防止XSS攻击
xss:
enabled: true
excludeUrls:
- /system/notice
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /auth/register
- /*/v2/api-docs
- /csrf
(注意是“认证”不是“鉴权”,认证主要是判断 token 是否有效,不涉及权限)
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form)
{
// 用户登录
LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
// 获取登录token
return R.ok(tokenService.createToken(userInfo));
}
请看 Service
/**
* 创建令牌
*/
public Map<String, Object> createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
// <1> 封装用户信息
Long userId = loginUser.getSysUser().getUserId();
String userName = loginUser.getSysUser().getUserName();
loginUser.setToken(token);
loginUser.setUserid(userId);
loginUser.setUsername(userName);
loginUser.setIpaddr(IpUtils.getIpAddr());
// <2> 刷新token
refreshToken(loginUser);
// <3> Jwt存储信息
Map<String, Object> claimsMap = new HashMap<String, Object>();
claimsMap.put(SecurityConstants.USER_KEY, token);
claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
// 接口返回信息
Map<String, Object> rspMap = new HashMap<String, Object>();
rspMap.put("access_token", JwtUtils.createToken(claimsMap));
rspMap.put("expires_in", expireTime);
return rspMap;
}
在 <1>
处:
把从 ruoyi-system 模块获取到的用户信息(用户、角色、权限)连同关键部分(uuid、userId、userName)重新封装
在 <2>
处:
refreshToken方法负责刷新token。何时刷新、如何刷新是一个值得讨论的问题。在本文的第三章重点讨论了这个问题。
在 <3>
处:
创建 token 的负载信息,把 uuid、userId、userName 作为 token 的负载,来创建 token。并返回给用户
public LoginUser login(String username, String password)
{
...
// IP黑名单校验【在哪里初始化黑名单的???】
String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST));
if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "很遗憾,访问IP已被列入系统黑名单");
throw new ServiceException("很遗憾,访问IP已被列入系统黑名单");
}
// 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
...
passwordService.validate(user, password);
return userInfo;
}
主要包括 2 个部分。
注意: remoteUserService.getUserInfo(username, SecurityConstants.INNER)
的 INNER 指定了该请求是“内部”请求,模块 auth 还会对 INNER 做处理。
以下代码com.ruoyi.system.api.RemoteUserService在 ruoyi-api-system
中,只是定义了相关的 api 并不是实现。RuoYi 抽象了与系统相关的 ruoyi-api-system
。在里面写公共的类或接口
@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class)
public interface RemoteUserService
{
/**
* 通过用户名查询用户信息
*
* @param username 用户名
* @param source 请求来源
* @return 结果
*/
@GetMapping("/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}
url是:/user/info/{username}。
调用服务是: ruoyi-system。
设置了降级 fallback 处理:RemoteUserFallbackFactory
备注:获取用户信息的详细分析参考:https://www.yuque.com/yuchangyuan/tkb5br/vktircc4smqw8ggs
主要是验证“可重试次数”和“密码正确性”
1、验证重试次数(默认如果错误超过5次就锁定 10分钟)
2、验证密码是否与数据库匹配。如登录成功需要清除密码错误次数。注册和验证密码用到的密码验证器要一致。
只有在将要达到过期时间,才会刷新,来续期,通常可以认为是 2/3 的时间点。
① 调用refresh方法刷新:refresh 接口
② 创建 token 时:createToken方法
③ 设置用户信息时:setLoginUser方法
④ 验证 token 时:verifyToken方法
主要是第四点。是通过 mvc 拦截器实现的
1、定时任务方案:
① 拦截用户请求,保存 key=userId,value=需要刷新的时间点到一个全局的 map 中
② 用 quartz 启动一个定时任务间隔一段时间扫描 map 来刷新
缺点:不太好控制
2、拦截器方案:
原理:是通过 mvc 的拦截器 HandlerInterceptor 实现的。
为什么不通过过滤器实现,而是拦截器?
答案:因为我们的目的不是要过滤掉请求,而是拦截请求并根据条件设置token的过期时间
public class HeaderInterceptor implements AsyncHandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
........
String token = SecurityUtils.getToken();
if (StringUtils.isNotEmpty(token))
{
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (StringUtils.isNotNull(loginUser))
{
// 验证和刷新 token 的过期时间
AuthUtil.verifyLoginUserExpire(loginUser);
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
}
return true;
}
}
如上面的代码,RuoYi 采用的也是拦截器方案,在HeaderInterceptor拦截器中,针对每个请求验证和刷新 token 的过期时间。详情参考:https://www.yuque.com/yuchangyuan/tkb5br/d387fbc5d3f5522fa013b5e087a0dad9