一、介绍
1、简介
JWT无状态登录,是token的一种。全称json web token轻量级的授权和身份认证规范。跨平台存在。结构是 JSON,它是一个 JavaScript 对象表示法。它有各种部分,标头、有效负载和签名部分。这些都被打包起来,然后对 Base64 进行编码和签名,以便它是 URL 安全的。
2、与session相对比较
(1)有状态登录session:tomcat中,把用户的登录信息保存在服务端的session中,再给用户一个cookie值。记录对应的session值,下次客户端带cookie来访问时,再识别对应的session找到用户的信息。缺点:服务器的负荷变大,保存大量的session数据。
(2)无状态登录jwt:服务器不保存用户的登录信息,由客户端请求携带自己的身份信息(带入token来获取身份认证)。服务器不再保存用户的登录信息,多次请求不用访问回到同一台服务,服务器可以弹性伸缩,减小服务端压力,缺点一旦JWT签发,在有效期内这个token将会一直有效,而session有自己的会话周期。
3、流程:
- 第一次请求访问时候,由服务器对用户进行身份验证
- 验证通过,将身份信息加密进程token,返回给客户端,作为其访问其他服务器集群的身份凭证。
- 每次请求携带token,服务器对其进行解密判断有效性。
4、安全问题:
token必须安全可靠,采用JWT + RSA非对称加密,另外,JWT一旦签发,在有效期内将会一直有效,可以通过设置较短过期时间,或在业务接口再次做判断、再加入拦截等方法来减小安全风险。
(1)Base64编码方式是可逆的,也就是透过编码后发放的Token内容是可以被解析的。一般而言,我们都不建议在有效载荷内放敏感讯息,比如使用者的密码。
(2)JWT其中的一个组成内容为Signature,可以防止通过Base64可逆方法回推有效载荷内容并将其修改。因为Signature是经由Header跟Payload一起Base64组成的。
(3) Cookie 被窃取了第三方可以做 CSRF 攻击:Cookie丢失,就表示身份就可以被伪造。故官方建议的使用方式是存放在LocalStorage中,并放在请求头中发送。
5、失效问题
- 无状态JWT令牌(Stateless JWT Token)发放出去之后,不能通过服务器端让令牌失效,必须等到过期时间过才会失去效用。
- 假设在这之间Token被拦截,或者有权限管理身份的差异造成授权Scope修改,都不能阻止发出去的Token失效并要求使用者重新请求新的Token。
6、在线解析平台
二、结构
在紧凑的形式中,JWT包含三个由点(.)分隔的部分,它们分别是:header、payload、sign。
如我生成了一个jwt token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWItZGVtbyIsImlzcyI6Ind0eXkiLCJ1c2VybmFtZTEiOiJscyIsImV4cCI6MTcyMDE2NjY4NCwidXNlcm5hbWUiOiJscyJ9.QZpJ0mMjm7PmXFb682Xnp5KhaUp8gQ50g-rOU8nvYow
1、Header
1.1、介绍
第一部分是header。为base64编码,可解码,不要存放关键信息。如我这里解码得到:
或者通过jwt在线解析平台:
1.2、组成
Header通常由两部分组成:
(1)令牌的类型(即JWT);
(2)常用的加密算法,见
com.auth0.jwt.algorithms.Algorithm
2、Payload
第二部分是包含具体的身份信息和声明 (claims)的载荷。为base64编码,可解码。
放声明内容,就是存放沟通讯息的地方,在定义上有3种声明(Claims):
(1)Registered claims(注册声明):
这些是一组预先定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。#Registered Claim Names#
(2)Public claims(公开声明):
这些可以由使用JWT的人员随意定义。 但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或将其定义为包含防冲突命名空间的URI。
(3)Private claims(私有声明):
这些是为了同意使用它们但是既没有登记,也没有公开声明的各方之间共享信息,而创建的定制声明。
如我这里解码得到:
通过jwt在线解析平台:
3、Signature
3.1、介绍
签名,由前两部分加密形成,用于验证 Token 的完整性和真实性,不能base64解码。
如我使用一个sign通过base64解码不了
3.2、加密算法
Signature 不能 被解密。因为 Signature 并不是加密的结果,而是使用指定的加密算法(如 HMAC、RSA 等)基于 Header 和 Payload 计算出来的一个摘要或签名,相当于先加密再hash。Signature 的作用是用于验证 JWT 是否被篡改过,而不是用来获取原始数据或加密的数据。
计算方法:
Signature是这么算出来的?
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'your_secret');
说明:
1、header和payload先用base64加密用点链接起来,再用header里alg所指定的算法计算摘要
2、your_secret 就是秘钥,加密方和解密方共同知道的,解密的时候要用
如:使用HMAC SHA256算法,签名将按照以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
通过在线解析平台:
3.3、使用建议
不要存放敏感信息在Token里。
Payload中的exp时效不要设定太长。
开启Only Http预防XSS攻击。
如果担心重播攻击(replay attacks )可以增加jti(JWT ID),exp(有效时间) Claim。
在你的应用程序应用层中增加黑名单机制,必要的时候可以进行Block做阻挡(这是针对掉令牌被第三方使用窃取的手动防御)。
三、api操作
1、sign生成token
需要指定算法,算法又依赖了secret,所以需要保管好secret防止别人随意生成来攻击系统
JWT.create()
//iss:签发方
.withIssuer("wtyy")
//aud:接收jwt的一方
.withAudience("web-demo")
//exp:jwt的过期时间
//.withExpiresAt()
//其他自定义通信信息
.withClaim("username", userEntity.getUsername())
.withClaim("age",userEntity.getAge())
.withExpiresAt(date)
.sign(algorithm);
2、verify验证token
需要指定算法,这个算法和secret要和生成时保持一致。保管好secret,即使别人用同样的算法但是secret不一样也无法通过校验。
JWTVerifier verifier = JWT.require(algorithm).build();
//验证不通过会抛异常
verifier.verify(token);
3、获取payload
注意,只要是一个jwt,都可以获取payload。
为了防止攻击应该先验证是不是签发方生成的,即先verify再获取payload。
DecodedJWT jwt = JWT.decode(token);
Map<String, Claim> claims = jwt.getClaims();
//通信字段
String username = claims.get("username").asString();
int age = claims.get("age").asInt();
获取payload后还可以通过这些通信信息做进一步的校验,如是否过期issuer是否正确等等从而判断token是否合法。
四、实现
1、(token)jwt插件:
有两个,Auto0和jsonwebtoken都可以生成token
- 性能方面auth0更优。
- 功能方面jsonwebtoken支持更高级的应用,如高级加密算法等。
其中,(1)auth0的maven依赖为:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
(2)jsonwebtoken的maven依赖为:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2、性能对比:
(1)Auto0
工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import org.springframework.beans.factory.annotation.Value;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class Auth0Util {
/**
* 引入apollo的配置
*/
private static String TOKEN_KEY = "54f12048-1f56-45c1-a3f1-2c546bc2bb42";
/**
* jwt有效时间
*/
private static long TOKEN_TIMEOUT = 10* 60 * 1000;
/**
* jwt生成方
*/
private final static String JWT_ISSUER = "lb-fw-gw";
/**
* 生成jwt
*
* @param header
* @return
*/
public static String create(Map<String, Object> header) {
return create(header, new HashMap<>(0), JWT_ISSUER, TOKEN_TIMEOUT);
}
public static String create(Map<String, Object> header, Map<String, String> claims) {
return create(header, claims, JWT_ISSUER, TOKEN_TIMEOUT);
}
public static String create(Map<String, Object> header, long timeout) {
return create(header, new HashMap<>(0), JWT_ISSUER, timeout);
}
/**
* 生成jwt
*
* @param header
* @param issuer
* @return
*/
public static String create(Map<String, Object> header, Map<String, String> claims, String issuer, long timeout) {
String token;
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_KEY);
Date date = new Date(System.currentTimeMillis() + timeout);
JWTCreator.Builder builder = JWT.create()
.withHeader(header)
.withIssuer(issuer)
.withExpiresAt(date);
for (String key : claims.keySet()) {
builder.withClaim(key, claims.get(key));
}
token = builder.sign(algorithm);
} catch (JWTVerificationException exception) {
System.out.println(exception.getMessage());
return null;
}
return token;
}
/**
* 验证jwt
*
* @param token
* @return
*/
public static DecodedJWT decode(String token) {
DecodedJWT jwt;
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_KEY);
JWTVerifier verifier = JWT.require(algorithm)
// .withIssuer(JWT_ISSUER)
// .acceptLeeway(1)
// .acceptExpiresAt(1)
// .acceptIssuedAt(1)
// .acceptNotBefore(1)
.build();
jwt = verifier.verify(token);
} catch (JWTVerificationException exception) {
System.out.println(exception.getMessage());
return null;
}
return jwt;
}
}
单测:
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
class Auth0UtilTest {
/**
* jwt创建测试结果
* 次数 : 3批耗时ms
* 1000 : 1502 1673 1513
* 10000 : 2353 2319 2311
* 100000 : 5681 5604 5452
*/
@Test
void create() {
Map<String, Object> header = new HashMap<>(4);
header.put("auth", "level1");
Map<String, String> claims = new HashMap<>(4);
claims.put("user", "xxxs");
long btime = System.currentTimeMillis();
int times = 100000;
for (int i = 1; i <= times; i++) {
Auth0Util.create(header);
}
System.out.println(System.currentTimeMillis() - btime);
}
/**
* jwt校验测试结果
* 次数 : 3批耗时ms
* 1000 : 898 927 939
* 10000 : 1973 1992 1882
* 100000 : 4402 4562 4649
*/
@Test
void decode() {
Map<String, Object> header = new HashMap<>(4);
header.put("auth", "level1");
Map<String, String> claims = new HashMap<>(4);
claims.put("user", "xxxs");
String token = Auth0Util.create(header); //创建一个新的token
System.out.println("token:" + token);
int times = 100000; //循环次数
long btime = System.currentTimeMillis();
for (int i = 1; i <= times; i++) {
Auth0Util.decode(token);
}
System.out.println(System.currentTimeMillis() - btime);
}
}
(2)jsonwebtoken
工具类
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @description: jsonwebtoken工具类
*/
public class JsonWebTokenUtil {
/**
* jwt密钥 必须大于32个字符
*/
private static String TOKEN_KEY = "54f12048-1f56-45c1-a3f1-2c546bc2bb42";
/**
* jwt有效时间
*/
private static long TOKEN_TIMEOUT = 60 * 30 * 1000;
/**
* jwt生成方
*/
private final static String JWT_ISSUER = "lb-fw-gw";
/**
* 生成jwt
*
* @param header
* @return
*/
public static String create(Map<String, Object> header) {
return create(header, new HashMap<>(2), JWT_ISSUER, TOKEN_TIMEOUT);
}
public static String create(Map<String, Object> header, Map<String, Object> claims) {
return create(header, claims, JWT_ISSUER, TOKEN_TIMEOUT);
}
public static String create(Map<String, Object> header, long timeout) {
return create(header, new HashMap<>(2), JWT_ISSUER, timeout);
}
public static String create(Map<String, Object> header, Map<String, Object> claims, long timeout) {
return create(header, claims, JWT_ISSUER, timeout);
}
/**
* 生成jwt
*
* @param header
* @return
*/
public static String create(Map<String, Object> header, Map<String, Object> claims, String issuer, long timeout) {
String token;
try {
Date date = new Date(System.currentTimeMillis() + timeout);
SecretKey key = Keys.hmacShaKeyFor(TOKEN_KEY.getBytes());
token = Jwts.builder()
.setHeader(header)
.setClaims(claims)
.setIssuer(issuer)
.setExpiration(date)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
} catch (JwtException exception) {
System.out.println(exception.getMessage());
return null;
}
return token;
}
/**
* 验证jwt
*
* @param token
* @return
*/
public static Jws<Claims> decode(String token) {
Jws<Claims> claimsJws;
try {
Key key = new SecretKeySpec(TOKEN_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName());
claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
} catch (JwtException exception) {
System.out.println(exception.getMessage());
return null;
}
return claimsJws;
}
}
单测:
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
class JsonWebTokenUtilTest {
/**
* jwt创建测试结果
* 次数 : 3批耗时ms
* 1000 : 2037 1850 2052
* 10000 : 4874 4833 4908
* 100000 : 18598 17841 17942
*/
@Test
void create() {
HashMap<String, Object> header = new HashMap<>(4);
header.put("auth", "level1");
Map<String, Object> claims = new HashMap<>(4);
claims.put("user", "xxxs");
long btime = System.currentTimeMillis();
int times = 100000;
for (int i = 1; i <= times; i++) {
JsonWebTokenUtil.create(header,claims);
}
System.out.println(System.currentTimeMillis() - btime);
}
/**
* jwt校验测试结果
* 次数 : 3批耗时ms
* 1000 : 2128 1980 1933
* 10000 : 9763 10073 9494
* 100000 : 47531 48893 48845
*/
@Test
void decode() {
HashMap<String, Object> header = new HashMap<>(4);
header.put("auth", "level1");
Map<String, Object> claims = new HashMap<>(4);
claims.put("user", "xxxs");
String token = JsonWebTokenUtil.create(header,claims); //创建一个新的token
int times = 100000;
long btime = System.currentTimeMillis();
for (int i = 1; i <= times; i++) {
JsonWebTokenUtil.decode(token);
}
System.out.println(System.currentTimeMillis() - btime);
}
}
3、demo
(1)pom:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
(2)util:
package com.demo.security.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.demo.security.constant.UserConstants;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
public class JwtUtil {
/**
* 校验token是否正确
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getUsername( HttpServletRequest req) {
try {
String token = req.getHeader("token");
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static String getUsername( String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成签名,5min(分钟)后过期
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + 30 * 60 * 1000);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
//iss:签发方
.withIssuer("wtyy")
//aud:接收jwt的一方
.withAudience("web-demo")
//exp:jwt的过期时间,这个过期时间必须要大于签发时间
//.withExpiresAt()
//其他自定义通信信息
.withClaim("username", username)
.withClaim("username1",username)
.withExpiresAt(date)
.sign(algorithm);
}
}
(3)测试:
如我使用sign生成了一个token,getUsername解析token获取其中的userName字段: