HTTP BASIC auth:每次请求API的时候,都会把用户名和密码通过restful API传给服务端。
可以实现一个无状态思想,即每次HTTP请求和以前都没有啥关系,只是获取目标URI,得到目标内容之后,这次连接就被杀死,没有任何痕迹。
缺点:通过http请求发送给服务端的时候,很有可能将我们的用户名密码直接暴漏给第三方客户端,风险特别大,因此生产环境下用这个方法很少。
在服务端全局创建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挺麻烦。
大致流程
Token: 访问资源的凭据。
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。
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机制的好处
相比Simple Web Tokens (SWT)(简单Web令牌)和Security Assertion Markup Language Tokens (SAML)(安全断言标记语言令牌);
正常情况下要比 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头、有效载荷和签名,将它们写成一行
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的第一部分
载荷就是存放有效信息的地方
claims是关于实体(常用的是用户信息)和其他数据的声明
claims有三种类型: registered, public,private claims。
这些是一组预定义的claims,非强制性的,但是推荐使用
自定义claims,注意不要和JWT注册表中属性冲突
一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
自定义的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的标准相比更加紧凑。
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”。
为了更友好在JVM上使用JWT
是基本于JWT, JWS, JWE, JWK框架的java实现。
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:https://github.com/jwtk/jjwt
Maven
io.jsonwebtoken
jjwt
0.9.0
Gradle:
dependencies {
compile 'io.jsonwebtoken:jjwt:0.9.0'
}
JwtBuilder builder= Jwts.builder()
.setId("xxx") //设置唯一编号
.setSubject("xxx")//设置主题 可以是JSON数据
.setIssuedAt(new Date())//设置签发日期
.signWith(SignatureAlgorithm.HS256,"hahaha");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
在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。
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的逻辑