JWT

来自:

铸鼎__ > Java毕业设计开发笔记 > 后端开发 > shiro+JWT

JWT 的原理

参考:

简书:什么是 JWT – JSON WEB TOKEN

RESTful API:JWT 认证

JWT官网

知乎:Base64编码详解

CSDN:从零入门HMAC-SHA256

服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

1
2
3
4
5
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的数据结构

实际的 JWT 大概就像下面这样。

img

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

1
2
3
Header(头部)
Payload(负载)
Signature(签名)

写成一行,就是下面的样子。

Header.Payload.Signature

下面依次介绍这三个部分。

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名,也就是说需要base64url加密后的header和base64url加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

名词:

  • Claim 解析了Jwt后的得到的明文信息;

总结:

看到有关HMAC SHA256算法原理的文章,其实就是此算法相当于加盐(secret)处理,有了这个secret才可以对传输过来的Token内部的Signature才可以认证是否是本服务器进行签发的,只有知道secret才可以对base64UrlEncode(header) + "." + base64UrlEncode(payload) 字符串进行一个完整性的鉴别(再进行一遍HMAC SHA256对比Signature),如果匹配就可以进行登录,含有secret服务器也可以进行对Token延期后发给客户端。

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。

JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这个方式跨域需要考虑更多问题,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT特点

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  • JWT 不加密的情况下,不能将秘密数据写入 JWT。

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

Session和JWT登录 整合方案

分析:

  • 用类似于JWT的token作为SessionId,客户端采用JWT模式来保存和发送这个sessionId,保存在cookie或localStorage中,发送时用header;
  • 为了接收非同源客户端的AJAX请求,服务器端需要启用 CORS(Cross-Origin Resource Sharing) 跨域访问,为指定域名的客户端开通白名单,以便接收其他不同客户端的请求;
  • 服务器端采用session的处理方式,保存session信息,并根据请求来更新过期时间,同时,服务器端兼容从cookie或者header中取得sessionId;
  • 平滑的处理token过期,如果用户一直在活动,需要刷新token,客户端要配合。

SessionID与JWT Token 两种方式的对比

JWT登录步骤 Session登录步骤
客户端 携带认证名和密码 发起登录请求 客户端 携带认证名和密码 发起登录请求
服务器端验证成功,返回 token 给客户端 服务器端验证成功,将用户信息存储下来,生成一个 sessionId, 返回给客户端,并通知客户端将sessionId set到cookie中
客户端保存 token(通常是保存在Cookie或者LocalStorage中) 客户端自动执行服务器端将sessionId set到cookie中的命令,sessionId被自动保存在cookie中
客户端以后每次请求,都在Header中 携带该Token 客户端以后每次请求,都会自动将cookie中的sessionId发送给服务器端
服务器端,每次接收非登录请求,都验证Header中是否有token 服务器端,每次接收请求,都从cookie中取出sessionId,根据这个id找到存储的用户信息,如果有,说明登录,否则说明未登录或者已过期。

Shiro简介

Apache Shiro 是 Java 的一个安全框架

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

参考:

Shiro框架原理及应用分析

CSDN:为什么使用token?session与token的区别

CSDN:shiro使用(使用token预热,为什么要使用token)

CSDN:shiro使用(使用token)

CSDN:shiro使用(增强版token,JWT和shiro结合使用)

为什么学习Shiro

  • 既然shiro将安全认证相关的功能抽取出来组成一个框架,使用shiro就可以非常快速的完成认证授权等功能的开发,降低系统成本
  • shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。
  • java领域中spring security(原名Acegi)也是一个开源的权限管理框架,但是spring security依赖spring运行,而shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择shiro。

基本功能

Shiro基本功能

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
  • Authorization:权限验证,验证某个用户是否拥有某项权限;
  • Session Management:会话管理,登录一次后没退登前,他的所有信息都在会话中;
  • Cryptography:加密保护数据安全,密码加密存储到数据库;
  • Web Support:Web 支持,可集成到Web环境;
  • Caching:缓存,用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  • Concurrency: 支持多线程应用的并发验证,在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户;
  • Remember Me:记住我,常见的功能,一次登录,下次来就不用登录;

注意:Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。

工作流程介绍

Shiro工作流程

代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。

  • SecurityManager:安全管理器,所有与安全有关的操作都会与 SecurityManager 交互,Shiro的核心,它负责与后边介绍的其他组件进行交互,比如对全部的subject进行安全管理。实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。;

  • Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作

    • 例如:Realm中获取JWT利用JWT的内部方解密出JWT中包含的信息,比如校验解析出JWT信息成一个Claim(校验后的JWT信息),获取Claim中的getSubject(获得用户唯一信息,比如userID),再将获取到的Subject,比如userID,根据userID对数据库进行查询,将查询出的User实体类一部分信息(保证唯一)放入另一个实体类,再将这个实体类存入比如Redis中,完成认证(未进行源码调试)
  • Subject:主体,代表当前用户,不一定是具体的人,当前应用交互的任何东西都是 Subject,可能是一个通过浏览器请求的用户,也可能是一个运行的程序,如网络爬虫,机器人等。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权;

  • Subject 的所有交互都会委托给 SecurityManager;

Shiro 的架构

Shiro 内部的架构

  • SecurityManager: Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制,管理着所有 Subject,且负责进行认证和授权、及会话、缓存的管理。
  • Realm:可以有 1 个或多个 Realm,可以认为是安全实体,相当于datasource数据源,但不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。securityManager进行安全认证需要通过Realm获取用户权限数据,用于获取安全实体的;可以是 JDBC 实现、 LDAP 实现、内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;一般要在应用中都需要实现自己的 Realm;
  • Subject:主体,任何可以与应用交互的 “用户”;
  • Authenticator:认证器,负责主体认证,是一个扩展点, Shiro 默认实现,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下用户认证通过;
  • Authorizer:授权器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据,它不依赖web容器的session,于是shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
  • SessionDAO:DAO 即,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过 JDBC 写到数据库;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
  • Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的,方便开发。比如提供常用的散列、加/解密等功能。。

作为开发者:

  1. 代码通过Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
  2. 需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
  3. Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。

身份验证

即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。

常见名词:

  • principals:身份,即主体的标识属性,可以是数据库字段任何属性,如用户名、邮箱等,但需要保证唯一。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
  • credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
  • 最常见的 principalscredentials 组合就是用户名 / 密码了。

身份认证流程

代码详见:

W3c School:Shiro 身份验证

img

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证成功了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。