在外面实现登入接口的时候,传统的方法是在用户登入界面,把用户名的账号密码提交给服务器,然后通过服务器端验证账号密码是否正确.
如果密码正确,则在服务器端创建session,通过cookie将sessionId返回给浏览器.
现在就出现了一个新问题,在集群环境下无法直接使用session.
在我们现在开发的项目中,很少会有企业将项目部署在一个服务器上,这样容易发生单点故障(一个服务器挂了,整个应用访问不了).
所以在通常情况下,一个web服务会部署在多个服务器上,通过Nginx等进行负载均衡.此时来自一个用户的请求就可能被分发到不同的服务器上.
而使用session只能存在一个服务器上,就好比用户登入的时候访问服务器A,此时成功登入,在服务器A上记录了用户的session,然后下一个请求被分配到了服务器B,但是服务器B没有记录用户的session,于是就会跳转到登入页面.这就出现了问题.
为了解决这种问题,我们来介绍一个新的技术:令牌技术.
令牌技术,听起来高大上,本质上就是一个字符串,用来作为用户的身份标识,类似于身份证号.
要用到令牌技术,服务器需要具备生成令牌 和验证令牌的能力.
用户登入之后,通过负载均衡,把请求转给了服务器A,此时服务器A进行账号密码验证,验证成功后,生成一个令牌并返回给客户端,
客户端收到令牌之后,把令牌存储起来,比如可以存储在cookie中.
用户登录成功之后,携带令牌进行其他操作,比如查询列表.此时请求转发到了服务器B,服务器B会先进行权限验证操作,验证令牌是否有效,如果有效,则说明用户已经正常登录,如果无效,则说明用户未进行登入.
令牌的优缺点:
解决了集群环境下的认证问题.
减轻了服务器的存储压力(无需在服务器中存储session)
但是需要自己实现令牌的生成传递和校验.
一.JWT组成
JWT全称:JSON Web Token
本质上是一个token,是一种紧凑的URL安全方法(URL安全:JWT 字符串可以直接放在 URL 地址栏传递,不会出现解析异常).
JWT由三部分组成,每部分中间使用(.)来分割,比如aaaa.bbbb.cccc
Header (头部) 头部包括令牌的类型(即 JWT)及使用的哈希算法(如 HMAC SHA256 或 RSA)
Payload (负载) 负载部分是存放有效信息的地方,里面是一些自定义内容。比如: {"userId":"123","userName":"zhangsan"},也可以存在 jwt 提供的现场字段,比如 exp (过期时间戳) 等.
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容.
Signature (签名) 此部分用于防止 jwt 内容被篡改,确保安全性.
防止被篡改,而不是防止被解析. JWT 之所以安全,就是因为最后的签名.jwt 当中任何一个字符被篡改,整个令牌都会校验失败. 就好比我们的身份证,之所以能标识一个人的身份,是因为他不能被篡改,而不是因为内容加密.(任何人都可以看到身份证的信息,jwt 也是)
我们可以在jwt.io上看到相关介绍.
可以看到在jwt官网上看到举的例子.

二.JWT的令牌生成和校验.
1.引入JWT令牌的依赖.
二.生成令牌
我们可以先简单构建一个令牌.
构建一个map将用户ID,name传入,然后通过setClaims()方法放入载荷,然后通过compact将信息打包生成令牌.

运行代码,获得到jwt令牌

将这个字符串解析.可以看到旁边就有信息了,我们这边只设置了载荷信息,所以没有签名和令牌类型以及使用的算法类型.

此时我们加入别的信息,比如加入签名
怎么设置签名呢?

需要用到KEY,KEY需要一个长字符串通过base64编码获得.
我们可以手动创建KEY,也可以自动生成.


然后就可以生成一个令牌.

三.校验令牌
使用刚刚生成的令牌当做token,然后使用jwt的parserBuilder方法构建解析器.然后输入密钥,如果使用的是对称加密,就使用生成令牌的密钥,如果是非对称加密,则需要另一个密钥.
然后创建解析器.创建之后我们还可以拿到里面的各种信息.

执行getBody()方法,如果没有报错,则说明令牌合法,如果报错则说明令牌非法.可以用来校验令牌.
四.用户登入
在之前我们使用session的方法判断用户是否登入.现在我们使用JWT令牌的方式来判断用户是否登入.
java
package org.example.blog.common.utils;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static final Integer DELAY = 1000 * 60 * 60 * 24;//一天有效期
private static final String SECRET_KEY = "5sjsdiijoqfueijq2349102jjdsodmcsfasfds777397A2443";//密钥
private static final Key key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
public static String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setExpiration(new Date(System.currentTimeMillis() + DELAY))
.setClaims(claims)
.signWith(key)
.compact();
}
public static Boolean check(String token){
if(!StringUtils.hasLength(token)){
return false;
}
JwtParser jwt = Jwts.parserBuilder()//构建解析器
.setSigningKey(key)//设置密钥,需要和生成令牌的密钥一致,使用HS256对称加密
.build();//创建解析器
try{
String body = jwt.parseClaimsJws(token).getSignature();
return true;
}catch(Exception e){
return false;
}
}
}
先创建一个JwtUtils工具类负责生成令牌和检验令牌.
然后在判断用户登入的时候,当用户成功登入,就生成令牌返回给前端,然后存在客户端中,后续请求通过看请求中是否携带正确令牌来判断用户是否登入.
java
public UserLoginResponse SelectUser(UserInfoRequest userInfoRequest) {
UserInfo userInfo = userInfoMapper.selectOne(new QueryWrapper<UserInfo>().lambda()
.eq(UserInfo::getUserName, userInfoRequest.getUsername())
.eq(UserInfo::getDeleteFlag, Constant.NOT_DELETE)
);
if(userInfo == null){
throw new BlogException("用户不存在");
}
if(!userInfo.getPassword().equals(userInfoRequest.getPassword())){
throw new BlogException("密码错误");
}
//生成令牌
Map<String, Object> map = new HashMap<>();
map.put("userId", userInfo.getId());
map.put("userName", userInfo.getUserName());
String token = JwtUtils.generateToken(map);
//后续需要将令牌存储在客户端.后续请求时需要从令牌中获取用户信息
return new UserLoginResponse(userInfo.getId(), token);
}
修改登入的前端代码,在登入成功之后新增在客户端存储用户ID以及token的代码:

这样就可以在后面的请求中携带令牌,并且通过令牌判断登入状态.