SSO 单点登录
在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。但随着企业的发展,用到的系统随之增多,运营人员在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于运营人员来说,很不方便。
于是,就想到是不是可以在一个系统登录,其他系统就不用登录了呢?这就是单点登录要解决的问题。单点登录英文全称 Single Sign On,简称就是 SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
1、认证机制
1.1、普通认证
如上图所示,用户在浏览器(Browser)中访问一个应用,这个应用需要登录,填写完用户名和密码后,登陆系统进行权限认证,如果通过,系统会在这个用户的 Session 中标记登陆信息(登陆状态、用户名等数据),同时在浏览器(Browser)中写入 Cookie,这个 Cookie 是这个用户在本地的唯一标识。
下次我们再访问这个应用的时候, 请求中会带上这个 Cookie,服务端会根据这个 Cookie 找到对应的 session,通过 session 来判断这个用户是否登录。如果不做特殊配置,这个 Cookie 的名字叫做 jsessionid,值在服务端是唯一的。
这样的登陆方法的优点是很简单,javaweb 中提供对 Session 和 Cookie 的管理支持,鉴权实现很容易。但是他也有很致命的缺点:
- Cookie 不能进行跨域携带,也就是一个服务器发放的 Cookie 是不能支持登陆其他系统时携带的。
- 服务器的 Session 之间也是不共享的,Cookie 和 Session 在创建时就是绑定的关系,只有使用配套的才能进行校验(其他的缺点不是这里讨论的范畴)。
也就是再登陆相同厂家的不同系统时,还是得重新进行上面的一系列操作,对于使用者而言十分的不友好。
1.2、域名控制
Cookie 不能够进行跨域携带,但是我们可以将 Cookie 的域设置为顶域,这样访问其子域就可以正常携带 Cookie 了(也只有子域才能进行携带),虽然说可以在 Cookie 中设置不是自己的域,也能只能设置顶域,如果想在自己的系统中设置 baidu.com Cookie 域是行不通的,因为只支持设置顶域和自己的域。
传统的项目的 Session 是存在服务端的,这样别的子系统服务器并没有这个 Session,无法完成共享,这个解决方案就是将 Session 进行外存,子系统之间使用的时候就直接用携带的 Cookie 去取 对应的 Cookie 验证即可。
上面的图就描述了利用设置顶域和外存的方式来完成相同子系统之间的单点登录。第一次登录的时候进行 Cookie 和 Session 的设置和保存,这样在登录第二个系统时将顶域 Cookie 携带上,按照 ID 从数据库中取出 Session 进行判断,如果有对应的 Session 则表示已经在受认的其他系统中完成登录,可以直接进行权限的放行,而不是再重新的进行登录。
1.3、Token 认证
最近几年由于单页app、web APIs等的兴起,基于token的身份验证开始流行。当我们谈到利用 token 进行认证,我们一般说的就是利用 JSON Web Tokens(JWTs) 进行认证。虽然有不同的方式来实现 token,事实上,JWTs 已成为标准,因此在本文中 token 与 JWTs 将会是一个事件。
基于 token 的身份验证是无状态的,服务器不需要记录哪些用户已经登录或者哪些 JWTs 已经处理。每个发送到服务器的请求都会带上一个 token(可以从请求头中携带,也可以作为参数进行携带),服务器利用这个 token 检查确认请求的真实性。
这里可以把 token 理解成一张门票。主办方每次只需要检查你这张门票的有效性,不需要知道你这张门票是在哪里买的,从谁买的,什么时候买的等等。而且还可以根据门票的一些信息给与不同的消费权限,例如消费等级、VIP 之类的。
2、JWT
JSON Web Token 是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息(所谓的加密传输)。
2.1、组成结构
如图即是 JWT 的组成,实质上就是一个由 头部 、 荷载 、 签证 三个字符串由 . 进行连接而成的一个字符串。
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoie1wiaWRcIjoxfSIsImlzcyI6ImFkbWluIiwiaWF0IjoxNjUxMzkzMTcyLCJleHAiOjE2NTEzOTY3NzJ9.MR6dSbt8koh1KvUB5RHGM6t6sd3RYEciqf6K7wK7eY4
2.2、header
头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个 JSON数据对象。
{
"typ":"JWT",
"alg":"HS256"
}
表示的就是当前类型是 JWT 然后使用的是 HS256 加密算子。
HS256 是一种对称算法,双方之间仅共享一个密钥,所以必须注意确保密钥不被泄密。
base64 加密之后的字符串为:eyJhbGciOiJIUzI1NiJ9
头部和荷载中的数据都会经过 base64 进行加密之后进行使用,这里就需要简单了解一些这个加密是怎么的一个原理。
Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2 的 6 次方等于 64,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节需要用 4 个可打印字符来表示。
2.3、payload
载荷就是存放有效信息的地方,就想是集装箱一样,这些有效信息包含三个部分。
-
标准中注册的声明(建议但不强制使用)
iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间(常用属性) nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 -
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密,本身的加密也不是很完善,容易造成泄漏。
-
私有的声明
私有的声明是签发者和消费者共同定义的声明,一般不建议存放敏感信息,因为使用的是通用的对称加密,意味着该部分的信息可以归类为明文信息。
其实指的就是自定义的
Claim(自定义 Map 数据)。同 JWT 中规定的相比,自定义的不会自动进行验证,除非明确的告诉接收方要进行验证和验证规则;而规定的数据接收方在接收到之后就知道该怎么去对这些数据进行验证。{ "sub":"object", "iss":"admin", }
2.4、signature
JWT 的第三部分是一个签证信息,这个签证信息由三部分混合加密组成(加密后的头部 + 加密后的荷载 + 秘钥) ,然后通过头部指定的加密算法进行加密得到最后一串密文。
秘钥是保存在服务器端的,是用来签发和验证 JWT,它就是你服务端的私钥,在任何场景都不应该流露出去。为了增强原有的秘钥,还可以使用一个对称加密来进行秘钥的加密。
encodedKey = Base64.getEncoder().encode(TokenUtil.JWT_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
3、JJWT
JJWT 是一个提供端到端的 JWT 创建和验证的工具。提供了开箱即用的 API,使用时只需要按照功能进行数据的配置即可。
3.1、配置依赖
这是一个开源的项目,而且已经交由 Maven 去管理,直接进行引用即可。
<dependencies>
<!--鉴权-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
3.2、Token 生成
按照项目的规则进行数据生成即可。
public static String createJWT(String id, String subject, long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long now = System.currentTimeMillis();
Date date = new Date(now);
if (ttlMillis <= 0) {
ttlMillis = TokenUtil.JWT_TTL;
}
long expire = now + ttlMillis;
Date expDate = new Date(expire);
// 生成秘钥
SecretKey secretKey = generalKey();
// 封装 Jwt 令牌信息
JwtBuilder builder = Jwts.builder()
.setId(id) // 唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("admin") // 签发者
.setIssuedAt(date) // 签发时间
.signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙
.setExpiration(expDate); // 设置过期时间
return builder.compact();
}
3.3、Token 解析
Token 是前后端进行交流的一个工具,后端信任之后会开放权限,后端必定就需要能够解析这个 Token。这里面也提供了一个 parser() 函数来进行解密,只需要将加密的密钥和 Token 传送即可,使用很简单。
解密之后会拿到一个 Claims 对象,这里面就存放有之前创建时设置的一些属性信息和自定义的验证信息。
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
4、Token 认证
上面我们已经弄明白了 Token 进行验证的原理,这样就可以使用在我们的网关鉴权当中,而不再使用传统的那种认证机制。具体的代码这里就不再演示,实现较为简单,看一下流程图即可(可能不太完善,仅供参考)。