模式切换
RuoYi-Vue-Plus 项目中登录功能实现梳理
本文主要讲解 RuoYi-Vue-Plus 项目中登录功能的实现逻辑。
接收请求
首先在 AuthController 中的 login 方法中接收包含登录信息的请求体。
java
public class AuthController {
@PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) {
// 方法实现
}
}解析请求
使用 JsonUtils.parseObject 方法将请求体解析为 LoginBody 对象,并使用 ValidatorUtils.validate 方法进行验证。
java
// 解析请求
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
// 验证
ValidatorUtils.validate(loginBody);在 validate 方法中,使用了 Jakarta Bean Validation API 验证对象。大致逻辑描述如下:
- 检索验证器实例:使用一个静态的验证器实例,即
SpringUtils.getBean(Validator.class)从 Spring 应用程序上下文中检索。 - 校验对象:接收一个对象和一个可选的验证组数组作为参数。调用 Validator 实例上的 validate 方法,传递对象和验证组。此方法返回一组 ConstraintViolation 对象,这些对象表示验证错误。
- 检查验证错误:检查
ConstraintViolation对象集是否为空,表明存在验证错误。 - 抛出异常:如果存在验证错误,该方法抛出一个
ConstraintViolationException,传递一条消息和一组验证错误。
java
/**
* Validator 校验框架工具
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidatorUtils {
private static final Validator VALID = SpringUtils.getBean(Validator.class);
/**
* 对给定对象进行参数校验,并根据指定的校验组进行校验
*
* @param object 要进行校验的对象
* @param groups 校验组
* @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
*/
public static <T> void validate(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
if (!validate.isEmpty()) {
throw new ConstraintViolationException("参数校验异常", validate);
}
}
}java
/**
* 用户登录对象
*
* @author Lion Li
*/
@Data
public class LoginBody implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端id
*/
@NotBlank(message = "{auth.clientid.not.blank}")
private String clientId;
/**
* 授权类型
*/
@NotBlank(message = "{auth.grant.type.not.blank}")
private String grantType;
/**
* 租户ID
*/
private String tenantId;
/**
* 验证码
*/
private String code;
/**
* 唯一标识
*/
private String uuid;
}验证客户端信息
根据 clientId 查询客户端信息,并验证客户端是否存在、授权类型是否匹配以及客户端状态是否正常。
java
// 授权类型和客户端id
String clientId = loginBody.getClientId();
String grantType = loginBody.getGrantType();
SysClientVo client = clientService.queryByClientId(clientId);
// 查询不到 client 或 client 内不包含 grantType
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
return R.fail(MessageUtils.message("auth.grant.type.error"));
} else if (!UserConstants.NORMAL.equals(client.getStatus())) {
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
}校验租户
调用 loginService.checkTenant 方法校验租户信息。
java
// 校验租户
loginService.checkTenant(loginBody.getTenantId());调用登录策略
根据授权类型调用相应的登录策略(如密码登录策略 PasswordAuthStrategy),并执行登录逻辑。
java
// 登录
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);在登录策略的设计模式中,根据不同的授权类型(如密码登录、短信登录等)执行不同的登录逻辑。在项目中,登录策略通过接口 IAuthStrategy 和其实现类(如 PasswordAuthStrategy 和 SmsAuthStrategy)来实现。
首先在 IAuthStrategy 接口中包含一个静态方法 login,根据授权类型动态获取相应的策略实现。接着定义了登录方法 login,让实现类提供具体的登录逻辑。
java
/**
* 授权策略
*
* @author Michelle.Chung
*/
public interface IAuthStrategy {
String BASE_NAME = "AuthStrategy";
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @param grantType 授权类型
* @return 登录验证信息
*/
static LoginVo login(String body, SysClientVo client, String grantType) {
// 授权类型和客户端id
String beanName = grantType + BASE_NAME;
if (!SpringUtils.containsBean(beanName)) {
throw new ServiceException("授权类型不正确!");
}
IAuthStrategy instance = SpringUtils.getBean(beanName);
return instance.login(body, client);
}
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @return 登录验证信息
*/
LoginVo login(String body, SysClientVo client);
}然后是具体的登录实现类,通过 Spring 的依赖注入和动态获取 Bean 的方式,可以灵活地扩展和管理不同的登录策略。以密码登录实现类 PasswordAuthStrategy 为例。
- 在该类中,实现了
IAuthStrategy接口,提供密码登录的具体实现。 - 使用
@Service("password" + IAuthStrategy.BASE_NAME)注解,将该类注册为 Spring 容器中的一个服务,并指定其Bean名称为passwordAuthStrategy。
java
/**
* 密码认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("password" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class PasswordAuthStrategy implements IAuthStrategy {
@Override
public LoginVo login(String body, SysClientVo client) {
// 具体的实现逻辑
}
}生成登录用户信息
在 PasswordAuthStrategy 类的 login 方法中,解析登录请求体,校验验证码(如果启用)。
java
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String username = loginBody.getUsername();
String password = loginBody.getPassword();
String code = loginBody.getCode();
String uuid = loginBody.getUuid();
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid);
}加载用户信息,校验用户密码,构建 LoginUser 对象。
java
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());生成 Token
使用 LoginHelper.login 方法生成 Token,并设置相关的登录模型参数(如设备类型、超时时间等)。
java
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);返回结果
将生成的 Token 和其他登录信息封装到 LoginVo 对象中,并返回给客户端。
java
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;