JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:
https://github.com/jwtk/jjwt
创建token
(1)新建项目中的pom.xml中添加依赖:
io.jsonwebtoken
jjwt
0.9.0
(2)创建测试类,代码如下
/* *
* @Description json web token 签发
* @param id 令牌ID
* @param subject 用户标识
* @param userId
* @param issuer 签发人
* @param period 有效时间(秒)
* @param roles 访问主张-角色
* @param permissions 访问主张-权限
* @param algorithm 加密算法
* @Return java.lang.String
*/
public static String issueJWT(String id,String subject, String userId, String issuer, Long period,
String roles, String permissions, SignatureAlgorithm algorithm) {
// 当前时间戳
Long currentTimeMillis = System.currentTimeMillis();
// 秘钥
byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
JwtBuilder jwtBuilder = Jwts.builder();
if (StringUtil.isNotBlank(id)) {
jwtBuilder.setId(id);
}
if (StringUtil.isNotBlank(subject)) {
jwtBuilder.setSubject(subject);
}
if (StringUtil.isNotBlank(issuer)) {
jwtBuilder.setIssuer(issuer);
}
// 设置签发时间
jwtBuilder.setIssuedAt(new Date(currentTimeMillis));
// 设置到期时间
if (null != period) {
jwtBuilder.setExpiration(new Date(currentTimeMillis + period*1000));
}
if (StringUtil.isNotBlank(userId)) {
jwtBuilder.claim("userId",userId);
}
if (StringUtil.isNotBlank(roles)) {
jwtBuilder.claim("roles",roles);
}
if (StringUtil.isNotBlank(permissions)) {
jwtBuilder.claim("perms",permissions);
}
// 压缩,可选GZIP
jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
// 加密设置
jwtBuilder.signWith(algorithm,secreKeyBytes);
return jwtBuilder.compact();
}
运行打印结果:
jwt:eyJhbGciOiJIUzUxMiIsInppcCI6IkRFRiJ9.eNosyz0LwjAUheH_cucGbpqbj3YTdNBBoQ6KiyRpqlW02rQiiP_dFFzf85wPXIYWSvDeSNTomMqFYCRqx4xUhiFpawuNWnEBGcTRJdykJUdvDXcWKSdPyJ2qmxCkREU2wTbGBIfuGu4shv4V-inaAUoujTSoSRcZhPfjHzipKYzJLuv07J7V8aRWtNC2343nYr8x6_ktbA9UzeD7AwAA__8.764bh6E-PDmLLerSrveC2uwsSWYnGKArzwy-5F0LBfSCWfNj5z6lCvKFUMLItxygHONkz1qNDNh6QPC-qMibmg
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
/**
* 验签JWT
*
* @param jwt json web token
*/
public static Token parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, IllegalArgumentException {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(appKey))
.parseClaimsJws(jwt)
.getBody();
Token jwtAccount = new Token();
//令牌ID
jwtAccount.setTokenId(claims.getId());
//客户标识
String subject = claims.getSubject();
jwtAccount.setSubject(subject);
//userId
jwtAccount.setUserId(claims.get("userId", String.class));
//签发者
jwtAccount.setIssuer(claims.getIssuer());
//签发时间
jwtAccount.setIssuedAt(claims.getIssuedAt());
//接收方
jwtAccount.setAudience(claims.getAudience());
//访问主张-角色
jwtAccount.setRoles(claims.get("roles", String.class));
//访问主张-权限
jwtAccount.setPerms(claims.get("perms", String.class));
return jwtAccount;
}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.。
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。
(1)创建token 并设置过期时间
long now=System.currentTimeMillis();
long exp=now+1000*30;//30秒过期
JwtBuilder jwtBuilder = Jwts.builder().setId( "888" )
.setSubject( "小白" )
.setIssuedAt( new Date() )//签发时间
.setExpiration( new Date( exp ) )//过期时间
.signWith( SignatureAlgorithm.HS256, "hahaha" );
String token = jwtBuilder.compact();
System.out.println(token);
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
long now=System.currentTimeMillis();
long exp=now+1000*30;//30秒过期
JwtBuilder jwtBuilder = Jwts.builder().setId( "888" )
.setSubject( "小白" )
.setIssuedAt( new Date() )//签发时间
.setExpiration( new Date( exp ) )//过期时间
.claim( "roles","admin" )
.signWith( SignatureAlgorithm.HS256, "hahaha" );
String token = jwtBuilder.compact();
System.out.println(token);
运行打印效果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI
解析TOKEN:
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIyNTM3NTQsImV4cCI6MTU2MjI1Mzc4Mywicm9sZXMiOiJhZG1pbiJ9.CY6CMembCi3mAkBHS3ivzB5w9uvtZim1HkizRu2gWaI";
Claims claims = Jwts.parser().setSigningKey( "hahaha" ).parseClaimsJws( token ).getBody();
System.out.println(claims);
System.out.println(claims.get( "roles" ));
附录完整的JWT工具类:
package com.inspur.tax.rest.token;
import java.io.IOException;
import java.security.SignatureException;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import javax.xml.bind.DatatypeConverter;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.inspur.tax.api.common.entity.TransEntity;
import com.inspur.tax.common.Const;
import com.inspur.tax.common.Result;
import com.inspur.tax.exception.BaseException;
import com.inspur.tax.rest.token.entity.Token;
import com.inspur.tax.utils.StringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionCodecResolver;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.DefaultHeader;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import io.jsonwebtoken.impl.TextCodec;
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
public class JwtUtil {
//秘钥
public static final String SECRET_KEY = Const.TOKEN_SECRET;
private static final ObjectMapper MAPPER = new ObjectMapper();
private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver();
//私有化构造
private JwtUtil() {
}
/* *
* @Description json web token 签发
* @param id 令牌ID
* @param subject 用户标识
* @param userId
* @param issuer 签发人
* @param period 有效时间(秒)
* @param roles 访问主张-角色
* @param permissions 访问主张-权限
* @param algorithm 加密算法
* @Return java.lang.String
*/
public static String issueJWT(String id,String subject, String userId, String issuer, Long period,
String roles, String permissions, SignatureAlgorithm algorithm) {
// 当前时间戳
Long currentTimeMillis = System.currentTimeMillis();
// 秘钥
byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
JwtBuilder jwtBuilder = Jwts.builder();
if (StringUtil.isNotBlank(id)) {
jwtBuilder.setId(id);
}
if (StringUtil.isNotBlank(subject)) {
jwtBuilder.setSubject(subject);
}
if (StringUtil.isNotBlank(issuer)) {
jwtBuilder.setIssuer(issuer);
}
// 设置签发时间
jwtBuilder.setIssuedAt(new Date(currentTimeMillis));
// 设置到期时间
if (null != period) {
jwtBuilder.setExpiration(new Date(currentTimeMillis + period*1000));
}
if (StringUtil.isNotBlank(userId)) {
jwtBuilder.claim("userId",userId);
}
if (StringUtil.isNotBlank(roles)) {
jwtBuilder.claim("roles",roles);
}
if (StringUtil.isNotBlank(permissions)) {
jwtBuilder.claim("perms",permissions);
}
// 压缩,可选GZIP
jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
// 加密设置
jwtBuilder.signWith(algorithm,secreKeyBytes);
return jwtBuilder.compact();
}
/* *
* @Description json web token 签发
* @param subject 用户标识
* @param userId
* @param period 有效时间(秒)
* @Return java.lang.String
*/
public static String issueJWT(String subject, String userId,Long period) {
return issueJWT(UUID.randomUUID().toString(), subject, userId, "token-server", period, "", null, SignatureAlgorithm.HS512);
}
/**
* 解析JWT的Payload
*/
public static String parseJwtPayload(String jwt){
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
String base64UrlEncodedHeader = null;
String base64UrlEncodedPayload = null;
String base64UrlEncodedDigest = null;
int delimiterCount = 0;
StringBuilder sb = new StringBuilder(128);
for (char c : jwt.toCharArray()) {
if (c == '.') {
CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb);
String token = tokenSeq!=null?tokenSeq.toString():null;
if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}
delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
if (delimiterCount != 2) {
String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
throw new MalformedJwtException(msg);
}
if (sb.length() > 0) {
base64UrlEncodedDigest = sb.toString();
}
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header =================
Header header = null;
CompressionCodec compressionCodec = null;
if (base64UrlEncodedHeader != null) {
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
Map m = readValue(origValue);
if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}
compressionCodec = codecResolver.resolveCompressionCodec(header);
}
// =============== Body =================
String payload;
if (compressionCodec != null) {
byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8);
} else {
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
}
return payload;
}
/**
* 验签JWT
*
* @param jwt json web token
*/
public static Token parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, IllegalArgumentException {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(appKey))
.parseClaimsJws(jwt)
.getBody();
Token jwtAccount = new Token();
//令牌ID
jwtAccount.setTokenId(claims.getId());
//客户标识
String subject = claims.getSubject();
jwtAccount.setSubject(subject);
//userId
jwtAccount.setUserId(claims.get("userId", String.class));
//签发者
jwtAccount.setIssuer(claims.getIssuer());
//签发时间
jwtAccount.setIssuedAt(claims.getIssuedAt());
//接收方
jwtAccount.setAudience(claims.getAudience());
//访问主张-角色
jwtAccount.setRoles(claims.get("roles", String.class));
//访问主张-权限
jwtAccount.setPerms(claims.get("perms", String.class));
return jwtAccount;
}
public static Map readValue(String val) {
try {
return MAPPER.readValue(val, Map.class);
} catch (IOException e) {
throw new MalformedJwtException("Unable to userpager JSON value: " + val, e);
}
}
/**
* @Title auth
* @Description token验证
* @param @param trans
* @param @return
* @return Token
*/
public static Token auth(TransEntity> trans) {
Token token = null;
try {
token = JwtUtil.parseJwt(trans.getComment().getToken(), JwtUtil.SECRET_KEY);
} catch (NullPointerException e) {
throw new BaseException(Result.COMMON_AUTH_EMPTY_ERROR);
} catch (CompressionException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
throw new BaseException(Result.COMMON_AUTH_ERROR);
} catch (ExpiredJwtException e) {
throw new BaseException(Result.COMMON_AUTH_EXPIRE_ERROR);
} catch (SignatureException e) {
throw new BaseException(Result.COMMON_AUTH_SIGN_ERROR);
}
if (null == token) {
throw new BaseException(Result.COMMON_AUTH_ERROR);
}
return token;
}
public static void main(String[] args) throws Exception{
String subject = "10115117000067865480";
String jwt = JwtUtil.issueJWT("f56820ca81ba0424c401b6dfee55064a", "oqR_g6J4E7arWuh9XO8NDmeSZ4RA",7200L);
System.out.println("jwt:"+jwt);
System.out.println("----------------------------------------");
// Thread.sleep(1000);
String ss = JwtUtil.parseJwtPayload(jwt);
System.out.println("ss:"+ss);
Token ooo = JwtUtil.parseJwt(jwt,SECRET_KEY);
System.out.println("ooo:"+ooo);
}
}