一文理解JWT鉴权登录的应用

全菜工程师小辉

共 5818字,需浏览 12分钟

 ·

2021-04-28 03:44

如果对cookie/token有疑问的,可以查看之前的博客快速了解会话管理三剑客cookie、session和JWT

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

本文将针对JWT在身份验证业务场景下的应用进行讲解。

前置知识

JWT的数据结构

JWT的表现形式是个字符串,它由头部、载荷与签名这三部分组成,中间以「.」分隔。像下面这样:

头部Header

头部帮助应用程序定义如何处理接收到的令牌。头部信息以JSON格式显示,转化为JWT时需要用base64url算法进行编码。

{
"alg": "HS256",
"typ": "JWT"
}

typ:令牌类型
alg:用于生成签名的算法

载荷Payload

载荷用来存储传递的数据,比如用户信息的姓名、性别、年龄等。载荷信息以JSON格式显示,转化为JWT时需要用base64url算法进行编码。要注意的是机密信息不要放到这里,比如密码等。

{
"name": "全菜工程师小辉",
"introduce": "啥都不会"
}

JWT规定了7个默认字段供开发者选用。

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众,相当于接受者
nbf (Not Before):生效的起始时间
iat (Issued At):签发时间
jti (JWT ID):编号,唯一标识

签名Signature

对于每种加密算法,签名都对应的一个计算公式。例如SHA256加密算法的签名如下:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload) + "." +
Secret
)

当网关或者服务收到JWT时会计算签名的值,并将其与接收到的签名进行对比。如果不相同,则意味着该令牌已被不可信的一方修改或生成。

Secret(秘钥)是一定不可以不能泄露。对于非对称加密和对称加密,秘钥的形式是不同的,安全性也不一样,但并不一定对称加密就不好。有关这个问题的讨论,之后的博客再详细讲解。

注:验证JWT可以使用参考文档2的网站。

对称加密与非对称加密

对称加密是最快速、最简单的一种加密方式,加密与解密用的是同样的密钥。

非对称加密可以在不直接传递密钥的情况下完成解密。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。是由一对密钥来进行加解密的过程,分别称为公钥和私钥。公钥和私钥是成对的,可以互相解密。

加密与签名的区别

非对称加密中:

公钥加密,私钥解密:可以实现消息加密,防止信息被泄露。这样只有持有对应私钥的服务才能将消息明文解析。

私钥加密,公钥解密:可以实现数字签名,防止信息被篡改。这样可以确实是谁发来的消息。因为服务端的公钥只能解对应方的私钥加密的签名信息。(签名信息可以是摘要未加密信息中的一部分信息,例如JWT中的签名)

对称加密中,加解密使用同一个密钥,如果秘钥泄露,会发生极大的危险且很难察觉。

对称加密中,签名和验签使用同一个密钥,也就意味着验签者既可以验签,也能对数据进行重新签名、伪造签名,不能解决造假问题。而非对称算法很好地解决这个问题,签名和验签使用不同的密钥,避免造假问题发生。

JWT在鉴权登录中的应用

单JWT在鉴权登录中的使用方法

单JWT的会话管理流程如下:

  1. 在用户登录网站的时候,输入密码、短信验证或者其他授权方式登录,登录请求到达服务端的时候,服务端对信息进行验证,然后计算出包含用户鉴权信息的JWT字符串作为accesstoken,返回给客户端。

  2. 客户端拿到accesstoken后,存储到cookie或者浏览器的LocalStorage中。

  3. 客户端再次发送非匿名的接口请求,需要在HTTP请求头中加入accesstoken。

  4. 服务端拿到accesstoken后,验证JWT的信息是否被篡改。

对称加密与非对称加密在登录鉴权场景的区别

对称加密:

对称加密的秘钥为了安全,只放在授权中心,从而导致下游微服务鉴权必须要重复请求授权中心。

一种可行的解决方法是在授权中心首次鉴权通过后,将验证通过的信息存放到header中进行路由传递。但这种解决方法会受到架构和部门协作的影响,不推荐大项目这样做。

另一种可行的解决方法是将授权中心的鉴权功能做成工具包,开放给所有服务引入使用。但这种解决方法会存在秘钥更迭或者泄露的问题,需要基于现有架构进行优化。

非对称加密:

私钥仅保存在授权中心,减少秘钥泄露的可能;下游服务可以使用公钥获取JWT信息,不需要频繁与授权中心进行通信,提高了系统的运作效率。

JWT在登录鉴权场景的优点

  1. 严格的结构化。JWT载荷部分包含了与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且载荷部分支持业务的定制化。

  2. 支持跨域验证,可以应用于单点登录;不依赖cookie,使得其可以防止CSRF攻击,也能在禁用 cookie 的浏览器环境中正常运行。

  3. 体积小,因而传输速度快。

  4. 传输方式多样,可以通过URL/POST参数/HTTP头部等方式传输。

注:实测在Amazon上4c8g的云服务上,从token模式转换成JWT模式,注册qps提升4倍且未遇到性能瓶颈。

单JWT在鉴权登录中存在的问题

为了用户体验,accesstoken会设置较长时间,但是JWT形式的accesstoken包含了与用户相关的验证消息,通常情况下是不会被服务端保存,这就导致一个严重的问题当客户端重置密码后或用户被封禁的时候,无法阻拦用户的请求。

JWT登录鉴权增加refreshtoken机制(双JWT机制)来解决这个问题。

JWT登录鉴权增加refreshtoken机制

refreshtoken是OAuth2认证中的一个概念,一般称为“更新令牌”,和OAuth2的accesstoken同时生成。作用是用来获取新的accesstoken,不用于接口请求的身份认证。

通常情况下,refreshtoken的有效期会比较长,而accesstoken的有效期比较短。当accesstoken由于过期而失效时,使用refreshtoken就可以获取到新的accesstoken,如果refreshtoken失效了,用户就只能重新登录(但在某些业务场景,业务方想要自动续期。下一节针对这个问题有思考)。

引入refreshtoken后,会话管理流程改进如下:

  1. 客户端输入密码、短信验证或者其他授权方式登录,登录请求到达服务端的时候,服务端生成有效时间较短的accesstoken(例如2小时)和有效时间较长的refreshtoken(例如 30天)

  2. 客户端拿到accesstoken和refreshtoken后,存储到cookie或者浏览器的LocalStorage中。

  3. 客户端再次发送非匿名的接口请求,需要在HTTP请求头中加入accesstoken。如果accesstoken没有过期,服务端鉴权后返回给客户端需要的数据。

  4. 如果携带accesstoken访问需要认证的接口时鉴权失败,则客户端使用refreshtoken向刷新接口申请新的accesstoken;如果refreshtoken没有过期,服务端向客户端下发新的 accesstoken。客户端使用新的accesstoken重试之前鉴权失败的接口,做到用户对续期无感知;如果refreshtoken鉴权失败,则客户端跳转至登录界面,引导用户重新登录。

refreshtoken获取流程:

refreshtoken使用流程:

双JWT下如何进行权限管理

在用户登录时,将生成的refreshtoken和用户信息进行保存。当用户被封禁时,直接将用户信息或者对应的refreshtoken加入黑名单。

黑名单在刷新接口的时候进行校验,从而实现了双JWT场景下的权限管理。

有人可能会觉得加在网关层会更好。但如果黑名单加在网关层的话,就失去了JWT使用的初衷,将JWT模式变成了token模式,所以不提倡在网关层加黑名单。

由于客户端无法获取到新的accesstoken,从而再也无法访问需要认证的接口。这样的方式虽然会有一定的窗口期(取决于accesstoken的失效时间),但基本上可以适应常规情况下对用户登录鉴权的精度要求。

refreshtoken的自动续期

在某些业务场景,业务方想要用户鉴权自动续期(即用户长期不需要手动登录或者永久不需要手动登录直到手动取消授权)。

这里给出可行的方案,但实际上都是有需要规避的安全风险。

  1. 在refreshtoken过期之前更换新的refreshtoken。将refreshtoken过期时间设置为7天,并在每次用户打开应用程序并每隔一定时间(例如1小时)刷新令牌。如果用户超过7天没有打开过应用程序,那用户就需要再次登录。

  2. refreshtoken永远不会过期。这样的机制会导致JWT失去了意义。为了防止客户端更换或注销,需要以某种方式对JWT进行识别,应用程序需要提供注销的方法。例如使用设备的名称例如“xiaohui的iPad”来标记对应的JWT,然后用户可以去应用程序撤销访问“xiaohui的iPad”,从而注销掉refreshtoken。

JWT实例代码

参考文档2的网站列出了各种语言对应的JWT库。

由于Auth0提供的JWT库简单实用,小辉项目中使用Auth0实现JWT功能。

Auth0的代码见参考文档1。

引入Auth0只需要在pom.xml文件中增加如下代码:

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.9.0</version>
</dependency>

以下为小辉项目中,脱敏后的简化代码,读者可以参考下。

private static String jwtSecret = "s222dad@@13fhu123129=1232!!!3PPPdsadsashdhbn@@!!sdauS";
private static String jwtIssuer = "xiaohui";
/**
* 对称加密算法,HMAC256创建JWT
*/

public static String creatJWT(){
String token = null;
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
token = JWT.create()
.withIssuedAt(new Date())
.withExpiresAt(DateUtils.addHours(new Date(), 2))
.withIssuer(jwtIssuer)
.withNotBefore(new Date())
.sign(algorithm);
} catch (JWTCreationException e) {
log.error("creatJWT error {}", e);
}
return token;
}

/**
* 检验JWT
* @param token
* @return
*/

public static boolean checkJWT(String token) {
DecodedJWT decodedJWT = null;
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(jwtIssuer)
.build();
decodedJWT = verifier.verify(token);
} catch (TokenExpiredException e) {
log.info("checkJWT timeout,token is {},for more information {}", token, e);
return false;
} catch (JWTVerificationException e) {
log.info("checkJWT error,token is {},for more information {}", token, e);
return false;
}
return true;
}

项目也有非对称加密算法RSA256的解决方案,不过综合考虑项目架构、工期与安全性等因素,最后小辉在生产项目使用的是HS256的对称加密算法。

JWT需要添加一些与业务相关的参数用于检验,可以有效提高接口被爬的门槛和提高服务的安全性。更多有关JWT安全问题,之后的博客再详细讲解。

参考文档:

  1. https://github.com/auth0/java-jwt

  2. https://jwt.io/


浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报