【面试题】传统Token与JWT区别及优缺点?JWT如何实现登入功能?
传统的Token
传统的Token,例如:用户登录成功生成对应的令牌,key:令牌 ,value:userid(用户信息)
隐藏了数据真实性,同时将该token存放到redis中,返回对应的真实令牌给客户端存放。
客户端每次访问后端请求的时候,会传递该token在请求中,服务器端接收到该token之后,从redis中查询如果存在的情况下,则说明在有效期内,如果在Redis中不存在的情况下,则说明过期或者token错误。
传统的Token实现登入的流程
- 前端点击登陆,服务器验证账号密码成功
- 服务器生成令牌,本质是一个32位的uuid
- 将该令牌存到数据库或redis中,key是uuid(Token),value是userId
- 把令牌返给客户端,客户端把令牌存在cookie中。
- 下次请求的时候就把令牌放在请求头里带上
- 从redis中验证该令牌是否过期
- 获取value内容userId
- 根据userId查询用户信息,再返回客户端
传统的Token的优、缺点
优点
-
可以隐藏真实数据,适当避免明文传输
-
适用于分布式/微服务,解决Session共享问题
-
安全系数高
缺点
-
存放在redis,必须依赖服务器,暂用服务器资源
-
效率较低,但是比起Session好多了
JWT
什么是JWT?
JWT的全称是JSON WEB Token。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT解密平台
开发工具箱 - JWT 在线解密 (box3.cn)https://www.box3.cn/tools/jwt.html
JWT组成的部分
JWT分层3部分 —— Header、Payload、Signature
(1)Header(头) 作用:记录令牌类型、签名算法等 例如:{“alg":"HS256","type","JWT}
(2)Payload(有效载荷)作用:携带一些用户信息 例如{"userId":"1","username":"mayikt"}
(3)Signature(签名)作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串
Header(头)
{
Typ="jwt" ---类型为jwt
Alg:"HS256" --加密算法为hs256
}
Payload(有效载荷)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
Signature(签名)
这段是签证信息,非常非常关键的部分。
这关乎你的这个 Token 是否安全,是否能被人仿造。
一般是对 header (base64后的) 和 payload (base64后的) 这两部分的数据通过 secret(私钥)进行签名后的结果。
所以这个签名算法就非常关键了,常用的有 SHA256 和 HMAC,我个人更推荐使用 RSA。
JWT优缺点
优点
- 无需服务器端存放数据,减轻服务器端的压力
- 占用带宽比较小、跨语言
- token自身包含用户信息且无法篡改,在服务(网关)中可以自行解析校验出用户信息,对认证服务器(account-svc)压力小
缺点
- 建议不要放敏感数据 userid、手机号码(如果非要放userId,deptId等信息,可采用rsa256算法加密)RSA256生成JWT
- JWT生成之后无法修改(发生变化)
- 后端无法统计,生成JWT
- 无法吊销令牌,只能等待令牌自身过期
- 令牌长度与其包含用户信息多少正相关,传输开销较大
JWT模拟登入
JWTLoginController
@RestController
public class JWTLoginController {
@Resource
private JWTLoginIService jwtLoginIService;
/**
* jwt登录的方式
*
* @return
*/
@PostMapping("loginJwt")
public Result loginJwt(@RequestBody UserLoginDto userLoginDto) {
return jwtLoginIService.loginJwt(userLoginDto);
}
/**
* jwt 验证
*
* @return
*/
@GetMapping("jwtVerification")
public Result jwtVerification(@RequestParam("jwt") String jwt) {
return jwtLoginIService.jwtVerification(jwt);
}
}
UserLoginDto
@Data
public class UserLoginDto {
private String mobile;
private String passWord;
}
UserInfo
@Data
public class UserInfo {
private String userId;
private Integer age;
private String mobile;
private String passWord;
}
JWTLoginIService
public interface JWTLoginIService {
Result loginJwt(UserLoginDto userLoginDto);
Result jwtVerification(String jwt);
}
JWTLoginIServiceImpl
@Service
public class JWTLoginIServiceImpl implements JWTLoginIService {
@Override
public Result loginJwt(UserLoginDto userLoginDto) {
String mobile = userLoginDto.getMobile();
if (StringUtils.isEmpty(userLoginDto.getMobile())) {
return Result.fail("mobile 不能为空!");
}
String passWord = userLoginDto.getPassWord();
if (StringUtils.isEmpty(userLoginDto.getPassWord())) {
return Result.fail("passWord 不能为空!");
}
String newPassWord = MD5Util.getMD5String(passWord);
// 为了方便这里就不查数据库了,我们来模拟一下这个动作
// 反正查数据库后,也是向userLoginDto里面塞值!
UserInfo userInfo = new UserInfo();
userInfo.setUserId("1");
userInfo.setAge(18);
userInfo.setMobile(mobile);
userInfo.setPassWord(newPassWord);
if (userInfo == null) {
return Result.fail("手机号码或者密码错误");
}
String jwt = JWTUtil.generateJsonWebToken(userInfo);
JSONObject data = new JSONObject();
data.set("jwt", jwt);
return Result.ok(data);
}
@Override
public Result jwtVerification(String jwt) {
if (StringUtils.isEmpty(jwt)) {
return Result.fail("jwt is null");
}
Claims claims = JWTUtil.checkJWT(jwt);
if(claims==null){
return Result.fail("jwt error");
}
return Result.ok("登入成功!");
}
}
JWTUtil
public class JWTUtil {
private static final String SUBJECT = "Harmony";
private static final long EXPIRITION = 1000 * 24 * 60 * 7;
private static final String APPSECRET_KEY = "Harmony_secret";
public static String generateJsonWebToken(UserInfo userInfo) {
String token = Jwts.builder()
.setSubject(SUBJECT)
.claim("userId", userInfo.getUserId())
.claim("age", userInfo.getAge())
.claim("mobile", DesensitizedUtil.mobilePhone(userInfo.getMobile()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
我们模拟JWT方式登入,生成一个JWT(Token),如果是Web开发,我们会把这个值交给Cookie或者loaclStorage,之后浏览器向服务器“发消息”都会携带这个值!
我们可以拿着这个JWT,这个验证逻辑里面发一次请求(在Web开发中,这一块逻辑一般是放在拦截器中!),模拟浏览器携带者“用户ID”,所以这里服务器给出的响应自然是通过的!