RouYi-Cloud登录认证JWT浅析

在这里插入图片描述
上面这张图是若依框架的登陆界面,我们来看看当点击登录时,前端是怎么发送请求给后端,后端又是怎么处理请求并保证用户成功登入系统的。

一、前端发送请求

在这里插入图片描述
我们看见登录的时候,发送了一个http://localhost/dev-api/auth/login请求
可是在前端,提示的是访问这个地址
在这里插入图片描述
那么问题来了,80这个端口去哪了?

二、请求地址分析

我们看前端,这里代理了一个8080端口的地址,目的是用于后端网关接收请求,因为网关地址的端口也是8080
在这里插入图片描述
在这里插入图片描述

然后重写路径,用process.env.VUE_APP_BASE_API这个变量的值替代了,也就是这个8080端口的地址被映射到了/dev-api,如下图

在这里插入图片描述
接下来/auth又是什么?
请求到了网关之后,网关自定义了一个路由,而这个路由能决定请求到底走哪个微服务。
在这里插入图片描述
很明显,网关把/auth这个路由映射到了ruoyi-auth这个微服务,这就决定了我们走的是登录认证这个服务模块。
知道了微服务的名字,我们看后端对应的模块
在这里插入图片描述
发现有/login的请求,于是我们就找到了正确的请求地址

三、代码解析

我们发现前端发送了这四个数据
在这里插入图片描述
而在后端用于接收数据的对象LoginBody中,似乎并没有code和uuid这两个属性
在这里插入图片描述
其实这是因为在请求到达网关的时候,可能经过了一些过滤器,而这些数据正好就用于过滤了,这里不必细究。
接下来我们来看看这段代码中的内部实现过程

	@PostMapping("login")
    public R<?> login(@RequestBody LoginBody form)
    {
        // 用户登录
        LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        // 获取登录token
        return R.ok(tokenService.createToken(userInfo));
    }

对于这句:sysLoginService.login(form.getUsername(), form.getPassword());我们看一下service层,具体实现代码如下:

	/**
     * 登录
     */
    public LoginUser login(String username, String password)
    {
        // 用户名或密码为空 错误
        if (StringUtils.isAnyBlank(username, password))
        {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
            throw new ServiceException("用户/密码必须填写");
        }
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
        {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
            throw new ServiceException("用户密码不在指定范围");
        }
        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
        {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
            throw new ServiceException("用户名不在指定范围");
        }
        // 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);

        if (R.FAIL == userResult.getCode())
        {
            throw new ServiceException(userResult.getMsg());
        }

        LoginUser userInfo = userResult.getData();
        SysUser user = userResult.getData().getSysUser();
        if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        }
        if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }
        passwordService.validate(user, password);
        recordLogService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
        recordLoginInfo(user.getUserId());
        return userInfo;
    }

这些普通的判断这里就不再赘述,相信大家都能看懂
在这里插入图片描述
对于这句代码

		// 查询用户信息
        R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);

这里远程调用了一个用户服务模块(远程调用不了解的可以看看博主写的这篇文章,理了一下大概的思路:微服务项目集成Feign实现远程调用),我们ctrl+点击getUserInfo看看接口
在这里插入图片描述
我们来看看用户服务模块的代码
在这里插入图片描述
那么就顺理成章地找到了对应的方法,实现了远程调用

这句代码有两个参数👇
在这里插入图片描述

usernameSecurityConstants.INNER,这个SecurityConstants.INNER是什么意思?
其实这就是一个字符串,是为了确保是两个服务之间在调用,防止外部干涉,这样就避免了重复登录(内部服务之间调用不用登录)
在这里插入图片描述
查询完之后返回了一个userResult,类型是R,这是若依封装的一个类,可以往里面传入泛型,这里的泛型就是LoginUser
然后判断是否查询失败
在这里插入图片描述
FAIL值是500
在这里插入图片描述
userResult.getCode()获取查询是否成功状态标志
这里注意,userResult并不是LoginUser类型,而是对应的R实体类
在这里插入图片描述
在这里插入图片描述
所以才有了下面这个方法获取用户信息
在这里插入图片描述
由此获取到表对象
在这里插入图片描述

接下来是普通的判断,不再赘述
在这里插入图片描述
以上已经成功判断出该用户是否存在于数据库,接下来就是判断密码是否正确
在这里插入图片描述
在这里插入图片描述
核心方法是matches(user, password)
在这里插入图片描述
其中user是数据库用户信息对象,rawPassword是用户登录输入的密码,这里使用md5算法和数据库存储的密码进行比对
在这里插入图片描述
比对成功后,返回给controller
在这里插入图片描述
再下来就是生成token
在这里插入图片描述
这里有一个refreshToken(loginUser)方法,实际上解决的就是在前端用户登陆的时候,如果清除了浏览器缓存,那么自然token也会被清除,这时候登陆的话会提示重新登陆,登陆完成之后更新了redis中存储的token有效期
在这里插入图片描述
存入token、userId、userName
在这里插入图片描述
然后通过JWTUtils工具类生成令牌
在这里插入图片描述
JWT生成逻辑

在这里插入图片描述

  • Jwts.builder():用于创建一个JWT构建器(JwtBuilder)。
  • setClaims(claims):将传入的claims(即JWT存入的信息claimsMap,包含了token、userId、userName)设置到JWT的payload部分。
  • JWT通常包含三部分:header(头部)、payload(负载)和signature(签名),claims就是payload的内容。
  • signWith(SignatureAlgorithm.HS512, secret)
    • SignatureAlgorithm.HS512:指定使用HS512算法对JWT进行签名。HS512是一种基于HMAC和SHA-512的签名算法,安全性较高。
    • secret:这是用于签名的密钥。在实际代码中,secret通常是一个字符串,一般是配置文件中定义的密钥。签名的作用是确保JWT在传输过程中未被篡改。
  • compact():将JWT的三部分(header、payload和signature)拼接成一个紧凑的字符串,格式为header.payload.signature,中间用点(.)分隔。这个字符串就是最终的JWT。

JWT生成之后存入rspMap里面,同时存入一个过期时间TOKEN_EXPIRE_TIME
在这里插入图片描述
于是将JWT和过期时间(即rspMap)同时返回到controller,controller也返回给前端,存入到cookie
在这里插入图片描述
我们从前端可以看到cookie,里面存入了JWT串和过期时间
在这里插入图片描述
说到这里,是不是觉得token、JWT、cookie三者到底是什么关系?怎么一会这个一会那个,别着急,接下来我们梳理一下三者之间的关系且相互之间是如何存储或者转化的。

四、token、JWT、cookie之间的关系

Token(令牌) 和 JWT 的关系

  • JWT 是 Token 的一种具体实现
    • Token 是一个广义的概念,可以是任何形式的令牌。
    • JWT 是一种标准化的、经过签名的 JSON 格式 Token,具有结构化和可验证的特点。
  • JWT 是一种特殊的 Token
    • Token 经过加工后可以生成 JWT。
    • JWT 是无状态的,适合分布式系统和无状态架构。

Token 和 Cookie 的关系

  • Token 可以存储在 Cookie 中
    • Token(包括JWT)可以存储在 Cookie 中,以便在客户端和服务器之间传递。
  • Cookie 是一种存储机制
    • Cookie 本身是一种存储机制,可以存储 Token,也可以存储其他类型的数据。
    • Cookie 的主要用途是会话管理,而 Token 的主要用途是身份验证和授权。

JWT 和 Cookie 的关系

  • JWT 可以存储在 Cookie 中
    • 将 JWT 存储在 Cookie 中是一种常见的做法,尤其是在需要利用 Cookie 的自动发送机制时。
  • JWT 和 Cookie 的用途不同
    • JWT 主要用于身份验证和授权,是无状态的。
    • Cookie 主要用于会话管理,是有状态的。

五、前端发送请求携带JWT

所以在前端发送登录请求的时候,就会携带cookie里面存储的JWT,经过解析之后获取其中的user_key
在这里插入图片描述
此时redis中已经存储了user_key的信息,再根据user_key去查询登录用户信息就能查到了
在这里插入图片描述
完结撒花!🌸🌸

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值