RuoYi Login Code Analysis
RuoYi Login Code Analysis
16 views
Jan 17, 2025

基于Kotlin版本RuoYi实现 登录接口适用于多种登录方式、适用于第三方登录授权,登录生成Token 技术点:

  1. 使用实现对象之间转换
  2. 使用实现密码加密、Token生成
  3. 图形验证码
  4. 使用一个接口多个实现的方式,适配多种登录方式

#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默认有以下实现

  • 邮箱 + 邮箱验证码
  • 用户名 + 密码 + 图形验证码
  • 第三方登录
  • 小程序登录

image.png

##邮箱 + 邮箱验证码

    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)
}

  
Total PV : 23760 UV : 5008
Copyright © 2024 陕ICP备2021015553号-2