1、什么是 JWT
Github:https://github.com/jwt
在介绍JWT之前,我们先来回顾一下利用token
进行用户身份验证的流程:
-
客户端使用用户名和密码请求登录
-
服务端收到请求,验证用户名和密码
-
验证成功后,服务端会签发一个token,再把这个token返回给客户端
-
客户端收到token后可以把它存储起来,比如放到cookie中
-
客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带
-
服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据
这种基于token
的认证方式相比传统的session
认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:
-
支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
-
无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
-
更适用CDN:可以通过内容分发网络请求服务端的所有资料
-
更适用于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多
-
无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御
而JWT
就是上述流程当中token
的一种具体实现方式,其全称是JSON Web Token
Json web token (WT),是为了在网络应用环境间传递声明而执行的种基 FISON的开放标准(RFC7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO) 场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token 也可直接被用于认证,也可被加密。
一句话: JWT用于分布式系统的单点登录SSO场景, 主要用来做用户身份鉴别或者资源 (接口)安全性的一 种技术或者一种机制。
-
身份鉴别
-
资源接口安全性校验,保护服务器资源不被泄露
![](https://i-blog.csdnimg.cn/blog_migrate/cf8264520f2151326ed42be80f099afc.png)
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token
,并且这个JWT token
带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:
-
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
-
后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
-
后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
-
前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
-
后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
-
验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
2、服务器的接口安全性问题
-
token生成和获取的阶段:一般来说都是在登录的时候,就生成token
-
然后在未来的每一次请求需要进行安全校验的情况下需要写到token到服务器进行对比和比较。
-
传统的做法一般可以使用 全局Map 或者 session 来完成,但是这个会大量消耗服务器资源。
-
现在比较主流的最佳的使用解决方案是:JWT,因为他是无状态并且不会去消耗太多的服务器资源的一种解决方案!
接口请求地址:http://localhost:8080/api/product/list?token=xxxxx
3、起源
说起 JWT ,我们应该来谈一谈基于token认证与session认证的区别。
1、传统的session
认证有如下的问题:
-
每个用户的登录信息都会保存到服务器的session中,随着用户的增多,服务器开销会明显增大
-
由于session是存在与服务器的物理内存中,所以在分布式系统中,这种方式将会失效。虽然可以将session统一保存到Redis中,但是这样做无疑增加了系统的复杂性,对于不需要redis的应用也会白白多引入一个缓存中间件
-
对于非浏览器的客户端、手机移动端等不适用,因为session依赖于cookie,而移动端经常没有cookie
-
因为session认证本质基于cookie,所以如果cookie被截获,用户很容易收到跨站请求伪造攻击。并且如果浏览器禁用了cookie,这种方式也会失效
-
前后端分离系统中更加不适用,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,cookie中关于session的信息会转发多次
-
由于基于Cookie,而cookie无法跨域,所以session的认证也无法跨域,对单点登录不适用
2、JWT的优势
-
简洁:JWT Token数据量小,传输速度也很快
-
因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
-
不需要在服务端保存会话信息,也就是说不依赖于cookie和session,所以没有了传统session认证的弊端,特别适用于分布式微服务
-
单点登录友好:使用Session进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话, token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题
-
适合移动端应用:使用Session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到Cookie(需要 Cookie 保存 SessionId),所以不适合移动端
4、传统的session认证
我们知道,http协议本身是一 种 无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下-次请求时, 用户还要再一-次进行用户认证 才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户 登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出米。
代码如下:
package com.zhao.springboot.jwt.controller;
import com.zhao.springboot.jwt.controller.entity.User;
import com.zhao.springboot.jwt.utils.MD5Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import cn.hutool.core.exceptions.ValidateException;
@RestController
public class LoginController {
@Autowired
UserService userService;
@PostMapping("/session/login")
public String sessionLogin(String username, String password, HttpSession session) throws ValidationException {
// 1、检验username 和 password 是否为空
// 2、根据用户查询用户是否存在
User user = userService.getUserByName(username);
if (user == null){
throw new ValidateException(403,"用户名或密码有误!!");
}
// 3、对密码记性加密加盐处理
password = MD5Utils.md5(password);
// 如果用户输入的密码与数据库查询的密码不一致
if (!password.equalsIgnoreCase(user.getPassword())){
throw new ValidateException(403,"用户名或密码有误!!");
}
// 4.如果登录成功就写入session、中
session.setAttribute("session_user",user);
return "susccess";
}
}
1、如果你的用户平台不多,大概注册用户10w,日活:3000个用户,每个用户对象都会创建一个会话!
session.setAttribute("session_user",user);
在服务器就可能存储3000多个会话,如果以用户的占用内存的字节大小是:10kb,大概会消耗服务器资源20MB,如果是30000W 200mb。
5、基本session认证暴露的问题
Session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的签别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF:因为是基Fcookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
![](https://i-blog.csdnimg.cn/blog_migrate/a19928200cf2856ce9c402c0309d7d01.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ac0ae477733da190bb6988f7028475ce.png)
6、基于token的鉴权机制
基于token的鉴权机制类似fhtp协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息就意味着基Ftoken认证机制的应用不需要去考虑用户在哪台服务器登承了, 这就为应用的扩展提供了便
流程上是这样的:
-
用户使用用户名或者密码球扁钢求服务端
-
服务端进行验证用户的信息
-
服务器通过验证发送给用户一个token
-
客户端存储token,并在每次请求时携带上这个token值
-
服务端验证token值,并返回数据
这个token必须在每次请求时传递给服务端,它应该保存在响应头中,另外,服务端要支持CORS(跨来资源共享)策略,一般我们在服务器这么做就可以了 Access-Control-Allow-Origin:*
那么我们现在回到JWT的主题上
![](https://i-blog.csdnimg.cn/blog_migrate/8ba0cd03dc88827633c723772e4a9f3d.png)
7、JWT 长什么样?
1、JWT
JWT是由三段信息构成的,将这三段信息文本用.
连接一起就构成了jwt字符串,就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT的组成
第一部分我们称它为头部 (header)
第二部分我们称为载荷 (payload,类似与飞机上承载的物品)
第三部分是签证(signature)
header
jwt的头部承载两部分信息:
-
声明类型:这里是jwt
-
声明加密的算法,通常直接使用 HMAC SHA256
{
"alg": "HS256",
"typ": "JWT"
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分!
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
载荷就是存放有效信息的地方,这个名字就是特指飞机上承载的货品,这些有效信息包含三个部分
-
标准中注册的声明
-
公共的声明
-
私有的声明
标准中注册的声明(建议但不强制使用)
-
iss:jwt签发者
-
sub:jwt所面向的用户
-
aud:接收jwt的一方
-
exp:jwt的过期时间,这个过期时间必须大于签发时间
-
nbf:定义在什么时间之前,该jwt都是不可用的
-
iat:jwt的签发时间
-
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
公共的声明:
公共的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必须信息,但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明:
私有声明是提供者和消费者所共同定义的声明,一般不建议 存放敏感信息,因为base64是对称解密的,意味着该部分恩信息可以归类为铭文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
然后其进行base64加密,得到jwt的第二部分!
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
-
header(base64后的)
-
payload(base64后的)
-
secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分!
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
//将加密后的头部和载荷结合起来,使用header中的加密算法进行加盐secret组合加密,得到第三部分
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt;
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务端的,jwt的签发生成也是服务端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该暴露出去。一旦客户端得知这个secert,那么就意味着客户端是可以自我签发jwt了。
如何应用
一般是在请求头里加入Authorization
,并加上Bearer
标注:
2、JWS
JWS ,也就是JWT Signature,其结构就是在之前nonsecure JWT的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证jwt不能被他人随意篡改。我们通常使用的JWT一般都是JWS
为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。加密的算法一般有2类:
-
对称加密:
secretKey
指加密密钥,可以生成签名与验签 -
非对称加密:
secretKey
指私钥,只用来生成签名,不能用来验签(验签用的是公钥)
JWT的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK
到目前为止,jwt的签名算法有三种:
-
HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
-
RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
-
ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
官网推荐了6个Java使用JWT的开源库,其中比较推荐使用的是java-jwt
和jjwt-root
8、JWT实战
1、导入jwt依赖
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
2、创建一个UserVo
package com.zhao.springboot.jwt.controller.entity;
import java.io.Serializable;
public class UserVo implements Serializable {
//下面的userid、username、openid是需要被jwt加密的数据
// 这些数据一定要唯一的
private Long userid;
private String username;
private String openid;
//2、通过jwt生成token
private String token;
// 3、刷新token
private String refreshToken;
public Long getUserid() {
return userid;
}
public void setUserid(Long userid) {
this.userid = userid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Long getOpenid() {
return openid;
}
public void setOpenid(Long openid) {
this.openid = openid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
3、统一返回token对象
package com.zhao.springboot.jwt.vo;
import com.zhao.springboot.jwt.controller.entity.UserVo;
public class AuthResponse {
private UserVo userVo;
private Long code;
// 调用过程中,保持一种调用
public static AuthResponse success(UserVo userVo, Long code){
AuthResponse authResponse = new AuthResponse();
authResponse.setUserVo(userVo);
authResponse.setCode(code);
return authResponse;
}
public static AuthResponse code(Long code){
AuthResponse authResponse = new AuthResponse();
authResponse.setUserVo(null);
authResponse.setCode(code);
return authResponse;
}
private AuthResponse() {
}
public UserVo getUserVo() {
return userVo;
}
public void setUserVo(UserVo userVo) {
this.userVo = userVo;
}
public Long getCode() {
return code;
}
public void setCode(Long code) {
this.code = code;
}
}
package com.zhao.springboot.jwt.vo;
/**
* 常量定义
*/
public class ResponseCode {
// 登录成功 1L
public static final Long SUCCESS = 1L;
// 密码有误
public static final Long INCORRECT_PASSWORD = 1000L;
// 用户名找不到
public static final Long USER_NOT_FOUND = 1001L;
}
4、生成token的服务实现
package com.zhao.springboot.jwt.handler.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.zhao.springboot.jwt.controller.entity.UserVo;
import com.zhao.springboot.jwt.vo.AuthResponse;
import com.zhao.springboot.jwt.vo.ResponseCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
public class JwtService {
private static final Logger log = LoggerFactory.getLogger(JwtService.class);
/**
* 1、header -- 确定算法和类型
* 2、payload -- 确定要加密的数据
* 3、sign -- 把 header 和 payload + 盐值 进行统一生成一个token!
*/
// 1、定义加密盐的盐信息 enc
private static final String KEY = "www.kuangstudy.com.123";
// 2、发行者
private static final String ISSUSER = "YYKK";
// 3、定义token的过期时间
//private static final Long TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 30L;
public static final Long TOKEN_EXPIRE_TIME = 1000 * 60L;
/**
* 有状态:服务端来维护你的数据信息,消耗的服务端资源
* 无状态:服务端不维护你数据信息,不会消耗服务端资源!
* @param userVo
* @return
*/
public String token(UserVo userVo){
// token签发时间
Date now = new Date();
// 1、确定加密算法 -- header
Algorithm algorithm = Algorithm.HMAC256(KEY);
// 2、开始创建和生成token
String token = JWT.create()
.withIssuer(ISSUSER) // 签发者
.withIssuedAt(now) // token 签发时间
.withExpiresAt(new Date(now.getTime()+TOKEN_EXPIRE_TIME)) // 设定jwt的服务器过期时间
.withClaim("username",userVo.getUsername())
.withClaim("userid",userVo.getUserid())
.withClaim("openid",userVo.getOpenid())
.sign(algorithm);
return token;
}
/**
* 验证token 验证用户!
* @param token
* @param userid 这里建议使用雪花算法的userid!
* @return
*/
public AuthResponse verifyUserId(String token,String userid) {
log.info("verifying jwt - userid = {}",userid);
try {
//1.定义算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
//2.进行校验
JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUSER)
.withClaim("userid",userid).build();
jwtVerifier.verify(token);
return AuthResponse.code(ResponseCode.SUCCESS);
} catch (Exception ex) {
log.error("auth verify fail,{}",ex);
return AuthResponse.code(ResponseCode.USER_NOT_FOUND);
}
}
/**
* token验证
* @param token
* @param username
* @return
*/
public AuthResponse verify(String token,String username) {
log.info("verifying jwt - username = {}",username);
try {
//1.定义算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
//2.进行校验
JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUSER)
.withClaim("username",username).build();
// 校验方法这个的含义:
// 内部会把token反解密出来
jwtVerifier.verify(token); //如果校验成功,不做任何处理,如果校验失败就抛出异常!
return AuthResponse.code(ResponseCode.SUCCESS);
} catch (Exception ex) {
log.error("auth verify fail,{}",ex);
return AuthResponse.code(ResponseCode.USER_NOT_FOUND);
}
}
/**
* 验证token 这个是验证微信小程序!
* @param token
* @param openid
* @return
*/
public AuthResponse verifyOpenid(String token,String openid) {
log.info("verifying jwt - openid = {}",openid);
try {
//1.定义算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
//2.进行校验
JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUSER)
.withClaim("openid",openid).build();
jwtVerifier.verify(token);
return AuthResponse.code(ResponseCode.SUCCESS);
} catch (Exception ex) {
log.error("auth verify fail,{}",ex);
return AuthResponse.code(ResponseCode.USER_NOT_FOUND);
}
}
}
5、关于token的生成派发和校验
package com.zhao.springboot.jwt.controller;
import cn.hutool.core.exceptions.ValidateException;
import com.zhao.springboot.jwt.entity.User;
import com.zhao.springboot.jwt.entity.UserVo;
import com.zhao.springboot.jwt.handler.jwt.JwtService;
import com.zhao.springboot.jwt.utils.MD5Utils;
import com.zhao.springboot.jwt.vo.AuthResponse;
import com.zhao.springboot.jwt.vo.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class LoginAuthController {
@Autowired
private JwtService jwtService;
@Autowired
private UserService userService;
@Autowired
RedisTemplate redisTemplate;
@PostMapping("/login")
public AuthResponse login(String username,String password){
// 2.验证用户名查询账户是否存在!
User user = userService.getUserByName(username);
if (user == null){
throw new ValidateException(403,"用户名或密码有误!!");
}
// 3.对密码加盐加密进行处理
password = MD5Utils.md5(password);
// 如果用户输入的面膜和数据库查询的密码不一致
if (!password.equalsIgnoreCase(user.getPassword())){
throw new ValidateException(403,"用户名或密码有误!!");
}
// 1.todo 验证用户名和密码是否正确
UserVo userVo = new UserVo();
userVo.setUserid(user.getUserid());
userVo.setOpenid(user.getOpenid());
userVo.setUsername(user.getUsername());
// token信息
String token = jwtService.token(userVo);
// 写入token
userVo.setToken(token);
// 刷新token的key
userVo.setRefreshToken(UUID.randomUUID().toString());
return AuthResponse.success(userVo, ResponseCode.SUCCESS);
}
@GetMapping("/verifyusername")
public AuthResponse verify(String username,String token){
return jwtService.verify(token,username);
}
@GetMapping("/verifyuserid")
public AuthResponse verifyuserId(String userid,String token){
return jwtService.verify(token,userid);
}
@GetMapping("/verifyopenid")
public AuthResponse verifyOpenId(String openid,String token){
return jwtService.verify(token,openid);
}
/**
* 续签的方法 -- 在有效期时间内,如果你执行了续签方法
* 1.重新生成token
* 2.把时间的计算从当前时间开始计算!
* @param refreshToken
* @return
*/
@PostMapping("/refresh")
public AuthResponse refresh(String refreshToken){
UserVo userVo = (UserVo) redisTemplate.opsForValue().get(refreshToken);
if (userVo == null){
// 告诉用户,token已经失效,查看当前用户是否还在有效期内!
return AuthResponse.code(ResponseCode.USER_NOT_FOUND);
}
// 重新生成token
String jwt = jwtService.token(userVo);
userVo.setToken(jwt);
userVo.setRefreshToken(UUID.randomUUID().toString());
redisTemplate.delete(refreshToken);
redisTemplate.opsForValue().set(userVo.getRefreshToken(),userVo,
JwtService.TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
return AuthResponse.success(userVo,ResponseCode.SUCCESS);
}
@GetMapping("/logout")
public AuthResponse logout(){
// redisTemplate.delete(refreshToken);
return AuthResponse.code(409L);
}
}
注意上面的代码:
@GetMapping("/verifyusername")
public AuthResponse verify(String username,String token){
return jwtService.verify(token,username);
}
@GetMapping("/verifyuserid")
public AuthResponse verifyuserId(String userid,String token){
return jwtService.verify(token,userid);
}
@GetMapping("/verifyopenid")
public AuthResponse verifyOpenId(String openid,String token){
return jwtService.verify(token,openid);
}
这里在实际开发中是不会定义的,这里只是为了进行测试与校验!
6、接口 + jwt安全性拦截
接口:
package com.zhao.springboot.jwt.common.anno;
public @interface IgnoreToken {
boolean required() default true;
}
拦截器:
package com.zhao.springboot.jwt.handler.jwt;
import cn.hutool.core.exceptions.ValidateException;
import com.zhao.springboot.jwt.common.anno.IgnoreToken;
import com.zhao.springboot.jwt.vo.AuthResponse;
import jdk.nashorn.internal.ir.annotations.Ignore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthorizationInterceptor.class);
@Autowired
private JwtService jwtService;
@Value("${spring.profiles.active}")
private String profiles;
private static final String AUTH = "Authorization";
private static final String AUTH_USERNAME = "ksd-user-name";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setContentType("application/json;charset=utf-8");
// 如果是开发环境直接通过
if (StringUtils.isEmpty(profiles) && profiles.equals("dev")){
return true;
}
String token = request.getHeader("token");
// 如果不是测试方法直接跳过
//if (!(handler instanceof HandlerMethod)) {
// return true;
//}
// 2.从http请求头获取接口
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 3.如果一个方法加了 @IgnoreToken代表不需要token直接放行返回
if(method.isAnnotationPresent(Ignore.class)){
IgnoreToken loginToken = method.getAnnotation(IgnoreToken.class);
if (loginToken.required()){
// 4.检测是否有IgnoreToken注释,有就跳过认证
String token = getParam(request,"token");
String userid = getParam(request,"userid");
log.info("token 是{}",token);
if (StringUtils.isEmpty(token)){
throw new ValidateException(300,"token不允许为空,请重新登录!");
}
//String sign = "xxxxxxxx"; 这里可以使用MD5进行加密,如果使用加密后的请添加对应的字段!
// String sign = MD5Utils.md5("xxxx"+userid); 这个更加灵活!
if (!"www.kuangstudy.com".equalsIgnoreCase(token)){
log.info(" token value不正确{}",token);
throw new ValidateException(300,"token签名不对,请重新登录!");
}
}
}
// 4.检测是否有IgnoreToken注释,有就跳过认证
String username = getParam(request,AUTH_USERNAME);
String token = getParam(request,AUTH);
if (StringUtils.isEmpty(token)){
throw new ValidateException(300,"Authorization 不允许为空,请重新登录!");
}
// 这个与下一个如果不能使用的使用可以注释一下
if (StringUtils.isEmpty(username)){
throw new ValidateException(300,"Authorization 不允许为空,请重新登录!");
}
// --------
AuthResponse authResponse = jwtService.verify(token,username);
if (authResponse.getCode() != 1L){
log.error("invalid error");
throw new ValidateException(300," token vali fail");
}
// 把用户放入到header当中返回
response.setHeader(AUTH_USERNAME,username);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
public static String getParam(HttpServletRequest request,String fileName){
// 1.在参数里面获取对应的filename的值
String param = request.getParameter(fileName);
// 2.如果不存在
if (StringUtils.isEmpty(param)){
Map data = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
Object o = data.get(fileName);
if (o != null){
// 3.就在请求头中获取对应的filename的值
param = String.valueOf(o);
}
}
if (StringUtils.isEmpty(param)){
param = request.getHeader(fileName);
}
return param;
}
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
7、配置拦截器
package com.zhao.springboot.jwt.config;
import com.zhao.springboot.jwt.handler.jwt.AuthorizationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class Interceptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor())
.addPathPatterns("/**");
}
@Bean
public AuthorizationInterceptor authorizationInterceptor(){
return new AuthorizationInterceptor();
}
}
在这里可能遇到的一个问题,那就是我们的AuthorizationInterceptor如果只是简单使用的话,没有生效请在Interceptor 中进行配置配出过滤规则!
8、token续签问题
使用redis缓存
认证中心在颁发token的同时,在redis设置key为用户数据(用户id或其他特征信息)+客户端环境特征(IP、UA、网卡MAC等),最好再将key进行加密,如MD5
,并设置对应的过期时间。
当收到token时,检查在redis是否过期:
-
如果过期则要求用户重新登录,
-
否则检查时间是否过半,如果没有则按照正常流程执行,
-
否则在redis中为key增加一半的有效时间
使用两个token
认证中心在颁发token的时候,颁发两个token,一个为正常使用token,一个为续签token,续签token过期时间比正常使用token时间长一倍。
-
当正常使用token未过期则按正常流程执行;
-
当正常使用token过期,续签token未过期,则重新签发两个新的token,延长其过期时间外其他信息完全相同;
-
如果续签token过期,则要求用户重新登录。
存在重复生成JWT的问题,解决办法:在认证中心设计一个Map,记录过去一段时间内(几秒钟)的原始JWT(key)和新生成的JWT(value),如果在一段时间发现同样的原始JWT,则返回相同的新JWT。
9、如何验证token是否有效
获取token的前两部分,即加密后的头部和载荷
-
检查token是否过期:将载荷解密后检查过期时间
-
验证token是否未被篡改:再次使用加密后的头部和载荷生成签名,比较两次签名是否相同
10、token由谁颁发
用户在第一次登录后,用认证中心统一颁发token
11、token如何传输
客户端在进行请求的时候,将token存放在cookie中或者存放在请求头中
12、常见异常信息
-
SignatureVerificationException
: 签名不一致异常 -
TokenExpiredException
: 令牌过期异常 -
AlgorithmMismatchException
: 算法不匹配异常 -
InvalidClaimException
: 失效的payload异常
拓展– – 盐值
盐值
JWT加密以及解密工具类(生成盐值,根据密码和盐值生成密文)
也可以看看这个博客:https://blog.csdn.net/weixin_44219219/article/details/116303931
MD5Utils工具类
import java.security.MessageDigest;
import java.util.Arrays;
import java.security.SecureRandom;
/**
* @program: huawen-cloud-parent
* @ClassName: MD5Utils
* @version: 1.0
* @description: md5加盐加密
* @create: 2019-12-13 15:06
**/
public class MD5Utils {
private static final Integer SALT_LENGTH = 12;
/**
* 16进制数字
*/
private static final String HEX_NUMS_STR="0123456789abcdef";
/**
* 加密密码
* @param password
* @return
* @throws Exception
*/
public static String getEncryptedPwd(String password) throws Exception{
try{
//声明加密后的口令数组变量
byte[] pwd = null;
//随机数生成器
SecureRandom random = new SecureRandom();
//声明盐数组变量
byte[] salt = new byte[SALT_LENGTH];
//将随机数放入盐变量中
random.nextBytes(salt);
//获得加密的数据
byte[] digest = encrypte(salt,password);
//因为要在口令的字节数组中存放盐,所以加上盐的字节长度
pwd = new byte[digest.length + SALT_LENGTH];
//将盐的字节拷贝到生成的加密口令字节数组的前12个字节,以便在验证口令时取出盐
System.arraycopy(salt, 0, pwd, 0, SALT_LENGTH);
//将消息摘要拷贝到加密口令字节数组从第13个字节开始的字节
System.arraycopy(digest, 0, pwd, SALT_LENGTH, digest.length);
//将字节数组格式加密后的口令转化为16进制字符串格式的口令
return byteToHexString(pwd);
}catch(Exception e){
throw new Exception("获取加密密码失败",e);
}
}
/**
* 验证密码是否正确
* @param password
* @param passwordInDb
* @return
* @throws Exception
*/
public static boolean validPassword(String password, String passwordInDb) throws Exception {
try{
//将16进制字符串格式口令转换成字节数组
byte[] pwdInDb = hexStringToByte(passwordInDb);
//声明盐变量
byte[] salt = new byte[SALT_LENGTH];
//将盐从数据库中保存的口令字节数组中提取出来
System.arraycopy(pwdInDb, 0, salt, 0, SALT_LENGTH);
//获得加密的数据
byte[] digest = encrypte(salt,password);
//声明一个保存数据库中口令消息摘要的变量
byte[] digestInDb = new byte[pwdInDb.length - SALT_LENGTH];
//取得数据库中口令的消息摘要
System.arraycopy(pwdInDb, SALT_LENGTH, digestInDb, 0, digestInDb.length);
//比较根据输入口令生成的消息摘要和数据库中消息摘要是否相同
if (Arrays.equals(digest, digestInDb)) {
//口令正确返回口令匹配消息
return true;
} else {
//口令不正确返回口令不匹配消息
return false;
}
}catch(Exception e){
throw new Exception("密码验证失败",e);
}
}
/**
* 将指定byte数组转换成16进制字符串(大写)
* @return
*/
public static String byteToHexString(byte[] bytes) {
StringBuffer md5str = new StringBuffer();
//把数组每一字节换成16进制连成md5字符串
int digital;
for (int i = 0; i < bytes.length; i++) {
digital = bytes[i];
if(digital < 0) {
digital += 256;
}
if(digital < 16){
md5str.append("0");
}
md5str.append(Integer.toHexString(digital));
}
return md5str.toString();
}
/**
*
* 根据盐生成密码
* @param salt
* @param passwrod
* @return
* @throws Exception
*/
public static byte[] encrypte(byte[] salt,String passwrod) throws Exception{
try{
//声明消息摘要对象
MessageDigest md = null;
//创建消息摘要
md = MessageDigest.getInstance("MD5");
//将盐数据传入消息摘要对象
md.update(salt);
//将口令的数据传给消息摘要对象
md.update(passwrod.getBytes("UTF-8"));
//获得消息摘要的字节数组
return md.digest();
}catch(Exception e){
throw new Exception("Md5解密失败",e);
}
}
/**
* 将16进制字符串转换成字节数组(大写)
* @param hex
* @return
*/
public static byte[] hexStringToByte(String hex) {
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] hexChars = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (HEX_NUMS_STR.indexOf(hexChars[pos]) << 4
| HEX_NUMS_STR.indexOf(hexChars[pos + 1]));
}
return result;
}
public static void main(String[] args) {
String encryptedPwd=new String();
try {
encryptedPwd = getEncryptedPwd("123456sssWW");
System.out.println(encryptedPwd);
} catch (Exception e) {
e.printStackTrace();
}
try {
boolean b = validPassword("123456sssWW", encryptedPwd);
System.out.println(b);
} catch (Exception e) {
e.printStackTrace();
}
}
}