前言
本文主要内容:
- RuoYi 框架的登录逻辑分析
- 自定义登录逻辑的基本思路
- 几个例子,如微信小程序登录
RuoYi 框架的登录逻辑分析
登录接口
-
登录入口,
SysLoginController
的login
方法,调用loginService.login
最终返回一个 Jwt token(登录完成) -
loginService.login
-
验证码校验
-
登录前置校验, 如输入校验、IP黑名单等
-
生成一个
UsernamePasswordAuthenticationToken
,并放到ThreadLocal
,然后作为参数调用 spring security 的authenticationManager.authenticate
该方法最终会调用UserDetailsServiceImpl.loadUserByUsername
-
从
authenticationManager.authenticate
的返回值中获取LoginUser
,并作为参数调用tokenService.createToken
来获得 Jwt token 并记录LoginUser
信息到 redis(登录完成)
-
-
UserDetailsServiceImpl.loadUserByUsername
-
userService.selectUserByUserName(username)
从系统用户表获取用户SysUser
对象 -
用户状态检查(是否存在、是否禁用等)
-
passwordService.validate(user);
检查是否短时间内登录失败次数(用到了ThreadLocal
来获得用户名密码)、调用PasswordEncoder
(默认是BCryptPasswordEncoder
)的matches
方法检验密码是否正确 -
将
SysUser
对象转换为LoginUser
对象返回给authenticationManager.authenticate
,LoginUser
类是 spring securityUserDetails
的实现
-
登录态验证
- 在
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
中,有一行代码是.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
,这个过滤器是JwtAuthenticationTokenFilter
- 过滤器的
doFilterInternal
方法首先 通过tokenService.getLoginUser
获取了LoginUser
,通过请求中的 Jwt token,还原出 token 的内容,是个 UUID,再通过 UUID 从 redis 中查找对应的LoginUser
- 检查
LoginUser
token 有效期,并在需要时刷新 token 有效期 - 生成
UsernamePasswordAuthenticationToken
,并设置到SecurityContextHolder
的认证信息中;如果这里不能从 token正常得到认证信息,那么就会没有认证信息,后续 security 流程就会需要重新登录。
自定义登录逻辑的基本思路
了解了登录流程之后,修改登录逻辑的切入点就很多了,这里列举几种思路。
微信小程序开放登录场景(调用第三方 API场景)
- 开放一个新的
wxLogin
接口 - 根据前端 wx.login() 获取的凭证 code,调用 auth.code2Session 接口换取唯一标识 openid
- 查找
sys_user
表中 username 为 openid 的记录,如果没有,存储到sys_user
表中 - 如果系统里需要有除了
sys_user
字段之外的用户信息要存储,建议新增一个用户表存储,不修改sys_user
表,便于与 RuoYi 保持同步,新的用户表的用户 id(或主键 id) 与sys_user
表的user_id
一致,这样业务上的用户 id 与系统登录态的用户 id 是一致的 - 将
SysUser
对象转换为LoginUser
对象 - 创建一个
UsernamePasswordAuthenticationToken
对象,并调用setDetails(loginUser)
- 设置到
SecurityContextHolder
的认证信息中 - 调用
tokenService.createToken(loginUser)
并返回,在此处登录信息已被记录到 redis - 后续 请求会由
JwtAuthenticationTokenFilter
来还原登录信息
登录界面不变,通过外部 API 验证登录,并同步外部用户信息
比如要做一个医院内的系统,医生信息存在于 HIS,如果本系统需要医生登录,需要通过 HIS 的登录接口进行验证,验证通过后,在本系统记录登录态,本系统知道当前用户是哪个医生。
-
复用现有的登录界面,用户输入用户名密码后,请求到
/login
接口 -
改造
UserDetailsServiceImpl.loadUserByUsername
方法,不再直接从数据库查询用户信息,而是新增一层-
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //这里进行修改,对接医院 HIS 获取用户,如果记录不存在,保存到用户表 if(") Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); String uid = usernamePasswordAuthenticationToken.getName(); String pwd = usernamePasswordAuthenticationToken.getCredentials().toString(); hisUserService.login(uid, pwd); //原始内容保留 SysUser user = userService.selectUserByUserName(username); //省略... //这里进行修改,注释掉密码验证 //passwordService.validate(user);
-
-
在
ruoyi-system
模块下新增一个IHisLoginService
及其实现类HisLoginServiceImpl
(没有直接修改SysLoginServiceImpl
方便同步Ruoyi
的更新),在实现类中,通过用户输入的用户名和密码,去调用 HIS 的登录接口- 如果登录失败,直接抛出异常,就像
UserDetailsServiceImpl.loadUserByUsername
那样 - 如果登录成功,向
sys_user
表同步 HIS 接口返回的信息,如医生名、科室信息等
- 如果登录失败,直接抛出异常,就像
注意: spring security 还会调用 org.springframework.security.authentication.dao.DaoAuthenticationProvider
的 additionalAuthenticationChecks
方法,这个方法会再次进行密码比对 if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
,所以可以在存储用户信息时,使用 SecurityUtils.encryptPassword
对密码加密后存储到 password
字段
下面是一个例子
@Service
public class HisUserServiceImpl implements IHisUserService {
Logger logger = LoggerFactory.getLogger(HisUserServiceImpl.class);
@Autowired
ISysUserService sysUserService;
@Override
public SysUser login(String uid, String pwd) {
RestTemplate restTemplate = new RestTemplate();
try {
String resp = restTemplate.getForObject(String.format("http://localhost:8080/login?uid=%s&pwd=%s", uid, pwd), String.class);
logger.info("收到HIS登录接口响应{}", resp);
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> map = objectMapper.readValue(resp, Map.class);
if(ObjectUtils.nullSafeEquals(map.get("Result"), true)){
SysUser user = sysUserService.selectUserByUserName(uid);
if(user != null){
user.setNickName(map.getOrDefault("CZYM", "医生").toString());
user.setPassword(SecurityUtils.encryptPassword(pwd));
sysUserService.updateUser(user);
}else{
user = new SysUser();
user.setUserName(uid);
user.setNickName(map.getOrDefault("CZYM", "医生").toString());
user.setPassword(SecurityUtils.encryptPassword(pwd));
sysUserService.insertUser(user);
}
} else if(ObjectUtils.nullSafeEquals(map.get("Result"), false)){
throw new ServiceException(map.getOrDefault("Message", "").toString());
} else {
throw new Exception("登录接口数据异常");
}
} catch (ServiceException e1){
throw new ServiceException("登录失败:" + e1.getMessage());
} catch (Exception e){
logger.error("HIS 登录接口异常:{}", e.getMessage());
throw new ServiceException("HIS 登录接口异常,请重试");
}
return null;
}
}