Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(Single Sign On,SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

组成

(注:如下所涉及的base64指base64 URL算法,其与普通的base64算法有区别:Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法)

由句号分隔的三段base64 URL串b1.b2.b3,如:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

  • header:头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,为json格式。用base64 URL算法转成一个串b1。示例:

    {
    "typ": "JWT",
    "alg": "HS256"
    }
  • paload:放入一些自定义信息,为json格式。用base64转成一个串b2。jwt预放入了五个字段:
    • iss: 该JWT的签发者
    • sub: 该JWT所面向的用户
    • aud: 接收该JWT的一方
    • exp(expires): 什么时候过期,这里是一个Unix时间戳
    • iat(issued at): 在什么时候签发的
  • signature:用header中所声明的签名算法(需要为之提供一个key),根据 base64( header).base64(paload) 算得签名值:第三个base64串b3。

注:

三部分都是明文的,可以通过base64解码看出原始内容,故jwt payload等部分中一定不要放入敏感数据如密码等内容。

注意签名和加密的区别:前者指根据内容产生一段摘要信息,摘要信息长度通常固定且比原始值少很多且不可逆,签名也可以理解为摘要、指纹、哈希等,具体算法有MD5、HS256等;后者则指用某种算法将原始内容转换成不可读的内容,只有知道解密方法才能根据被加密的内容获知原始内容,具体算法有RSA等。

校验原理

由于用base64,所以可以直接逆转码提取header、payload信息。服务端收到token后会根据header声明的加密算法再计算下signature,若与token中的signature不同则当成未授权的token。

JWT认证方式的实现方式

1、客户端不需要持有密钥,由服务端通过密钥生成Token。
2、客户端登录时通过账号和密码到服务端进行认证,认证通过后,服务端通过持有的密钥生成Token,Token中一般包含失效时长和用户唯一标识,如用户ID,服务端返回Token给客户端。
3、客户端保存服务端返回的Token。
4、客户端进行业务请求时在Head的Authorization字段里面放置Token,如:Authorization: Bearer Token
5、服务端对请求的Token进行校验,并通过Redis查找Token是否存在,主要是为了解决用户注销,但Token还在时效内的问题,如果Token在Redis中存在,则说明用户已注销;如果Token不存在,则校验通过。
6、服务端可以通过从Token取得的用户唯一标识进行相关权限的校验,并把此用户标识赋予到请求参数中,业务可通过此用户标识进行业务处理。
7、用户注销时,服务端需要把还在时效内的Token保存到Redis中,并设置正确的失效时长。
时序图如下:

在上述过程中,登录时服务端需要查询数据库以确定用户名、密码是否正确,在登录成功之后的其他请求中则可以直接从token中提取需要的信息而不需要查询数据库。

功能

(与传统session或token的区别):

  • 适合用于向Web应用传递一些非敏感信息如userId、isAdmin等,不能包含密码等敏感信息;
  • 本身具备失效判断机制:根据串本身就能知道该token是否失效,而不用自己出来了;
  • 服务端不需要存储token,而是分散给各个客户端存储,session机制则要。有利就有弊,jwt增加了计算开销如加解密,但总的利大于弊。
  • 服务端能识别被篡改的token,所以只要token校验通过,就可以把里面封装的信息当成可信的。
  • 由于jwt是分发到客户端存储的而服务端不需要存储,故很容易借之实现单点登录(假设需单点登录的各域名有共同顶级域名):只需要将含有JWT的Cookie的domain设置为顶级域名即可,各子域名站点就能够获得该JWT从而实现共享。

有利就有弊:

  • 单纯地实验jwt存在问题:一个浏览器内(即一个session)可以多账号同时登录、一个账号可以在多个浏览器上同时登录,需要借助session等加以解决。
  • jwt的一个很重要的特点或优点是使得服务端完全无状态(stateless),服务端无须存储认证相关信息了。但这也成为其缺点:用户注销时token不会立马失效,只能等签发有效期到期。可以通过Redis等为用户登出时的token维护一个黑名单来解决,但这就使得有状态了。

相比较于session/cookie, token能提供更加重要的好处:
  1. CORS。 
  2. 不需要CSRF的保护。 
  3. 更好的和移动端进行集成。 
  4. 减少了授权服务器的负载。 
  5. 不再需要分布式会话的存储。 
有一些交互操作会用这种方式需要权衡的地方: 
  1. 更容易受到XSS攻击 
  2. 访问令牌可以包含过时的授权声明(e。g当一些用户权限撤销) 
  3. 在claims 的数在曾长的时候,Access token 也能在一定程度上增长。 
  4. 文件下载API难以实现的。 
  5. 无状态和撤销是互斥的。

更好的方案-结合token和kookie:生成token后发给客户端并设置客户端cookie,之后请求优先检验请求头是否有token,没有的话从cookie取。这样对于浏览器端前端无需写带token的逻辑(通过cookie来让浏览器自动带上)、移动端则通过设置请求头token实现认证;而后端则可以兼容这两种场景。

使用示例

依赖:

        <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

代码:

 import java.security.Key;

 import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.impl.crypto.MacProvider; public class JWTtest { public static void main(String[] args) {
// 生成jwt
Key key = MacProvider.generateKey();// 这里是加密解密的key。
String compactJws = Jwts.builder()// 返回的字符串便是我们的jwt串了
.setSubject("Joe")// 设置主题
.claim("studentId", 2)// 添加自定义数据
.signWith(SignatureAlgorithm.HS512, key)// 设置算法(必须)
.compact();// 这个是全部设置完成后拼成jwt串的方法
System.out.println("the generated token is: " + compactJws); // 解析jwt
try { Jws<Claims> parseClaimsJws = Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);// compactJws为jwt字符串
Claims body = parseClaimsJws.getBody();// 得到body后我们可以从body中获取我们需要的信息
// 比如 获取主题,当然,这是我们在生成jwt字符串的时候就已经存进来的
String subject = body.getSubject();
System.out.println("the subject is: " + subject);
System.out.println("the studentId is: " + body.get("studentId")); // OK, we can trust this JWT } catch (SignatureException | MalformedJwtException e) {
// TODO: handle exception
// don't trust the JWT!
// jwt 解析错误
} catch (ExpiredJwtException e) {
// TODO: handle exception
// jwt 已经过期,在设置jwt的时候如果设置了过期时间,这里会自动判断jwt是否已经过期,如果过期则会抛出这个异常,我们可以抓住这个异常并作相关处理。
}
}
}

实践踩坑记录

token失效后如何更新

劣势:jwt与session相比的一大劣势是有效期放在token里保存在客户端,故服务端无法更改有效期,因此如果单只用一个token则在token有效期到后用户就会被提示需重新登录,而不是像session那样每次有访问就可由服务端延长session有效期。

如何解决?

方案:登录后同时生成accessToken、refreshToken,前者在调用业务接口时带上,后者则用于更新accessToken,后者有效期比前者长。当accessToken失效时,由客户端携带refreshToken请求获取新的accessToken,若此时refreshToken也过期,则真正过期了,跳到登录页。此方案可减少用户一直在用系统时被提示重新登录的频率,但没有全部杜绝false positive,因为refreshToken也有过期时间。
实现:生成新accessToken时,需要确保新的与原token具有一样的业务claim。具体实践中,如何更新accessToken?几种方法(以下 更新token 指由refreshToken去获取新的accessToken):

1、登录成功后生成两个token时把accessToken加到refreshToken的claim中,更新token时从refreshToken解析出原accessToken的clasim,根据该claim生成新accessToken。问题在于如果原accessToken失效了,则此时对accessToken parseClaims会报过期错误从而拿不到accessToken中的claim。不可行

2、更新token时由前端将原accessToken作为参数传给后端。与上个方法一样,有parseClaims失败的问题从而拿不到原claim。不可行

3、生成两个token时确保refreshToken包含accessToken所具有的所有业务claim,这样更新时可以仅根据refreshToken即可完成。可行。主要代码示例如下:

@Component
public class TokenFactory {
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
private JwtSettings settings; @Autowired
public TokenFactory(JwtSettings settings) {
this.settings = settings;
} /**
* 根据userContext设置加入到token中的数据
*
* @param userContext
* @return
*/
private Claims generateClaims(UserContext userContext) { String username = userContext.getUsername();
if (null == username || username.trim().equals(""))
throw new IllegalArgumentException("用户名为空无法创建jwt token"); if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty())
throw new IllegalArgumentException("用户没有任何权限"); // 设置token里的数据
Claims claims = Jwts.claims().setSubject(userContext.getUsername());
claims.put(JwtToken.basicTokenPayload_keyUserId, userContext.getUserId());
claims.put(JwtToken.basicTokenPayload_keyRoles,
userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
Map<String, Object> customProperties = userContext.getCustomProperiesInToken();
if (null != customProperties) {
customProperties.entrySet().forEach(entry -> {
String key = entry.getKey();
if (claims.containsKey(key)) {
throw new IllegalArgumentException(String.format("token payload已包含属性'%s'", key));
} else {
claims.put(key, entry.getValue());
}
});
} return claims;
} /**
* 设置jwt自有的几个payload如签发者、有效期等 并生成token
*
* @param claims
* @param tokenId
* @param ttlMinutes
* @return
*/
private final String createTokenStr(Claims claims, String tokenId, Integer ttlMinutes) { LocalDateTime currentTime = LocalDateTime.now(); String token = Jwts.builder().setClaims(claims).setId(tokenId).setIssuer(settings.getTokenIssuer()) .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(
Date.from(currentTime.plusMinutes(ttlMinutes).atZone(ZoneId.systemDefault()).toInstant()))
.signWith(signatureAlgorithm, settings.getTokenSigningKey()).compact(); return token;
} /**
* 根据userContext生成token,返回包含两个元素,分别为accessToken、refreshToken
*
* @param userContext
* @return
*/
@SuppressWarnings("unchecked")
public final List<JwtToken> createTokens(UserContext userContext) {
Claims claims = generateClaims(userContext);
String tokenId = UUID.randomUUID().toString();// 确保生成的两个token id一样 AccessToken accessToken = new AccessToken(
createTokenStr(claims, tokenId, settings.getTokenExpirationTimeMinutes())); // refresh token,与access token的区别:role多包含了一个元素;有效期不同
// role包含access token的role元素,以可根据refresh token生成新的access token
((List<String>) (claims.get(JwtToken.basicTokenPayload_keyRoles))).add(Scopes.REFRESH_TOKEN.authority());
RefreshToken refreshToken = new RefreshToken(
createTokenStr(claims, tokenId, settings.getRefreshTokenExpireTimeMinutes())); return Arrays.asList(accessToken, refreshToken); } public final AccessToken createAccessToken(UserContext userContext) {
return (AccessToken) ((List<JwtToken>) (createTokens(userContext))).get(0);
} /**
* 根据refreshToken生成新的accessToken。新accessToken与原accessToken除了 生成时间 和 有效截止时间
* 不一样外其他均一样
*
* @param refreshToken
* @return
*/
@SuppressWarnings("unchecked")
public AccessToken createAccessToken(RefreshToken refreshToken) {
// 若由旧的accessToken生成新的accessToken则若旧者已过期此时parseClaims会报过期错从而拿不到原claim,故转由refreshToken生成 Claims claims = refreshToken.parseClaims(settings.getTokenSigningKey()).getBody(); // 与生成accessToken、refreshToken时两者的关系对应
((List<String>) (claims.get(JwtToken.basicTokenPayload_keyRoles))).remove(Scopes.REFRESH_TOKEN.authority()); return new AccessToken(createTokenStr(claims, claims.getId(), settings.getTokenExpirationTimeMinutes()));
} }

排他登录、登出的实现

借助中心化缓存如Redis来完成

参考资料

  • Json Web Token:http://blog.leapoahead.com/2015/09/06/understanding-jwt/
  • Json Web Token单点登录:http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/
  • https://blog.csdn.net/a82793510/article/details/53509427

Json Web Token(JWT)的更多相关文章

  1. JSON WEB TOKEN(JWT)的分析

    JSON WEB TOKEN(JWT)的分析 一般情况下,客户的会话数据会存在文件中,或者引入redis来存储,实现session的管理,但是这样操作会存在一些问题,使用文件来存储的时候,在多台机器上 ...

  2. JSON Web Token(JWT)使用步骤说明

    在JSON Web Token(JWT)原理和用法介绍中,我们了解了JSON Web Token的原理和用法的基本介绍.本文我们着重讲一下其使用的步骤: 一.JWT基本使用 Gradle下依赖 : c ...

  3. JSON Web Token(JWT)原理和用法介绍

    JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案.今天给大家介绍一下JWT的原理和用法. 官网地址:https://jwt.io/ 一.跨域身份验证 Internet服务无法与 ...

  4. JSON Web Token(JWT)机制

    JSON Web Token(JWT)机制  JWT是一种紧凑且自包含的,用于在多方传递JSON对象的技术.传递的数据可以使用数字签名增加其安全行.可以使用HMAC加密算法或RSA公钥/私钥加密方式. ...

  5. Spring Boot集成JSON Web Token(JWT)

    一:认证 在了解JWT之前先来回顾一下传统session认证和基于token认证. 1.1 传统session认证 http协议是一种无状态协议,即浏览器发送请求到服务器,服务器是不知道这个请求是哪个 ...

  6. JSON Web Token(JWT)的详解

    1.传统身份验证和JWT的身份验证 传统身份验证: HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用.这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户 ...

  7. JSON Web Token(JWT)学习笔记

    1.JWT 的Token 标准的Token由三个部分并以.(点号)连接方式组成,即 header.payload.signature,如下 eyJhbGciOiJIUzI1NiIsInR5cCI6Ik ...

  8. 10分钟了解JSON Web令牌(JWT)

    JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案.虫虫今天给大家介绍JWT的原理和用法. 1.跨域身份验证 Internet服务无法与用户身份验证分开.一般过程如下. 1.用户 ...

  9. 了解JSON Web令牌(JWT)

    JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案.今天给大家介绍JWT的原理和用法. 1.跨域身份验证 Internet服务无法与用户身份验证分开.一般过程如下. 1.用户向服 ...

随机推荐

  1. web前端不可错过的开发工具–Adobe Brackets

    Adobe Brackets是一个开源的基于HTML/CSS/JavaScript开发,运行在native shell上的集成开发环境.该项目由Adobe创建和维护,根据MIT许可证发布.提供Wind ...

  2. no console to display at this time

    no console to display at this time我把控制台关掉,重新run as 还是同样问题,于是运行其他项目,但是其他项目能正常运行,说明项目写的有问题,而不是控制台的问题

  3. C# abstract virtual override new finally java final finalize

    virtual:声明虚方法.可以被其派生类所重写的.重写方法需要使用override或者new关键字. override:覆盖原方法.可对重写virtual.override.abstract进行重写 ...

  4. 迭代函数:zip、enumerate,list解析

    #encoding:utf-8 """ 并行迭代: zip enumerate 获取元素及下标 list解析 iter """ #zip # ...

  5. 10个提升MySQL性能的小技巧

    从工作量分析到索引的三条规则,这些专家见解肯定会让您的MySQL服务器尖叫. 在所有的关系数据库中,MySQL已经被证明了完全是一头野兽,只要通知停止运行就绝对不会让你多等一秒钟,使你的应用置于困境之 ...

  6. API判断本机安装的Revit版本信息

    start [Transaction(TransactionMode.Manual)] [Regeneration(RegenerationOption.Manual)] public class c ...

  7. Android:TextView控件

    3.2.1    TextView TextView 可以说是 Android 中最简单的一个控件了,你在前面其实也已经和它打过了一 些打交道.它主要用于在界面上显示一段文本信息,比如你在第一章看到的 ...

  8. selenium+python自动化78-autoit参数化与批量上传

    前言 前一篇autoit实现文件上传打包成.exe可执行文件后,每次只能传固定的那个图片,我们实际测试时候希望传不同的图片. 这样每次调用的时候,在命令行里面加一个文件路径的参数就行. 一.命令行参数 ...

  9. Android系统资源图标android.R.drawable

    Android™ 2.1 android.R.drawable Icon Resources Android™ 1.5 android.R.drawable Icon Resources Androi ...

  10. 使用 cmake 进行交叉编译

    cmake 因为“又”要额外学一门语言而被诟病,但这并不妨碍越来越多私人项目用 cmake 来管理:autoconfig 确实是更好的发行工具,但用 cmake 管理项目显然更加的容易.如果要应用这些 ...