认证机制之JWT
常见的认证机制
HTTP BASIC AUTH
HTTP BASIC auth:每次请求API的时候,都会把用户名和密码通过restful API传给服务端。
可以实现一个无状态思想,即每次HTTP请求和以前都没有啥关系,只是获取目标URI,得到目标内容之后,这次连接就被杀死,没有任何痕迹。
缺点:通过http请求发送给服务端的时候,很有可能将我们的用户名密码直接暴漏给第三方客户端,风险特别大,因此生产环境下用这个方法很少。
Session和cookie
在服务端全局创建session对象,session对象保存着各种关键信息
向客户端发送一组sessionId,成为一个cookie对象保存在浏览器中。
认证时,cookie的数据会传入服务端与session进行匹配,进行数据认证。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t3YeeZAC-1576822693805)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\msohtmlclip1\01\clip_image002.jpg)]
实现的是一个有状态的思想,即该服务的实例可以将一部分数据随时进行备份,并且在创建一个新的有状态服务时,可以通过备份恢复这些数据,以达到数据持久化的目的。
缺点
-
安全性。cookies的安全性不好,攻击者可以通过获取本地cookies进行欺骗或者利用cookies进行CSRF攻击。
-
跨域问题。使用cookies时,在多个域名下,会存在跨域问题。
-
有状态。session在一定的时间里,需要存放在服务端,因此当拥有大量用户时,也会大幅度降低服务端的性能。
-
状态问题。当有多台机器时,如何共享session也会是一个问题,也就是说,用户第一个访问的时候是服务器A,而第二个请求被转发给了服务器B,那服务器B如何得知其状态。
-
移动手机问题。现在的智能手机,包括安卓,原生不支持cookie,要使用cookie挺麻烦。
大致流程
- 用户向服务器发送用户名和密码
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个* session_id*,写入用户的* Cookie*。
- 用户随后的每一次请求,都会通过* Cookie*,将* session_id *传回服务器
- 服务器收到* session_id*,找到前期保存的数据,由此得知用户的身份。
JWT-Token认证
Token: 访问资源的凭据。
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。
JWT简介
什么是JWT
JSON Web Token(JWT)是一个基于json数据结构的,非常轻巧的规范
它允许我们使用jwt在用户和服务器之间传输安全可靠的信息。
可以通过数字签名进行验证和信任
虽然JWT可以加密以在各方之间提供保密,但只将专注于签名令牌。
签名令牌可以验证其中包含的声明的完整性,而加密令牌则隐藏其他方的声明。
当使用公钥/私钥对签署令牌时,签名还证明只有持有私钥的一方是签署私钥的一方。
特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。
优点
在分布式系统中,很好地解决了单点登录问题,很容易解决了session共享的问题。
那Token机制相对于Cookie机制的好处
- 支持跨域访问:Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提 是传输的用户认证信息通过HTTP头传输.
- 无状态:Token机制本质是校验, 他得到的会话状态完全来自于客户端, Token机制在服务端不需要存储session信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
- 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript, HTML,图片等),而你的服务端只要提供API即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候,你可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等) 时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认 证机制就会简单得多。 CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256 计算 的Token验证和解析要费时得多. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要 为登录页面做特殊处理.
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)
相比Simple Web Tokens (SWT)(简单Web令牌)和Security Assertion Markup Language Tokens (SAML)(安全断言标记语言令牌);
- JWT比SAML更简洁,在HTML和HTTP环境中传递更方便;
- 在安全方面,SWT只能使用HMAC算法通过共享密钥对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公钥/私钥对进行签名。与签名JSON的简单性相比,使用XML数字签名可能会存在安全漏洞;
- JSON解析成对象相比XML更流行、方便。
缺点
- 占带宽
正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。
- 无论如何你需要操作数据库
在网站上使用 JWT,对于用户加载的几乎所有页面,都需要从缓存/数据库中加载用户信息,如果对于高流量的服务,你确定这个操作合适么?如果使用redis进行缓存,那么效率上也并不能比 session 更高效
-
无法在服务端注销,很难解决劫持问题
-
无法作废已经颁布的令牌
-
不易应对过期数据
-
性能问题
JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。
使用缺陷
用用进行交互的页面包含动态数据,需要频繁操作数据库时
JWT存储数据比cookie开销大
适用情景
从服务器到服务器或客户端到服务器(如:移动应用 APP 或单页面应用)的 API 服务
你的客户端需要通过 API 进行身份验证,并返回 JWT
客户端使用返回的 JWT 经过身份验证去请求其它的 API 服务
这些其它 API 服务通过客户端的 JWT 验证客户端是可信的,并且可以执行某些操作无需再次验证
授权:单点登录或 OpenID Connect 认证,实现一种通过第三方验证用户的方法。
因为JWT使用起来轻便,开销小,服务端不用记录用户状态信息(无状态)
信息交换:JWT是在各个服务之间安全传输信息的好方法,由于使用标头和有效负载计算签名,还可以验证内容是否未被篡改。
数据结构
数据结构:一个很长的字符串,字符之间通过"."分隔符分为三个子串。各子串之间也没有换行符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7wtjrwV-1576822693808)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\msohtmlclip1\01\clip_image004.jpg)]
JWT的三个部分如下。JWT头、有效载荷和签名,将它们写成一行
JWT头(header)
组成
alg:散列算法
typ:令牌类型
{
alg: 签名使用的算法, 默认为HMAC SHA256(HS256)
typ: 令牌的类型,JWT令牌统一写为JWT
}
令牌类型
只能是JWT
散列算法
JWS | 算法名称 | 描述 |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
转换字符串
使用Base64Url算法将JSON转换为字符串,形成JWT的第一部分
有效载荷(payload)
载荷就是存放有效信息的地方
claims
claims是关于实体(常用的是用户信息)和其他数据的声明
claims有三种类型: registered, public,private claims。
registered claims
这些是一组预定义的claims,非强制性的,但是推荐使用
- iss(发行人)
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
public claims
自定义claims,注意不要和JWT注册表中属性冲突
一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
private claims
自定义的claims
用于在同意使用这些claims的各方之间共享信息
它们既不是Registered claims,也不是Public claims。
使用Base64Url算法将JSON转换为字符串,形成JWT的第一部分
注意
对于签名令牌,此信息虽然可以防止篡改,但任何人都可以读取。除非加密,否则不要将敏感信息放入到Payload或Header元素中。
自定义claim和标准claim的区别
JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
使用方法
JWT.create().withHeader(map) // header
.withClaim(“iss”, “Service”) // payload
.withClaim(“aud”, “APP”)
.withIssuedAt(iatDate) // sign time
.withExpiresAt(expiresDate) // expire time
签名
目的
确保数据不会被篡改
参数
编码的Header,编码的Payload,秘钥,Header中指定的算法
密钥:仅保存在服务器中,不向用户公开
在使用私钥签名令牌的情况下,它还可以验证JWT的请求方是否是它所声明的请求方。
生成签名公式
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),
secret)
输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,与SAML等基于XML的标准相比更加紧凑。
Base64URL算法
JWT头和有效载荷序列化的算法都用到了Base64URL。
该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。
Base64中用的三个字符是"+","/“和”=",由于在URL中有特殊含义
Base64URL中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。
注意
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
原理
服务器认证以后,生成一个 JSON 对象,发回给用户
用户将信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。
服务器完全只靠这个对象认定用户身份。
为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
如果将它存储在Cookie中,就可以自动发送,但是不会跨域
一般是将它放入HTTP请求的Header Authorization字段中。Authorization: Bearer
当跨域时,也可以将JWT被放置于POST请求的数据主体中。
JWT 的内容(内部的 JSON 数据)通常是不加密的。这意味着,即使没有密钥,也可以查看 JWT 内的数据。JWT 默认并不会加密你的数据,它只是帮助你验证是你信任的一方创建了它。
大概的流程
-
客户端使用用户名跟密码请求登录
-
服务端收到请求,去验证用户名与密码
-
验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
-
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
-
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
-
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
Token机制,我认为其本质思想就是将session中的信息简化很多,当作cookie用,也就是客户端的“session”。
实现JWT—JJWT
为了更友好在JVM上使用JWT
是基本于JWT, JWS, JWE, JWK框架的java实现。
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:https://github.com/jwtk/jjwt
依赖
Maven
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
Gradle:
dependencies {
compile 'io.jsonwebtoken:jjwt:0.9.0'
}
创建token
JwtBuilder builder= Jwts.builder()
.setId("xxx") //设置唯一编号
.setSubject("xxx")//设置主题 可以是JSON数据
.setIssuedAt(new Date())//设置签发日期
.signWith(SignatureAlgorithm.HS256,"hahaha");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
解析token
在web应用中创建token是由服务端进行然后发给客户端
客户端在下次向服务端发送请求时需要携带这个token。
服务端接到这个token 应该解析出token中的信息,根据这些信息查询数据库返回相应的结果。
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y";
Claims claims = Jwts.parser().setSigningKey("hahaha").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
运行打印效果:
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.
设置过期时间
(1)创建token 并设置过期时间
long now=System.currentTimeMillis();
long exp=now+1000*30;//30秒过期
JwtBuilder jwtBuilder = Jwts.builder().setId( "xxx" )
.setSubject( "xxx" )
.setIssuedAt( new Date() )//签发时间
.setExpiration( new Date( exp ) )//过期时间
.signWith( SignatureAlgorithm.HS256, "hahaha" );
String token = jwtBuilder.compact();
System.out.println(token);
(2)解析TOKEN
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";
Claims claims = Jwts.parser().setSigningKey("hahaha").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
当前时间超过过期时间,则会报错。
自定义claims
想存储更多的信息可以定义自定义claims。
long now=System.currentTimeMillis();
long exp=now+1000*30;//30秒过期
JwtBuilder jwtBuilder = Jwts.builder().setId( "xxx" )
.setSubject( "xxx" )
.setIssuedAt( new Date() )//签发时间
.setExpiration( new Date( exp ) )//过期时间
.claim( "roles","admin" )//自定义的claim
.signWith( SignatureAlgorithm.HS256, "hahaha" );
String token = jwtBuilder.compact();
System.out.println(token);
解析TOKEN:
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIyNTM3NTQsImV4cCI6MTU2MjI1Mzc4Mywicm9sZXMiOiJhZG1pbiJ9.CY6CMembCi3mAkBHS3ivzB5w9uvtZim1HkizRu2gWaI";
Claims claims = Jwts.parser().setSigningKey( "hahaha" ).parseClaimsJws( token ).getBody();
System.out.println(claims);
System.out.println(claims.get( "roles" ));
注意,jjwt不支持jdk11,0.9.1以后的jjwt必须实现signWith()方法才能实现
补充
如何使token失效
只能是客户端把token清理掉,或者token自己过期
服务端手动增加清楚token的逻辑