JWT详细介绍
一、what
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
二、when
下列场景中使用JSON Web Token是很有用的:
- Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。
- 单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
- Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
三、结构
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
- Header
- Payload
- Signature
因此,一个典型的JWT看起来是这个样子的:
xxxxx.yyyyy.zzzzz
四、Header
Header header典型的由两部分组成:
- token的类型(“JWT”)
- 算法名称(比如:HMAC SHA256或者RSA等等)
例如:
{
'alg': "HS256",
'typ': "JWT"
}
然后,用Base64对这个JSON编码就得到JWT的第一部分Header。
五、Payload
Payload JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered
, public
和 private
。
- Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
- Public claims : 可以随意定义。
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
例如:
{
"sub": '1234567890',
"name": 'john',
"admin":true
}
Reserved claims(保留),它的含义就像是编程语言的保留字一样,属于JWT标准里面规定的一些claim。JWT标准里面定好的claim有:
- iss(Issuser):代表这个JWT的签发主体;
- sub(Subject):代表这个JWT的主体,即它的所有人;
- aud(Audience):代表这个JWT的接收对象;
- exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
- nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
- iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
- jti(JWT ID):是JWT的唯一标识。
对payload进行Base64编码就得到JWT的第二部分
注意:不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
六、Signature
为了得到签名部分,你必须有:
- 编码过的header
- 编码过的payload
- 一个秘钥
签名算法是header中指定的那个,然后它们签名即可。
如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
七、JWT用户认证流程
基于Token的身份认证是如何工作的 基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
- 用户携带用户名和密码请求访问
- 服务器校验用户凭据
- 应用提供一个token给客户端
- 客户端存储token,并且在随后的每一次请求中都带着它
- 服务器校验token并返回数据
注意:
- 每一次请求都需要token
- Token应该放在请求header中
- 我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin:
八、使用Token的好处
1. 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
2. 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!
3. token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。
九、与OAuth2的区别
- OAuth2是一种授权框架 ,JWT是一种认证协议
- 无论使用哪种方式切记用HTTPS来保证数据的安全性
- OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。
十、JWT的框架JJWT
1. JJWT是什么
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
2. JJWT的好处
- JJWT的目标是最容易使用和理解用于在JVM上创建和验证JSON Web令牌(JWTs)的库。
- JJWT是基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。
- JJWT还添加了一些不属于规范的便利扩展,比如JWT压缩和索赔强制。
3. 规范兼容
-
创建和解析明文压缩JWTs
-
创建、解析和验证所有标准JWS算法的数字签名紧凑JWTs(又称JWSs):
-
HS256: HMAC using SHA-256
-
HS384: HMAC using SHA-384
-
HS512: HMAC using SHA-512
-
RS256: RSASSA-PKCS-v1_5 using SHA-256
-
RS384: RSASSA-PKCS-v1_5 using SHA-384
-
RS512: RSASSA-PKCS-v1_5 using SHA-512
-
PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
-
PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
-
PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
4. JWTUtil
/**
* 解析jwt
*/
public static Claims parseJWT(String jsonWebToken) {
try {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(base64Secret))
.parseClaimsJws(jsonWebToken).getBody();
return claims;
} catch (Exception ex) {
// ex.printStackTrace();
return null;
}
}
/**
* 构建jwt,将参数信息加密
*/
public static String createJWT(TokenInfoModel tokenInfoModel) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Secret);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//Token头部信息
Map<String, Object> map = new HashMap<>();
map.put("typ", "JWT");
map.put("alg", "HS256");
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeader(map)
.claim("accountId", tokenInfoModel.getAccountId())//用户ID
.claim("roleIds", tokenInfoModel.getRoleIds())//角色
.claim("username", tokenInfoModel.getUsername())//用户名
.setIssuer(issuer)//JWT签发者
.signWith(signatureAlgorithm, signingKey);
//添加Token过期时间
if (expiresSecond >= 0) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
long expMillis = nowMillis + expiresSecond;
Date exp = new Date(expMillis);
builder.setExpiration(exp).setNotBefore(now);
}
//生成JWT
return builder.compact();
}
/**
* 获取头文件中的Token,根据Token取得用户信息,返回model
*
* @param Token
* @return
*/
public static TokenInfoModel geTokenInfo(String Token) {
//String Token= headers.get("authorization").get(0).substring(7);
Claims claims = JwtUtil.parseJWT(Token);
TokenInfoModel tokenInfoModel = new TokenInfoModel();
try {
if (claims != null) {
tokenInfoModel.setAccountId(Long.valueOf(String.valueOf(claims.get("accountId"))));
List<Long> roleIds = new ArrayList<>();
for (Integer o : (List<Integer>) claims.get("roleIds")) {
roleIds.add(Long.valueOf(o));
}
tokenInfoModel.setRoleIds(roleIds);
tokenInfoModel.setUsername((String)claims.get("username"));
}
}catch (Exception e){
tokenInfoModel = new TokenInfoModel();
}
return tokenInfoModel;
}
public static String eccrypt(String info) throws NoSuchAlgorithmException{
MessageDigest messageDigest;
String encodeStr = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(info.getBytes("UTF-8"));
encodeStr = byte2Hex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodeStr;
}
/**
* 将byte转为16进制
* @param bytes
* @return
*/
private static String byte2Hex(byte[] bytes){
StringBuffer stringBuffer = new StringBuffer();
String temp = null;
for (int i=0;i<bytes.length;i++){
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length()==1){
//1得到一位的进行补0操作
stringBuffer.append("0");
}
stringBuffer.append(temp);
}
return stringBuffer.toString();
}
5. JWTFilter
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(
Arrays.asList(
"/api/auth/login",
)));
/**
* Reserved claims(保留),它的含义就像是编程语言的保留字一样,属于JWT标准里面规定的一些claim。JWT标准里面定好的claim有:
* <p>
* iss(Issuser):代表这个JWT的签发主体;
* sub(Subject):代表这个JWT的主体,即它的所有人;
* aud(Audience):代表这个JWT的接收对象;
* exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
* nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
* iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
* jti(JWT ID):是JWT的唯一标识。
*/
@Override
public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
boolean allowedPath = ALLOWED_PATHS.contains(path);
logger.info(path);
if (allowedPath) {
chain.doFilter(req, res);
} else {
//处理token过滤
//等到请求头信息authorization信息
final String token = request.getHeader("authorization");
PrintWriter out = null;
try {
final Claims claims = JwtUtil.parseJWT(token);
if (claims == null) {
response.setStatus(HttpServletResponse.SC_OK);
JSONObject responseJSONObject = new JSONObject();
responseJSONObject.put("error_code","401");
responseJSONObject.put("error_msg","身份验证已过期");
out = response.getWriter();
out.append(responseJSONObject.toString());
return ;
}
} catch (final Exception e) {
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_OK);
JSONObject responseJSONObject = new JSONObject();
responseJSONObject.put("error_code","401");
responseJSONObject.put("error_msg","身份验证异常");
out = response.getWriter();
out.append(responseJSONObject.toString());
return ;
}
System.out.println("123123"+request.getRequestURI());
TokenInfoModel tokenInfo = JwtUtil.geTokenInfo(token);
// operateLogService.save(path,tokenInfo);
//将请求转发给过滤器链上下一个对象
chain.doFilter(req, res);
}
}