JWT简介
背景
在传统的有状态服务应用中,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但在分布式应用中,由服务端保存用户状态不是一种很好的选择,因此JWT诞生
JWT概述
JWT(JSON WEB Token)是一个标准,借助JSON格式数据作为WEB应用请求中的令牌,进行数据的自包含设计,实现各方安全的信息传输,在数据传输过程中还可以对数据进行加密,签名等相关处理。同时JWT也是目前最流行的跨域身份验证解决方案(其官方网址为:https://jwt.io/)。可以非常方便的在分布式系统中实现用户身份认证。
JWT数据结构
JWT通常由三部分构成,分别为Header(头部),Payload(负载),Signature(签名),其格式如下:
xxxxx.yyyyy.zzzzz
例如:
eyJhbGciOiJIUzI1NiJ9.eyJwZXJtaXNzaW9ucyI6InN5czpyZXM6Y3JlYXRlLHN5czpyZXM6cmV0cmlldmUiLCJleHAiOjE2MjY5MzIyNTksImlhdCI6MTYyNjkzMDQ1OSwidXNlcm5hbWUiOiJqYWNrIn0.SQrRS5nuID1Xv5GMvUgnr7xrVzB7GcRFrkNak-x16Mw
Header部分
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将这个 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
Payload部分
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT规范中规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature部分
Signature 部分是对前两部分的签名,其目的是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT快速入门
环境准备
第一步:创建项目,例如:
第二步,添加依赖,其pom.xml文件内容如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.2.RELEASE</version>
</parent>
<groupId>com.cy</groupId>
<artifactId>03-jt-security-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--添加jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
</project>
第三步:创建配置文件application.yml (暂时不写任何内容)
第四步:定义启动类,代码如下
package com.cy.jt;
@SpringBootApplication
public class SecurityJwtApplication {
public static void main(String[] args) {
SpringApplication.run(
SecurityJwtApplication.class,
args);
}
}
第四步:运行启动类,检测是否可成功启动
创建和解析token
编写单元测试,实践Token对象的创建与解析,例如:
@Test
void testCreateAndParseToken(){
//1.创建令牌
//1.1定义负载信息
Map<String,Object> map=new HashMap<>();
map.put("username", "jack");
map.put("permissions", "sys:res:create,sys:res:retrieve");
//1.2定义过期实践
Calendar calendar=Calendar.getInstance();
calendar.add(Calendar.MINUTE, 30);
Date expirationTime=calendar.getTime();
//1.3定义密钥
String secret="AAABBBCCCDDD";
//1.4生成令牌
String token= Jwts.builder()
.setClaims(map)
.setIssuedAt(new Date())
.setExpiration(calendar.getTime())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
System.out.println(token);
//2.解析令牌
Claims claims = Jwts.parser().setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
System.out.println("claims="+claims);
}
创建JWT工具类
为了简化JWT在项目中的应用,我们通常会构建一个工具类,对token的创建和解析进行封装,例如:
package com.cy.jt.security.util;
public class JwtUtils {
/**
* 秘钥
*/
private static String secret="AAABBBCCCDDDEEE";
/**
* 有效期,单位秒
* 默认30分钟
*/
private static Long expirationTimeInSecond=1800L;
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public static Claims getClaimsFromToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
throw new IllegalArgumentException("Token invalided.");
}
}
/**
* 判断token是否过期
* @param token token
* @return 已过期返回true,未过期返回false
*/
private static Boolean isTokenExpired(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
/**
* 为指定用户生成token
* @param claims 用户信息
* @return token
*/
public static String generateToken(Map<String, Object> claims) {
Date createdTime = new Date();
Date expirationTime = new Date(System.currentTimeMillis() + expirationTimeInSecond * 1000);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdTime)
.setExpiration(expirationTime)
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}
}
JWT在项目中的应用
AuthController 认证服务
定义AuthController用于处理登录认证业务,代码如下:
package com.cy.jt.security.controller;
@RestController
public class AuthController {
@RequestMapping("/login")
public Map<String,Object> doLogin(String username,
String password){
Map<String,Object> map=new HashMap<>();
if("jack".equals(username)&&"123456".equals(password)){
map.put("state","200");
map.put("message","login ok");
Map<String,Object> claims=new HashMap<>();//负载信息
claims.put("username",username);
map.put("Authentication", JwtUtils.generatorToken(claims));
return map;
}else{
map.put("state","500");
map.put("message","login failure");
return map;
}
}
}
ResourceController 资源服务
定义一个资源服务对象,登录成功以后可以访问此对象中的方法,例如:
package com.cy.jt.security.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ResourceController {
@RequestMapping("/retrieve")
public String doRetrieve(){
//检查用户有没有登录
//执行业务查询操作
return "do retrieve resource success";
}
@RequestMapping("/update")
public String doUpdate(){
//检查用户有没有登录
//执行业务查询操作
return "do update resource success";
}
}
TokenInterceptor 拦截器及配置
假如在每个方法中都去校验用户身份的合法性,代码冗余会比较大,我们可以写一个Spring MVC 拦截器,
在拦截器中进行用户身份检测,例如:
package com.cy.jt.security.interceptor;
import com.cy.jt.security.util.JwtUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 令牌(token:ticker-通票)拦截器
* 其中,HandlerInterceptor为Spring MVC中的拦截器,
* 可以在Controller方法执行之前之后执行一些动作.
* 1)Handler 处理器(Spring MVC中将@RestController描述的类看成是处理器)
* 2)Interceptor 拦截器
*/
public class TokenInterceptor implements HandlerInterceptor {
/**
* preHandle在目标Controller方法执行之前执行
* @param handler 目标Controller对象
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
//http://localhost:8080/retrieve?Authentication=
//String token= request.getParameter("Authentication");
String token=request.getHeader("Authentication");
//判定请求中是否有令牌
if(token==null||"".equals(token))
throw new RuntimeException("please login");
//判定令牌是否已经过期
boolean flag=JwtUtils.isTokenExpired(token);
if(flag)
throw new RuntimeException("login timeout,please login");
return true;//true表示放行,false表示拦截到请求以后,不再继续传递
}
}
拦截器编写好以后,需要将拦截器添加到spring mvc执行链中并设置要拦截的请求,可通过配置类完成这个过程,代码如下:
package com.cy.jt.security.config;
/**
* 定义Spring Web MVC 配置类
*/
@Configuration
public class SpringWebConfig implements WebMvcConfigurer {
/**将拦截器添加到spring mvc的执行链中
* @param registry 此对象提供了一个list集合,可以将拦截器添加到集合中
* */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor())
//配置要拦截的url
.addPathPatterns("/retrieve","/update");
}
}
Postman访问测试
第一步:登录访问测试
第二步:进行资源访问测试,例如:
总结(Summary)
本章节重点讲解了JWT产生的背景,它的构成和项目中的基本应用,需要在实践中进行分析和理解.
重难点分析
- JWT 诞生的背景?(分布式架构应用平台下无状态会话时,规范令牌(通票)数据格式)
- JWT 规范定义的数据格式?(头,负载-详细内容,签名,思考一篇文章的构成,)
- JWT 规范下JAVA相关API的应用?(jjwt依赖-Jwts)
- 基于JWT规范下JAVA API 创建令牌,解析令牌
FAQ分析
- JWT 是什么?(一种规范的数据格式)
- JWT规范中的数据格式有几部分构成?(3部分,前两部分会进行Base64编码,最会基于签名算法加密)
- JWT的负载(Payload-存储实际用户信息的部分)部分可以自定义吗?(Claims)
- JWT令牌对象一般是在哪里创建?(服务端,可以创建令牌以后,响应到客户端)
- JWT令牌假如要存储在客户端你会存储在哪里?(Cookie,localStorage,sessionStorage)
- JWT令牌以怎样的方式有客户端传递到服务端?(请求参数,请求头)
Bug分析
- 创建token和解析token时一定要相同的密钥
- Token过期了