一、引文

用户身份认证的方式

1、单一服务器模式

一般过程如下

  1. 用户向服务器发送用户名和密码。
  2. 验证服务器后,相关数据(如用户名,用户角色等)将保存在当前会话(session)中。
  3. 服务器向用户返回session_id,session 信息都会写入到用户的 Cookie
  4. 用户的每个后续请求都将通过在 Cookie 中取出 session_id 传给服务器
  5. 服务器收到session_id对比之前保存的数据,确认用户的身份。

缺点

  • 单点性能压力,无法扩展。
  • 分布式架构中,需要 session 共享方案,session 共享方案存在性能瓶颈。

session 共享方案

session 广播:性能瓶颈,不推荐

redis 代替 session:推荐,性能高

2、SSO(Single Sign On)模式

单点登录英文全称 Single Sign On,简称就是 SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。

CAS 框架:CAS(Central Authentication Service)是实现 SSO 单点登录的框架

简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

严格来说,OAuth2 不是一个标准协议,而是一个安全的授权框架。它详细描述了系统中不同角色、用户、服务前端应用(比如 API),以及客户端(比如网站或移动 App)之间怎么实现相互认证。

实现方式:CAS 单点登录、OAuth2

  • 如图所示,图中有 3 个系统,分别是业务 A、业务 B、和 SSO。
  • 业务 A、业务 B 没有登录模块。
  • 而 SSO 只有登录模块,没有其他的业务模块。

一般过程如下:

  1. 当业务 A、业务 B 需要登录时,将跳到 SSO 系统。
  2. SSO 从用户信息数据库中获取用户信息并校验用户信息,SSO 系统完成登录。
  3. 然后将用户信息存入缓存(例如 redis)。
  4. 当用户访问业务 A 或业务 B,需要判断用户是否登录时,将跳转到 SSO 系统中进行用户身份验证,SSO 判断缓存中是否存在用户身份信息。
  5. 这样,只要其中一个系统完成登录,其他的应用系统也就随之登录了。这就是单点登录(SSO)的定义。

优点 :

用户身份信息独立管理,更好的分布式管理。可以自己扩展安全策略

缺点:

认证服务器访问压力较大。

3、Token 模式


优点:

  • 无状态: token 是无状态,session 是有状态的
  • 基于标准化:API 可以采用标准化的 JSON Web Token (JWT)

缺点:

  • 占用带宽
  • 无法在服务器端销毁

二、访问令牌的类型

自包含令牌:JWT 令牌

1、什么是 JWT 令牌

JWTJSON Web Token的缩写,即JSON Web令牌,是一种自包含令牌。

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

JWT 官网有一张图描述了 JWT 的认证过程

JWT 的作用

  • JWT 最重要的作用就是对 token 信息的防伪作用

JWT 的原理

  • 一个 JWT 由三个部分组成:JWT 头、有效载荷、签名哈希。最后由这三者组合进行 base64 编码得到JWT

典型的,一个 JWT 看起来如下图

该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串

每一个子串表示了一个功能块,总共有以下三个部分:JWT 头有效载荷签名哈希

①JWT 头

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

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

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256

typ属性表示令牌的类型,JWT 令牌统一写为JWT。最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存

② 有效载荷

有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT 指定七个默认字段供选择。

1
2
3
4
5
6
7
sub: 主题
iss: jwt签发者
aud: 接收jwt的一方
iat: jwt的签发时间
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

除以上默认字段外,我们还可以自定义私有字段,如下例:

1
2
3
4
{
"name": "shiguang",
"admin": true
}

注意:默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON 对象也使用 Base64 URL 算法转换为字符串保存。

③ 签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头(JWT 头)中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)    ==>   签名hash

在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个 JWT 对象。

④Base64URL 算法

如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。

作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx)。

Base64 中用的三个字符是”+”,”/“和”=”,由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换:

“=”去掉

“+”用”-“替换

“/“用”_“替换

这就是 Base64URL 算法。

注意:base64 编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把 base64 编码解成明文,所以不要在 JWT 中放入涉及私密的信息。

2、JWT 的用法

客户端接收服务器返回的 JWT,将其存储在 CookielocalStorage 中。

此后,客户端将在与服务器交互中都会带JWT

如果将它存储在 Cookie 中,就可以自动发送,但是不会跨域,因此一般是将它放入 HTTP 请求的Header Authorization字段中。

当跨域时,也可以将 JWT 放置于 POST 请求的数据主体中。

三、JWT 问题和趋势

  1. JWT 默认不加密,但可以加密。生成原始令牌后,可以使用该令牌再次对其进行加密。
  2. 当 JWT 未加密时,一些私密数据无法通过 JWT 传输。
  3. JWT 不仅可用于认证,还可用于信息交换。善用 JWT 有助于减少服务器请求数据库的次数。
  4. JWT 的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦 JWT 签发,在有效期内将会一直有效
  5. JWT 本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT 的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行身份验证。
  6. 为了减少盗用和窃取,JWT 不建议使用 HTTP 协议来传输代码,而是使用加密的 HTTPS 协议进行传输。

四、jwt 测试

创建 Maven 项目

1、依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

2、生成 token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 过期时间,24小时,时间单位:毫秒
private static long tokenExpiration = 24 * 60 * 60 * 1000;
// 秘钥
private static String tokenSignKey = "ShiGuang";

@Test
public void testCreateToken() {
String token = Jwts.builder()
// 令牌类型
.setHeaderParam("type", "JWT")
// 签名算法
.setHeaderParam("alg", "HS256")
// 令牌主题
.setSubject("Hello JWT")
// 签发者
.setIssuer("sg")
// 接收者
.setAudience("wf")
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
// 20秒后可用
.setNotBefore(new Date(System.currentTimeMillis() + 20 * 1000))
.setId(UUID.randomUUID().toString())

// 自定义字段
.claim("name", "shiguang")
.claim("admin", "true")

.signWith(SignatureAlgorithm.HS256, tokenSignKey)//签名哈希
.compact(); //转换成字符串

System.out.println(token);
}

2、解析 token

利用上面生成的 token 进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void testGetUserInfo() {

// 生成的token
String token = "token";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);

Claims claims = claimsJws.getBody();

String subject = claims.getSubject();
String issuer = claims.getIssuer();
String audience = claims.getAudience();
Date issuedAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
Date notBefore = claims.getNotBefore();
String id = claims.getId();

System.out.println(subject);
System.out.println(issuer);
System.out.println(audience);
System.out.println(issuedAt);
System.out.println(expiration);
System.out.println(notBefore);
System.out.println(id);

String name = (String) claims.get("name");
String admin = (String) claims.get("admin");

System.out.println(name);
System.out.println(admin);
}