上面这张图是若依框架的登陆界面,我们来看看当点击登录时,前端是怎么发送请求给后端,后端又是怎么处理请求并保证用户成功登入系统的。
一、前端发送请求
我们看见登录的时候,发送了一个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
看看接口
我们来看看用户服务模块的代码
那么就顺理成章地找到了对应的方法,实现了远程调用
这句代码有两个参数👇
username
和SecurityConstants.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
去查询登录用户信息就能查到了
完结撒花!🌸🌸