1.JWT简介
2.JWT的结构
3.基于服务器的传统身份认证
4.基于token的身份认证
5. JWT的优势
6.Java中使用JJWT实现JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JSON Web Token由三部分组成,它们之间用圆点(.
)连接。这三部分分别是:头部(header)
、载荷(payload)
(类似于货车的载荷就是货物)和签证(signature)
。因此,一个典型的JWT看起来是这个样子的:aaaaa.bbbbbb.cccccc
。对于每一部分:
header:
JWT的第一部分是header,它包含两部分信息:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。如:
{ 'typ': 'JWT', 'alg': 'HS256' }
然后,用Base64加密(该加密是可以对称解密的)对这个JSON进行编码就得到了JWT的第一部分。
payload:
JWT的第二部分是payload,它包含声明信息。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:registered, public 和 private。
- Registered claims : 注册声明有一组预定义的声明,它们不是强制的,但是推荐:
1.1iss (issuer)
:jwt签发者;
1.2sub (subject)
:jwt所面向的用户;
1.3aud (audience)
:接收jwt的一方;
1.4iat (issued at time)
: jwt的签发时间;
1.5exp (expiration time)
:jwt的过期时间,这个过期时间必须要大于签发时间;
1.6nbf (not before)
:定义在什么时间之前,该jwt都是不可用的;
1.7jti
: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。- Public claims : 可以随意定义。
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
对payload进行Base64编码就得到JWT的第二部分。注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。payload示例:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
signature:
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
Base64编码后的header
、Base64编码后的payload
和一个自定义的私人密钥secret
:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这个signature部分需要base64加密后的header和payload通过使用
.
连接组成的字符串,和通过header中声明的加密方式进行加盐secret
组合加密,构成了jwt的第三部分。
签名主要是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
注意:JWT的签发生成也是在服务器端的,secret的作用是进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。 对于上述的总结,这是JWT官网的一张图:
由于HTTP协议是无状态的,这就意味着如果我们已经通过账号密码进行认证后,在下一次请求的时候,服务器仍然不知道该请求是有谁发起的,就必须还得认证。
所以为了解决该问题,传统的方式是将已经认证过的用户信息存储在服务器上,也就是存储在Session对象中,并将这个Session的sessionId存储到Cookie中响应回浏览器。这样客户端就拿到了自己在服务器的认证信息,下次请求时就可以携带着这个sessionId,服务器就能知道该用户是已经认证过了的了。这就是传统的基于Session的认证、会话保持技术。
但是这种基于服务器的身份认证方式存在一些问题:
- 服务器负担增加:每次用户认证通过以后,服务器需要消耗内存来保存用户信息,随着认证通过的用户越来越多,服务器的在这里的开销和压力就会越来越大。
- 扩展局限 : 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
- CSRF : 因为是基于Cookie来进行用户识别的,Cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
由于上述Session+Cookie搭配存在的问题,所以后续提出了基于token的鉴权机制,token类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要再去考虑用户是在哪一台服务器登录了,这就为应用的扩展提供了便利,如同时服务器同时服务于PC浏览器和移动端App。
基于token的认证流程:
1.客户端带着用户名和密码请求服务器;
2.服务器对用户身份进行认证,通过认证后生成一个包含用户信息的token,作为用户的唯一凭证并返回给客户端;
3.客户端存储token,并在每次请求时在请求头header中携带上这个token值;
4.服务器验证token值,通过则完成请求。
注意:有时服务器需要设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *
。
通常情况下,token放在Authorization header
中,并用Bearer schema
。header应该看起来是这样的:Authorization: Bearer
:
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用Cookie。
Java中提供了JJWT库来实现对JWT的快速生成和便捷解析,首先导入依赖:
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
创建JWT工具类JwtUtils.java
:
public class JwtUtils {
/**
* 签名需要的私有密钥
*/
private static final String SECRET = "myScrect";
/**
* 重载createJwt,通过用户信息和生成Jwt
* @param user 用户信息
* @return token
*/
public static String createJwt(User user) {
return createJwt(user,-1);
}
/**
* 通过用户信息和过期时间生成Jwt
* @param user 用户信息
* @param ttlMillis 过期时间
* @return token
*/
public static String createJwt(User user, long ttlMillis) {
// 签名算法 HS256,即jjwt已经封装header中需要的算法名称
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成Jwt的时间,即签发时间
long nowMillis = System.currentTimeMillis();
//构建Jwt
JwtBuilder builder = Jwts.builder()
//jwt的唯一标识
.setId(String.valueOf(user.getId()))
//jwt面向用户
.setSubject(user.getTelephone())
//jwt的签发者
.setIssuer("Korbin")
//jwt的签发时间
.setIssuedAt(new Date(nowMillis))
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm,SECRET);
//自定义payload的claim信息
builder.claim("role", "admin");
// 设置过期时间,需要大于签发时间
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
builder.setExpiration(new Date(expMillis));
}
String token = builder.compact();
return token;
}
/**
* 解析jwt,解析时若过期会抛出ExpiredJwtException异常
* @param jwt token
* @return jwt对象
*/
public static Claims parseJwt(String jwt){
//解析jwt
JwtParser parser = Jwts.parser();
//获取解析后的对象
Claims claims = Jwts.parser()
//设置签名秘钥,和生成的签名的秘钥一模一样
.setSigningKey(SECRET)
//设置需要解析的jwt
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
测试运行:
public static void main(String[] args) {
User user = new User();
user.setId(10007L);
user.setTelephone("13511448855");
String token = JwtUtils.createJwt(user,1000*60);
System.out.println(token);
Claims claims = JwtUtils.parseJwt(token);
System.out.println("jwtId:"+claims.getId());
System.out.println("jwtSubject:"+claims.getSubject());
System.out.println("jwtIssuer:"+claims.getIssuer());
System.out.println("jwtIssuedAt:"+claims.getIssuedAt());
System.out.println("role:"+claims.get("role"));
System.out.println("expiration:"+claims.getExpiration());
}