- jwt(java web token)简介
它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
组成部分:
Header 头部:一般为token类型和加密算法
Payload 负载:一些用户信息和额外的声明数据
Signature 签名:签名需要使用编码后的header和payload以及一个秘钥 (很安全),前两段的结合加密
JWT简介参考
jwt的工作流程
下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)
1、用户导航到登录页,输入用户名、密码,进行登录
2、服务器验证登录鉴权,如果该用户合法,根据用户的信息和服务器的规则生成JWT Token
3、服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
4、用户得到token,存在localStorage、cookie或其它数据存储形式中。
5、以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
6、服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
7、用户取得结果
为了更好的理解这个token是什么,我们先来看一个token生成后的样子,下面那坨乱糟糟的就是了。
格式为三部分(头,负载,签名)
xxxxx.yyyyyyy.zzzzz
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg
但仔细看到的话还是可以看到这个token分成了三部分,每部分用 . 分隔,每段都是用 Base64 编码的。如果我们用一个Base64的解码器的话 ( https://www.base64decode.org/ ),可以看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了:
{
"alg":"HS512"
}
这是告诉我们HMAC采用HS512算法对JWT进行的签名。(签名算法)
第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解码之后是
{
"sub":"wang",
"created":1489079981393,
"exp":1489684781
}
这段告诉我们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, created 和 exp。在我们这个例子中,分别代表着用户名、创建时间和过期时间,当然你可以把任意数据声明在这里。(要签名的内容)
看到这里,你可能会想这是个什么鬼token,所有信息都透明啊,安全怎么保障?别急,我们看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg。同样使用Base64解码之后,咦,这是什么鬼东西?
D X �DmYTeȧL�UZcPZ0$gZAY�_7�wY@
最后一段其实是签名(最终的签名,也就是密文),这个签名必须知道秘钥才能计算。这个也是JWT的安全保障。这里提一点注意事项,由于数据声明(Claim)是公开的,千万不要把密码等敏感字段放进去,否则就等于是公开给别人了。
也就是说JWT是由三段组成的,按官方的叫法分别是header(头)、payload(负载)和signature(签名):
header.payload.signature
头中的数据通常包含两部分:一个是我们刚刚看到的 alg,这个词是 algorithm 的缩写,就是指明算法。另一个可以添加的字段是token的类型(按RFC 7519实现的token机制不只JWT一种),但如果我们采用的是JWT的话,指定这个就多余了。
{
"alg": "HS512",
"typ": "JWT"
}
payload中可以放置三类数据:系统保留的、公共的和私有的:
1、系统保留的声明(Reserved claims):这类声明不是必须的,但是是建议使用的,包括:iss (签发者), exp (过期时间),sub (主题), aud (目标受众)等。这里我们发现都用的缩写的三个字符,这是由于JWT的目标就是尽可能小巧。
2、公共声明:这类声明需要在 IANA JSON Web Token Registry 中定义或者提供一个URI,因为要避免重名等冲突。
3、私有声明:这个就是你根据业务需要自己定义的数据了。
签名的过程是这样的:采用header中声明的算法,接受三个参数:base64编码的header、base64编码的payload和秘钥(secret)进行运算。签名这一部分如果你愿意的话,可以采用RSASHA256的方式进行公钥、私钥对的方式进行,如果安全性要求的高的话。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode 是个什么鬼?百度一下
JWT的生成和解析
为了简化我们的工作,这里引入一个比较成熟的JWT类库,叫 jjwt ( https://github.com/jwtk/jjwt )。这个类库可以用于Java和Android的JWT token的生成和验证。
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
JWT的生成可以使用下面这样的代码完成:
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret) //采用什么算法是可以自己选择的,不一定非要采用HS512
.compact();
}
数据声明(Claim)其实就是一个Map,比如我们想放入用户名,可以简单的创建一个Map然后put进去就可以了。
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username());
解析也很简单,利用 jjwt 提供的parser传入秘钥,然后就可以解析token了。
Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
JWT本身没啥难度,但安全整体是一个比较复杂的事情,JWT只不过提供了一种基于token的请求验证机制。但我们的用户权限,对于API的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。所以我们这里要利用Spring的一个子项目Spring Security来简化我们的工作。
- pom配置jwt
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
package birunet.jwt.util;
import java.security.Key;
import java.util.Date;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* jwt 加密解密工具类
*
*
*/
public class JwtUtils {
// 解析 JWT
public static Claims paraJWT(String jsonWebToken, String base64Security) {
Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
.parseClaimsJws(jsonWebToken).getBody();
if (claims != null) {
return claims;
}
return null;
}
// 前三个参数为自己用户token的一些信息比如id,权限,名称等。
// 不要将隐私信息放入(大家都可以获取到)
/**
* 其实生成的token 就是一个字符串
* @param name
* @param userId
* @param role
* @param audience
* @param issuer
* @param TTLMillis
* @param base64Security
* @return
*/
public static String createJWT(String name, String userId, String role, String audience, String issuer,
long TTLMillis, String base64Security) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 生成签名密钥 就是一个base64加密后的字符串?
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
JSONObject jsonObject = new JSONObject();
jsonObject.put("userName", name);
jsonObject.put("userLoginName", userId);
// 添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT").setIssuedAt(now) // 创建时间
.setSubject(jsonObject.toString()) // 主题,也差不多是个人的一些信息
.setIssuer(issuer) // 发送谁
.setAudience(audience) // 个人签名
.signWith(signatureAlgorithm, signingKey); // 估计是第三段密钥
// 添加Token过期时间
if (TTLMillis >= 0) {
// 过期时间
long expMillis = nowMillis + TTLMillis;
// 现在是什么时间
Date exp = new Date(expMillis);
// 系统时间之前的token都是不可以被承认的
builder.setExpiration(exp).setNotBefore(now);
}
// 生成JWT
return builder.compact();
}
}