一、JWT是什么
JWT的全称为json web token。是一种生成token的方式。一般我们访问一个系统的流程就是:请求登录接口,该接口会返回一个token,为了防止对象数据被篡改,生成JSON时会加上签名,请求其他接口都要带上token,token验证通过才能访问成功,而JWT就是生成token的一种机制。
广义上讲JWT是一个标准的名称;狭义上讲JWT指的就是用来传递的那个token字符串。
二、JWT的组成
JWT含有三个部分:
- 头部(header)
- 载荷(payload)
- 签证(signature)
2.1 头部(header)
头部一般有两部分信息:类型
、加密的算法
(通常使用HMAC SHA256)
头部一般使用base64加密:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
解密后:
{
"typ":"JWT",
"alg":"HS256"
}
2.2 载荷(payload)
该部分一般存放一些有效的信息。JWT的标准定义包含五个字段:
-
iss
:该JWT的签发者 -
sub
:该JWT所面向的用户 -
aud
:接收该JWT的一方 -
exp(expires)
:什么时候过期,这里是一个Unit的时间戳 -
iat(issued at)
:在什么时候签发的
2.3签证(signature)
JWT最后一个部分。该部分是使用了HS256加密后的数据;包含三个部分:
- header(base64后的)
- payload(base64后的)
- secret 私钥
secret
是保存在服务器端的,JWT的签发生成也是在服务器端的,secret
就是用来进行JWT的签发和JWT的验证
,所以,它就是你服务端的秘钥
,在任何场景都不应该流露出去。一旦客户端得知这个secret
,那就意味着客户端可以自我签发JWT了。
2.4 解密调试器
官网的加密串解密调试器:https://jwt.io/#debugger-io
三、如何使用JWT?
3.1 工作原理
在身份鉴定的实现中,传统的方法是在服务端存储一个 session,但是如果是大并发系统服务器内存占用将不可控,给客户端返回一个 cookie。
而使用JWT之后,当用户使用它的认证信息登录系统之后,会返回给用户一个JWT, 用户只需要本地保存该 token(通常使用localStorage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在Authorization
头部使用Bearer
模式添加JWT,其内容格式:
Authorization: Bearer <token>
请求header形如:
fetch('api/users', {
headers: {
'Authorization': 'Bearer ' + token
}
})
Postman发起JWT请求Header的key为Authorization
,Header的value为Bearer <token>
,示意
因为用户的状态在服务端内容中是不存储的,所以这是一种无状态的认证机制。服务端的保护路由将会检查请求头 Authorization 中的JWT信息,如果合法,则允许用户的行为。由于JWT是 自包含的,因此,减少了需要查询数据库的需要。
JWT的这些特征使得我们可以完全依赖无状态的特性提供数据API服务。因为JWT并不使用Cookie的,所以你可以在任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)
3.2 流程介绍:
- 用户使用账号和密码发出POST登录请求;
- 服务器使用私钥创建一个JWT;
- 服务器返回这个JWT给浏览器;
- 浏览器将该JWT串放在请求头中向服务器发送请求;
- 服务器验证该J
四、搭建SpringBoot + JWT工程
4.1 新建module
引入依赖,完整POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.erbadagang.springboot.jwt</groupId>
<artifactId>jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt</name>
<description>JWT project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.0.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT token-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.2 application.yml
在工程application.yml
配置文件中添加JWT的配置信息:
#####JWT配置#####
audience:
# 代表这个JWT的接收对象,存入audience
aud: 98f6bcd4621d37
# 密钥, 经过Base64加密, 可自行替换。Base64加解密工具:http://tool.chinaz.com/Tools/Base64.aspx
base64Secret: Z3VveGl1emhpRXJiYWRhZ2FuZ1dpbnNwYWNlVjMuMA==
# JWT的签发主体,存入issuer
iss: issued by 郭秀志
# 过期时间毫秒
expiresSecond: 172800
Base64加解密工具:http://tool.chinaz.com/Tools/Base64.aspx
4.3 Audience
新建配置信息的实体类,以便获取JWT配置:
package com.erbadagang.springboot.jwt.model;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @description 配置信息DTO,通过读取配置文件自动赋值。
* @ClassName: Audience
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/16 20:07
* @Copyright:
*/
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
//代表这个JWT的接收对象,存入audience
private String aud;
private String base64Secret;
//JWT的签发主体,存入issuer
private String iss;
private int expiresSecond;
}
4.4 创建JWT工具类
package com.erbadagang.springboot.jwt.util;
import com.erbadagang.springboot.jwt.common.exception.CustomException;
import com.erbadagang.springboot.jwt.common.response.ResultCode;
import com.erbadagang.springboot.jwt.model.Audience;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
/**
* @description JWT工具类,生成及解析token
* @ClassName: JwtTokenUtil
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/16 16:55
* @Copyright:
*/
@Slf4j
public class JwtTokenUtil {
public static final String AUTH_HEADER_KEY = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 解析jwt
*
* @param jsonWebToken
* @param base64Security
* @return
*/
public static Claims parseJWT(String jsonWebToken, String base64Security) {
try {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
.parseClaimsJws(jsonWebToken).getBody();
return claims;
} catch (ExpiredJwtException eje) {
log.error("===== Token过期 =====", eje);
throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED);
} catch (Exception e) {
log.error("===== token解析异常 =====", e);
throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
}
}
/**
* 构建jwt
*
* @param userId
* @param username
* @param role
* @param audience
* @return
*/
public static String createJWT(String userId, String username, String role, Audience audience) {
try {
// 使用HS256加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
// 可以将基本不重要的对象信息放到claims
.claim("role", role)
.claim("userId", userId)
.setSubject(username) // 代表这个JWT的主体,即它的所有人
.setIssuer(audience.getIss()) // 代表这个JWT的签发主体;
.setIssuedAt(new Date()) // 是一个时间戳,代表这个JWT的签发时间;
.setAudience(audience.getAud()) // 代表这个JWT的接收对象;
.signWith(signatureAlgorithm, signingKey);
//添加Token过期时间
int TTLMillis = audience.getExpiresSecond();
if (TTLMillis >= 0) {
long expMillis = nowMillis + TTLMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp) // 是一个时间戳,代表这个JWT的过期时间;
.setNotBefore(now); // 是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的
}
//生成JWT
return builder.compact();
} catch (Exception e) {
log.error("签名失败", e);
throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
}
}
/**
* 从token中获取用户名
*
* @param token
* @param base64Security
* @return
*/
public static String getUsername(String token, String base64Security) {
return parseJWT(token, base64Security).getSubject();
}
/**
* 从token中获取用户ID
*
* @param token
* @param base64Security
* @return
*/
public static String getUserId(String token, String base64Security) {
String userId = parseJWT(token, base64Security).get("userId", String.class);
return Base64Util.decode(userId);
}
/**
* 是否已过期
*
* @param token
* @param base64Security
* @return
*/
public static boolean isExpiration(String token, String base64Security) {
return parseJWT(token, base64Security).getExpiration().before(new Date());
}
}
4.5 拦截器
JWT验证主要是通过过滤器验证,所以我们需要添加一个拦截器来演请求头中是否包含有后台颁发的 token,这里请求头的格式:
Authorization: Bearer <token>
package com.erbadagang.springboot.jwt.interceptor;
import com.erbadagang.springboot.jwt.annotation.JwtIgnore;
import com.erbadagang.springboot.jwt.common.exception.CustomException;
import com.erbadagang.springboot.jwt.common.response.ResultCode;
import com.erbadagang.springboot.jwt.model.Audience;
import com.erbadagang.springboot.jwt.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* token验证拦截器
*/
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private Audience audience;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 忽略带JwtIgnore注解的请求, 不做后续token认证校验
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
if (jwtIgnore != null) {
return true;
}
}
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// 获取请求头信息authorization信息
final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
log.info("## authHeader= {}", authHeader);
if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
log.info("### 用户未登录,请先登录 ###");
throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
}
// 获取token
final String token = authHeader.substring(7);
if (audience == null) {
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
audience = (Audience) factory.getBean("audience");
}
// 验证token是否有效--无效已做异常抛出,由全局异常处理后返回对应信息
JwtTokenUtil.parseJWT(token, audience.getBase64Secret());
return true;
}
}
4.6 配置拦截器
package com.erbadagang.springboot.jwt.config;
import com.erbadagang.springboot.jwt.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截路径可自行配置多个 可用 ,分隔开
registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
}
/**
* 跨域支持
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
.maxAge(3600 * 24);
}
}
这里JWT可能会有跨域问题,配置跨域支持。
4.7 编写测试Controller接口
package com.erbadagang.springboot.jwt.controller;
import com.alibaba.fastjson.JSONObject;
import com.erbadagang.springboot.jwt.annotation.JwtIgnore;
import com.erbadagang.springboot.jwt.common.response.Result;
import com.erbadagang.springboot.jwt.model.Audience;
import com.erbadagang.springboot.jwt.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@Slf4j
@RestController
public class AdminUserController {
@Autowired
private Audience audience;
@PostMapping("/login")
@JwtIgnore
public Result adminLogin(HttpServletResponse response, String username, String password) {
// 这里模拟测试, 默认登录成功,返回用户ID和角色信息
String userId = UUID.randomUUID().toString();
String role = "admin";
// 创建token
String token = JwtTokenUtil.createJWT(userId, username, role, audience);
log.info("### 登录成功, token={} ###", token);
// 将token放在响应头
response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, JwtTokenUtil.TOKEN_PREFIX + token);
// 将token响应给客户端
JSONObject result = new JSONObject();
result.put("token", token);
return Result.SUCCESS(result);
}
@GetMapping("/users")
public Result userList() {
log.info("### 查询所有用户列表 ###");
return Result.SUCCESS();
}
}
4.8 测试
接下来我们使用PostMan工具进行测试。
4.8.1 未登录
没有登录时候直接访问:http://localhost:8080/users 接口:
4.8.2 登录
访问登录接口,模拟用户guoxiuzhi登录:
http://localhost:8080/login?username=guoxiuzhi&password=12345678
其中的token信息:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJ1c2VySWQiOiJkZWIxODhmMS1mYTZiLTQ0MDktYTVlNC1hMTY5ZmJkNGVhOWIiLCJzdWIiOiJndW94aXV6aGkiLCJpc3MiOiJpc3N1ZWQgYnkg6YOt56eA5b-XIiwiaWF0IjoxNTk0OTAzMTgxLCJhdWQiOiI5OGY2YmNkNDYyMWQzNyIsImV4cCI6MTU5NDkwMzQ4MSwibmJmIjoxNTk0OTAzMTgxfQ.ry1h3Lpq_GqkIjGUARYNSBhDctb0wgk4b0N6PIGizUM
该token可以在JWT官网的加密串解密调试器:https://jwt.io/#debugger-io进行解密查看数据:
4.8.3 带token访问
携带生成token再次访问:http://localhost:8080/users 接口
注意:这里选择
Bearer Token
类型,就把不要在 Token中手动Bearer
,postman会自动拼接。
底线
本文源代码使用 Apache License 2.0开源许可协议,可从Gitee代码地址通过git clone
命令下载到本地或者通过浏览器方式查看源代码。