Apache Shiro + JWT
JWT
来自:
铸鼎__ > Java毕业设计开发笔记 > 后端开发 > shiro+JWT
JWT 的原理
参考:
服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
1 | { |
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
JWT 的数据结构:
实际的 JWT 大概就像下面这样。
它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
1 | Header(头部) |
写成一行,就是下面的样子。
Header.Payload.Signature
下面依次介绍这三个部分。
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
1 | { |
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
1 | iss (issuer):签发人 |
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
1 | { |
注意,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旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
参考:
CSDN:为什么使用token?session与token的区别
为什么学习Shiro
- 既然shiro将安全认证相关的功能抽取出来组成一个框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
- shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。
- java领域中spring security(原名Acegi)也是一个开源的权限管理框架,但是spring security依赖spring运行,而shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择shiro。
基本功能
- Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
- Authorization:权限验证,验证某个用户是否拥有某项权限;
- Session Management:会话管理,登录一次后没退登前,他的所有信息都在会话中;
- Cryptography:加密保护数据安全,密码加密存储到数据库;
- Web Support:Web 支持,可集成到Web环境;
- Caching:缓存,用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
- Concurrency: 支持多线程应用的并发验证,在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户;
- Remember Me:记住我,常见的功能,一次登录,下次来就不用登录;
注意: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 的架构
- 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 提供了一些常见的加密组件用于如密码加密 / 解密的,方便开发。比如提供常用的散列、加/解密等功能。。
作为开发者:
- 代码通过Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
- 需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
- Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。
身份验证
即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。
常见名词:
- principals:身份,即主体的标识属性,可以是数据库字段任何属性,如用户名、邮箱等,但需要保证唯一。一个主体可以有多个
principals
,但只有一个Primary principals
,一般是用户名 / 密码 / 手机号。 - credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
- 最常见的
principals
和credentials
组合就是用户名 / 密码了。
身份认证流程
代码详见:
- 首先调用
Subject.login(token)
进行登录,其会自动委托给Security Manager
,调用之前必须通过SecurityUtils.setSecurityManager()
设置; SecurityManager
负责真正的身份验证逻辑;它会委托给Authenticator
进行身份验证;Authenticator
才是真正的身份验证者,Shiro API
中核心的身份认证入口点,此处可以自定义插入自己的实现;Authenticator
可能会委托给相应的AuthenticationStrategy
进行多Realm
身份验证,默认ModularRealmAuthenticator
会调用AuthenticationStrategy
进行多Realm
身份验证;Authenticator
会把相应的token
传入Realm
,从Realm
获取身份验证信息,如果没有返回 / 抛出异常表示身份验证成功了。此处可以配置多个Realm
,将按照相应的顺序及策略进行访问。