JWT由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
-
Header
-
Payload
-
Signature
三种都是经过Base64URL编码后的串。
写成一行,就是下面的样子。
Header.Payload.Signature
具体的构成方式如下:
base64(header).base64(payload).base64( HS256(base64(header) + “.” + base64(payload), secret) )
Header
jwt的头部由两部分信息组成:
-
type:声明类型,这里是jwt
-
alg:声明加密的算法 通常直接使用 HMAC SHA256
{
“typ”:“jwt”,
“alg”:“HS256”
}
对头部信息进行Base64编码的得到第一部分的信息。
Payload
载荷就是存放有效信息的地方,它包含声明(要求)。声明有三种类型:
-
registered claims:标准中注册的声明。这里有一组预定义的声明,它们不是强制的,但是推荐
-
public claims:公共的声明
-
private claims:私有的声明
标准中注册的声明 (建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之前,该jwt都是不可用的
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
对Payload进行Base64加密就得到了JWT第二部分的内容。
Signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
-
header (base64后的)
-
payload (base64后的)
-
secret
Signature 部分是对前两部分的签名,防止数据篡改。
第三部分需要base64加密后的header和base64加密后的payload使用 .
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。
注意:
secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。
一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。(???)
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
@SpringBootTest
class LearnJwtApplicationTests {
private static final String secret = “secret”;
private static final String secretBase64;
static {
secretBase64 = Base64.getEncoder().encodeToString(secret.getBytes());
}
@Test
void testJwtCreate() {
JwtBuilder jwtBuilder = Jwts.builder();
String token = jwtBuilder
// header
.setHeaderParam(“alg”, “HS256”)
.setHeaderParam(“typ”, “JWT”)
// payload
.claim(“sub”, “1234567890”) // 标准声明
.claim(“iat”, 1516239022) // 标准声明
.claim(“name”, “John Doe”) // 自定义声明
// signature
.signWith(SignatureAlgorithm.HS256, secretBase64)
.compact();
System.out.println();
System.out.println(token);
}
@Test
void testJWTParse() {
String token = “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.” +
// “eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.”+
“eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9vIiwiaWF0IjoxNTE2MjM5MDIyfQ.” +
“XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o”;
JwtParser jwtParser = Jwts.parser();
Jws claimsJws = jwtParser.setSigningKey(secretBase64).parseClaimsJws(token);
JwsHeader header = claimsJws.getHeader();
Claims body = claimsJws.getBody();
String signature = claimsJws.getSignature();
System.out.println(“----- header -----”);
System.out.println("alg : " + header.getAlgorithm());
System.out.println("typ : "+header.getType());
System.out.println(“----- body -----”);
System.out.println("sub : "+body.getSubject());
System.out.println("iat : "+body.getIssuedAt());
System.out.println("name : "+body.get(“name”));
System.out.println("id : "+body.getId());
System.out.println(“----- signature -----”);
System.out.println("signature : " + signature);
}
}
========================================================================
前端储存JWT
import axios from ‘axios’;
axios.post(
“/user/login”,
{
username: this.userId,
password: this.userPsw,
},
).then((res)=>{
if (res.status === 200) {
localStorage.setItem(“token”, res.data.data);
this.$message({
title: “登录成功”,
message: “登录成功!正在为您跳转页面…”,
type: “success”,
duration: 1000,
showClose: false,
onClose: () => {
this.$store.dispatch(“setNoToken”, false);
this.$router.go(-1);
},
});
} else {
this.$alert(
错误代码${res.data.code}:${res.data.message}
,
“登录失败”,
{
type: “error”,
}
);
}
});
请求携带JWT
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域(cookie跨域那些事),所以更好的做法是放在 HTTP 请求的头信息Authorization
字段里面。
Authorization: Bearer
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
import axios from ‘axios’;
axios.post(
‘/test’, // url
{num: 1}, // data
{
headers: {
Authorization: Bearer ${token}
}
}, // options
).then((res)=>{
console.log(res);
})
前端提取JWT携带的信息
前面也有提到,JWT的payload模块可以携带一些业务逻辑所必要的非敏感信息。因此,前端需要能够解析出JWT字符串。
举个例子,在sduoj中,需要判断用户的登录信息是否合法,其中一项评判标准就是JWT是否过期。在后端的服务器代码中存在这一部分逻辑,而在前端的代码中,也包含了这一段逻辑代码。
前端的代码逻辑中,仅在用户首次与sduoj的前端页面建立会话时进行判断。
// 首先判断localStorage中是否存在token字段
if(localStorage.getItem(“token”) !== null) {
/**
-
注意,由于localStorage是以字符串形式取出的
-
当token字段是空字符串时,javascript会将其转换为布尔值false。
-
因此需要增加!==null进行判断。
*/
// 使用模块加载器将通过yarn安装的jsonwebtoken加载进来
let jwt = require(“jsonwebtoken”);
// 使用jsonwebtoken解析JWT字符串
const TOKEN = jwt.decode(localStorage.getItem(“token”));
/**
-
通过jsonwebtoken解析后将会得到包含payload非敏感部分明文的JSON对象
-
提取jwt的过期时间,过期时间处在exp字段下
-
注意:jwt的时间单位比javascript、java等语言的时间单位要大3个数量级
-
因此,需要给解析出的时间乘上1000
*/
let exp = TOKEN.exp * 1000;
// 判断当前时间是否已超过token的过期时间
if (exp <= new Date().getTime()) {
this.$message({
message: “您的身份认证已过期,请重新登陆”,
type: “warning”,
duration: 1000,
onClose: () => {
/**
-
消息提示关闭后执行两项任务
-
1.将过期的token字段从localStorage中移除
-
2.为用户跳转至登录界面
*/
localStorage.removeItem(“token”);
this.$router.push(“/login”);
},
});
}
} else {
// 当localStorage中不存在token字段时,自动跳转至登录界面
this.$router.push(“/login”);
}
生成、解析JWT
package com.jsy.learnjwt.util;
import com.jsy.learnjwt.entity.CheckResult;
import com.jsy.learnjwt.entity.SystemConstant;
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
public class JwtUtil {
/**
-
签发JWT
-
@param id JWT的唯一标识
-
@param subject 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userId,roleId之类的,作为什么用户的唯一标志
-
@param ttlMillis 有效时间
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) // 是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setSubject(subject)
.setIssuer(“user”) // 颁发者是使用 HTTP 或 HTTPS 方案的 URL(区分大小写),其中包含方案、主机及(可选的)端口号和路径部分
.setIssuedAt(now) // jwt的签发时间
.signWith(SignatureAlgorithm.HS256, secretKey); // 设置签名使用的签名算法和签名使用的秘钥
if (ttlMillis > 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
/**
- 验证JWT
*/
public static CheckResult validateJWT(String jwtStr) {
CheckResult checkResult = new CheckResult();
try {
Claims claims = parseJWT(jwtStr);
checkResult.setClaims(claims);
checkResult.setSuccess(true);
} catch (ExpiredJwtException e) { // JWT 过期
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_EXPIRE);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
private static SecretKey generalKey() {
byte[] encodedKey = SystemConstant.JWT_SECRET.getBytes();
return new SecretKeySpec(encodedKey, 0, encodedKey.length, “AES”);
}
/**
- 解析JWT字符串
*/
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
拦截器处理token的验证
package com.jsy.learnjwt.config;
import com.alibaba.fastjson.JSONObject;
import com.jsy.learnjwt.entity.CheckResult;
import com.jsy.learnjwt.entity.SystemConstant;
import com.jsy.learnjwt.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
/**
-
@author: SongyangJi
-
@description:
-
@since: 2021/12/27
*/
@Slf4j
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.从Cookie获取token
String token = getTokenFromCookie(request);
if (StringUtils.isBlank(token)) {
// 2.从headers中获取
token = request.getHeader(“token”);
}
if (StringUtils.isBlank(token)) {
// 3.从请求参数获取
token = request.getParameter(“token”);
}
if (StringUtils.isBlank(token)) {
//输出响应流
JSONObject jsonObject = new JSONObject();
jsonObject.put(“msg”, “403”);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json; charset=utf-8”);
response.getOutputStream().write(jsonObject.toString().getBytes(StandardCharsets.UTF_8));
return false;
}
// 验证token
CheckResult checkResult = JwtUtil.validateJWT(token);
if (checkResult.isSuccess()) {
// 验证通过
return true;
} else {
if (checkResult.getErrCode().equals(SystemConstant.JWT_ERRCODE_EXPIRE)) {
//输出响应流
JSONObject jsonObject = new JSONObject();
jsonObject.put(“msg”, SystemConstant.JWT_ERRCODE_EXPIRE);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json; charset=utf-8”);
response.getOutputStream().write(jsonObject.toString().getBytes(StandardCharsets.UTF_8));
return false;
} else if (checkResult.getErrCode().equals(SystemConstant.JWT_ERRCODE_FAIL)) {
//输出响应流
JSONObject jsonObject = new JSONObject();
jsonObject.put(“msg”, SystemConstant.JWT_ERRCODE_FAIL);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json; charset=utf-8”);
response.getOutputStream().write(jsonObject.toString().getBytes(StandardCharsets.UTF_8));
return false;
}
//输出响应流
JSONObject jsonObject = new JSONObject();
jsonObject.put(“msg”, “403”);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json; charset=utf-8”);
response.getOutputStream().write(jsonObject.toString().getBytes(StandardCharsets.UTF_8));
return false;
}
}
private String getTokenFromCookie(HttpServletRequest request) {
String token = null;
Cookie[] cookies = request.getCookies();
int len = null == cookies ? 0 : cookies.length;
if (len > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(“token”)) {
token = cookie.getValue();
break;
}
}
}
return token;
}
}
设置对应路径的接口的拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 设置对应路径的接口的拦截器
registry.addInterceptor(myInterceptor).addPathPatterns(“/token/**”);
bject.put(“msg”, “403”);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json; charset=utf-8”);
response.getOutputStream().write(jsonObject.toString().getBytes(StandardCharsets.UTF_8));
return false;
}
}
private String getTokenFromCookie(HttpServletRequest request) {
String token = null;
Cookie[] cookies = request.getCookies();
int len = null == cookies ? 0 : cookies.length;
if (len > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(“token”)) {
token = cookie.getValue();
break;
}
}
}
return token;
}
}
设置对应路径的接口的拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 设置对应路径的接口的拦截器
registry.addInterceptor(myInterceptor).addPathPatterns(“/token/**”);