RuoYi Login Code Analysis
RuoYi Login Code Analysis
16 views
Jan 17, 2025
基于Kotlin版本RuoYi实现 登录接口适用于多种登录方式、适用于第三方登录授权,登录生成Token 技术点:
- 使用实现对象之间转换
- 使用实现密码加密、Token生成
- 图形验证码
- 使用一个接口多个实现的方式,适配多种登录方式
#Controller
com/blank/web/controller/AuthController.kt:45
@PostMapping("/login")
fun login(@Validated @RequestBody body: String): R<LoginVo> {
// 此处不做具体转换,主要获取clientId用于查询具体登录实现
val loginBody: LoginBody? = JsonUtils.parseObject(body, LoginBody::class.java)
validate(loginBody)
// 授权类型和客户端id
val clientId = loginBody!!.clientId
val grantType = loginBody.grantType
val client = clientService.queryByClientId(clientId!!)
// 登录具体实现、返回登录之后的系统用户
val loginVo = IAuthStrategy.login(body, client, grantType)
return ok(data = loginVo)
}
#Interface
com.blank.web.service.IAuthStrategy
interface IAuthStrategy {
/**
* 登录
*/
fun login(body: String?, client: SysClient?): LoginVo
companion object {
/**
* 登录
*/
fun login(body: String?, client: SysClient?, grantType: String?): LoginVo {
// 根据授权类型拼接出Bean名称,获取具体的实现类
val clientId: String? = client?.clientId
val beanName = grantType + BASE_NAME
if (!containsBean(beanName)) {
throw ServiceException("授权类型不正确!")
}
val instance = SpringUtil.getBean<IAuthStrategy>(beanName)
return instance.login(body, client)
}
const val BASE_NAME = "AuthStrategy"
}
}
切换不同实现类
val instance = SpringUtil.getBean<IAuthStrategy>(beanName)
instance.login(body, client)
#Impl
RuoYi默认有以下实现
- 邮箱 + 邮箱验证码
- 用户名 + 密码 + 图形验证码
- 第三方登录
- 小程序登录
##邮箱 + 邮箱验证码
override fun login(body: String?, client: SysClient?): LoginVo {
// 登录JSON转为当前登录方式需求对象
val loginBody: EmailLoginBody? = JsonUtils.parseObject(body, EmailLoginBody::class.java)
// 空值校验
validate(loginBody)
val email = loginBody!!.email!!
val emailCode = loginBody.emailCode!!
// 通过邮箱查找用户
val user = loadUserByEmail(email)
// 登录失败重试次数超过设定值,锁定账户一段时间(基于Redis实现com.blank.web.service.SysLoginService#checkLogin)
loginService.checkLogin(LoginType.EMAIL, user!!.userName!!) {
// 校验邮箱 + 验证码是否匹配,否则抛出异常(使用Redis保存验证码)
!validateEmailCode(email, emailCode)
}
// 构建登录用户
val loginUser = loginService.buildLoginUser(user)
loginUser.clientKey = client?.clientKey
loginUser.deviceType = client?.deviceType
// 用户Model,用于生成Token
val model = SaLoginModel()
model.setDevice(client?.deviceType)
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client?.timeout!!)
model.setActiveTimeout(client.activeTimeout!!)
model.setExtra(LoginHelper.CLIENT_KEY, client.clientId)
// 生成Token
login(loginUser, model)
// 记录登录日志
loginService.recordLogininfor(user.userName!!, Constants.LOGIN_SUCCESS, message("user.login.success"))
loginService.recordLoginInfo(user.userId!!)
val loginVo = LoginVo()
loginVo.accessToken = StpUtil.getTokenValue()
loginVo.expireIn = StpUtil.getTokenTimeout()
loginVo.clientId = client.clientId
return loginVo
}
Sa-Token生成用户Token
生成后Token保存在Session中,使用StpUtil.getTokenValue()获取Token
fun login(loginUser: LoginUser, model: SaLoginModel) {
var model = model
val storage = SaHolder.getStorage()
storage[LOGIN_USER_KEY] = loginUser
storage[USER_KEY] = loginUser.userId
storage[DEPT_KEY] = loginUser.deptId
model = ObjectUtil.defaultIfNull(model, SaLoginModel())
StpUtil.login(
loginUser.getLoginId(),
model.setExtra(USER_KEY, loginUser.userId)
.setExtra(DEPT_KEY, loginUser.deptId)
)
StpUtil.getSession()[LOGIN_USER_KEY] = loginUser
}
获取Redis中验证码对比
/**
* 校验邮箱验证码
*/
private fun validateEmailCode(email: String, emailCode: String): Boolean {
val code = getCacheObject<String>(GlobalConstants.CAPTCHA_CODE_KEY + email)
if (StrUtil.isBlank(code)) {
loginService.recordLogininfor(email, Constants.LOGIN_FAIL, message("user.jcaptcha.expire"))
throw CaptchaExpireException()
}
return code == emailCode
}
##用户名 + 密码 + 图形验证码
com.blank.web.service.impl.PasswordAuthStrategy
相似步骤不再说明,除了验证码其余步骤同邮箱登录类似
override fun login(body: String?, client: SysClient?): LoginVo {
// ...
val captchaEnabled = captchaProperties.enable!!
// 验证码开关
if (captchaEnabled) {
validateCaptcha(username, code, uuid)
}
// ...
return loginVo
在application中配置验证码开关
captcha:
enable: true
# 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证
type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
category: CIRCLE
# 数字验证码位数
numberLength: 1
# 字符验证码长度
charLength: 4
校验验证码
private fun validateCaptcha(username: String, code: String, uuid: String) {
// 从Redis中获取生成的验证码,Key为验证码UUID
val verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "")
val captcha = getCacheObject<String>(verifyKey)
// 删除验证码
deleteObject(verifyKey)
if (captcha == null) {
loginService.recordLogininfor(username, Constants.LOGIN_FAIL, message("user.jcaptcha.expire"))
throw CaptchaExpireException()
}
if (!code.equals(captcha, ignoreCase = true)) {
loginService.recordLogininfor(username, Constants.LOGIN_FAIL, message("user.jcaptcha.error"))
throw CaptchaException()
}
}
##第三方登录与小程序登录
步骤类似,系统主要记录第三方账号和系统账号之间关系,第三方账号返回登录成功之后,根据对应关系查询系统用户返回
override fun login(body: String?, client: SysClient?): LoginVo {
// ...
val social = sysSocialService.selectByAuthId(authUserData.source + authUserData.uuid)
if (!ObjectUtil.isNotNull(social)) {
throw ServiceException("你还没有绑定第三方账号,绑定后才可以登录!")
}
// ...
}
#其他
##生成验证码
com.blank.web.controller.CaptchaController#getCode
@GetMapping("/auth/code")
fun getCode(): R<CaptchaVo> {
val captchaVo = CaptchaVo()
val captchaEnabled = captchaProperties.enable!!
if (!captchaEnabled) {
captchaVo.captchaEnabled = false
return ok(data = captchaVo)
}
// 保存验证码信息
val uuid = IdUtil.simpleUUID()
val verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid
// 生成验证码
val captchaType = captchaProperties.type
val isMath = CaptchaType.MATH === captchaType
val length = if (isMath) captchaProperties.numberLength else captchaProperties.charLength
val codeGenerator: CodeGenerator = ReflectUtil.newInstance(
captchaType!!.clazz, length
)
val captcha = SpringUtil.getBean(captchaProperties.category!!.clazz)
captcha.generator = codeGenerator
captcha.createCode()
var code = captcha.code
if (isMath) {
val parser: ExpressionParser = SpelExpressionParser()
val exp = parser.parseExpression(StringUtils.remove(code, "="))
code = exp.getValue(String::class.java)
}
setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION.toLong()))
captchaVo.uuid = uuid
captchaVo.img = captcha.imageBase64
return ok(data = captchaVo)
}