一.什么是JWT:
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,以JSON对象的形式在各方之间安全地传输信息。由于该信息是数字签名的,因此可以验证和信任它。JWTs可以使用秘钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
虽然JWTs也可以加密以提供各方之间的机密性,但是我们将重点讨论签名令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌可以向其他方隐藏这些声明。当令牌使用公钥/私钥对签名时,签名还证明只有持有私钥的一方才是签名的一方。
优点:是在分布式系统中,很好地解决了单点登录问题,很容易解决了session共享的问题。
缺点:是无法作废已颁布的令牌/不易应对数据过期。
二.使用JWT的场景:
1.Authorization(授权):
这是使用JWT最常见的场景。一旦用户登录,随后的每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是目前广泛使用JWT的一个特性,因为它的开销小,而且能够跨域访问(这是cookie做不到的)。
具体思路如下:
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。此后,客户端将在与服务器交互中都会在请求头中携带JWT,服务端(通常是拦截器或过滤器)使用自己保存的key计算、验证签名以判断该JWT是否可信。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization(当然该字段可以自定义)字段中。当跨域时,也可以将JWT被放置于POST请求的数据主体中。
2.Information Exchange(信息交换):
JWT是在各方之间安全传输信息的好方法。因为JWTs可以签名——例如,使用公钥/私钥对——所以你可以确保发送方是他们所说的人。此外,由于签名是使用header和payload计算的,您还可以验证内容有没有被篡改。善用JWT有助于减少服务器请求数据库的次数。
三.JWT的结构:
一个token分3部分,按顺序为
- Header
- Payload
- Signature
3部分之间用“.
”号做分隔。例如
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBUFAiLCJ1c2VyX2lkIjoiMTEiLCJpc3MiOiJTZXJ2aWNlIiwiZXhwIjoxNTQ1Mjc1NzAxLCJpYXQiOjE1NDQ0MTE3MDF9.8pekdPk2ORBFem2D7VD3g0mhWduNQA2vNyNDG_rWuBw
Header(头部)
标头通常由两部分组成:令牌的类型和正在使用的散列算法(如HMAC SHA256或RSA)。如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,将这个JSON编码为Base64Url,形成JWT的第一部分。
Payload(有效载荷)
令牌的第二部分是有效载荷,主要包含实体(通常是用户)和其他数据的声明。声明有三种类型:registered, public, and private 。
1.Registered claims(注册声明):
详细可以参考:https://tools.ietf.org/html/rfc7519#section-4.1
这些是一组预定义的声明,它们不是强制使用的,而是推荐使用的,以提供一组有用的、可互操作的声明。其中包括:iss(issuer 发行者)、exp(expiration time 过期时间)、sub(subject 主体)、aud(audience 受众)等7个,如下。
iss: jwt签发者
sub: jwt所面向的用户,可以用来存放用户id
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
注意,只要JWT要求是紧凑的,声明的名称就能是三个字符(即三个字母)长。
2.Public claims(公开声明):
这些可以由使用JWTs的用户随意定义。但是为了避免冲突,应该在IANA JSON Web Token Registry表中定义它们,或者将它们定义为包含抗冲突namespace的URI。
3.Private claims(私有声明):
这些是自定义声明,用于在同意使用这些声明的双方之间共享信息,这些既不是注册声明,也不是公共声明。
一个有效载荷示例可以是:
{
"sub": "1234567890",//主体
"name": "John Doe",
"admin": true
}
然后对payload进行Base64Url编码,形成JWT的第二部分。
请注意,对于已签名的令牌,该信息虽然受到保护,不受篡改,但任何人都可以读取(读取后通过base64解码即可得到明文)。除非经过加密,否则不要将秘密信息放在JWT的payload或header中。
Signature(签名)
要创建签名部分,您必须获取编码的头、编码的有效载荷、秘钥(secret)、头中指定的算法,并对其签名。
例如,如果您想使用HMAC SHA256算法,签名将以以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
signature 用于验证消息在此过程中没有更改,并且,对于使用私钥签名的令牌,它还可以验证JWT的发件人就是它说的那个人。
更具体的理解:未签名的令牌由base64url
编码的header和payload拼接而成(使用"."分隔),签名则通过私有的key计算而成,最后在未签名的令牌尾部拼接上base64url
编码的签名(同样使用"."分隔)就是完整的JWT了:
key = 'secretkey' //秘钥
unsignedToken = base64UrlEncode(header) + '.' + base64UrlEncode(payload) //未签名token
signature = HMACSHA256(unsignedToken,key) //签名
//完整的token
token = base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + base64UrlEncode(signature)
最后,JWT是由被"."分割的三个Base64-URL字符串组成,该字符串可以在HTML和HTTP环境中轻松传递,同时与基于xml的标准(如SAML)相比更紧凑。
如果您想使用JWT并将这些概念付诸实践,可以使用jwt.io Debugger来解码、验证和生成JWT。
Base64URL算法
该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法,很简单把。
具体可以查看我发的一篇文章:base64原理与运用
四.JWT的工作原理:
在身份验证中,当用户成功地使用其用户名/密码登录时,将返回JWT。由于JWT是凭据,因此必须非常小心地防止安全问题。一般来说,不要保存超过所需时间的令牌。
当用户希望访问受保护的路由或资源时,用户代应该在请求中携带JWT,通常在请求头中携带,建议命名为Authorization,比如
Authorization: jwt字符串
在某些情况下,这可能是一种无状态授权机制。服务器检查(通常自己定义一个拦截器/过滤器检查)Authorization
header请求头中是否存在有效的JWT,如果存在,则允许用户访问受保护的资源。如果JWT包含必要的数据,还可以减少对数据库的查询。如果令牌在请求头中发送,则跨源资源共享(Cross-Origin Resource Sharing, CORS)不会成为问题,因为它不使用cookie。
下图显示了如何获得JWT并使用它访问api或资源:
请注意,使用带签名的令牌时,令牌中包含的所有信息都将公开给用户或其他方,即使他们无法更改它。这意味着你不应该将秘钥信息放在令牌中。
五.为什么要使用JWT
让我们讨论JWT与Simple Web Tokens (SWT) 和Security Assertion Markup Language Tokens (SAML)相比的优点。
1.紧凑:由于JSON比XML更简洁,因此在编码时,它的大小也更小,这使得JWT比SAML更紧凑。这使得JWT成为在HTML和HTTP环境中传递的一个很好的选择。
2.可以有多种算法,安全:SWT只能由使用HMAC算法的共享秘钥对称地签名。JWT和SAML令牌可以使用X.509证书形式的公钥/私钥对进行签名。但是,与JSON的简单性相比,使用XML数字签名签名XML而不引入模糊的安全漏洞是非常困难的。
3.json容易解析:JSON解析器在大多数编程语言中很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象映射。这使得使用JWT比使用SAML更容易。
4.适合在移动端使用:在使用方面,JWT是按Internet规模使用的。这突出了JWT在多个平台(尤其是移动平台)上的客户端处理的易用性。
六.使用JWT管理session的缺点
1.更多的空间占用。如果将原存在服务端session中的各类信息都放在JWT中保存在客户端,可能造成JWT占用的空间变大,需要考虑cookie的空间限制等因素,如果放在Local Storage,则可能受到XSS攻击。
2.更不安全。这里是特指将JWT保存在Local Storage中,然后使用Javascript取出后作为HTTP header发送给服务端的方案。在Local Storage中保存敏感信息并不安全,容易受到跨站脚本攻击,跨站脚本(Cross site script,简称xss)是一种“HTML注入”,由于攻击的脚本多数时候是跨域的,所以称之为“跨域脚本”,这些脚本代码可以盗取cookie或是Local Storage中的数据。可以从这篇文章查看XSS攻击的原理解释。
3.无法作废已颁布的令牌。所有的认证信息都在JWT中,由于在服务端没有状态,即使你知道了某个JWT被盗取了,你也没有办法将其作废。在JWT过期之前(你绝对应该设置过期时间),你无能为力。
七.实战
1.引入依赖
<!-- =================JWT集成================= -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!-- JJWT(jst的框架):它是为了更友好在JVM上使用JWT,是基本于JWT, JWS, JWE, JWK框架的java实现。 -->
<!-- 诞生原因:原始JWT的API不够有好,不停的用withClaim放数据 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.登录生成jwt代码
private static final String SECRET = "companiontek";//秘钥
/**
* @param staffId 用户id
* @param phone 用户手机号
* @param expirationTime 过期时间
* @return 登录时生成token
*/
public static String createJavaWebToken(String staffId, String phone, Date expirationTime) {
//生成登录令牌
return Jwts.builder().setSubject(staffId).claim("phone", phone).setIssuedAt(new Date())
.setExpiration(expirationTime).signWith(SignatureAlgorithm.HS256, getKeyInstance()).compact();
}
private static Key getKeyInstance() {
//We will sign our JavaWebToken with our ApiKey secret
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
return signingKey;
}
3.过滤器/拦截器根据token获取用户id
/**
* 解析Token返回staffId,当验证失败返回null
*
* @param request
* @return
*/
public static Integer parserStaffIdByToken(HttpServletRequest request) {
try {
String token = request.getHeader("token");
final Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token).getBody();
return Integer.parseInt(claims.getSubject());
} catch (Exception e) {
}
return null;
}
参考资料: