---
id: "2019-03-01-18-52"
date: "2019/03/01 18:52"
title: "spring boot 基于JWT实现单点登录"
tags: ["spring-boot", "SSO", "单点登录", "jwt"]
categories:
  - "java"
  - "spring boot学习"
---

![塞尔达，林克](http://qiniupic.fleyx.com/blog/20201027201713.png)

&emsp;&emsp;最近我们组要给负责的一个管理系统 A 集成另外一个系统 B，为了让用户使用更加便捷，避免多个系统重复登录，希望能够达到这样的效果——用户只需登录一次就能够在这两个系统中进行操作。很明显这就是**单点登录(Single Sign-On)**达到的效果,正好可以明目张胆的学一波单点登录知识。

本篇主要内容如下：

- SSO 介绍
- SSO 的几种实现方式对比
- 基于 JWT 的 spring boot 单点登录实战

**注意:**
&emsp;&emsp;SSO 这个概念已经出现很久很久了，目前各种平台都有非常成熟的实现，比如`OpenSSO`，`OpenAM`，`Kerberos`，`CAS`等,当然很多时候成熟意味着复杂。本文不讨论那些成熟方案的使用,也不考虑 SSO 在 CS 应用中的使用。

# 什么是 SSO

&emsp;&emsp;单点点说就是：一次登录后可免登陆访问其他的可信平台。比如我们登录淘宝网后，再打开天猫首页可以发现已经是登录状态了。SSO 是一种比较流行的服务于企业业务整合的一种解决方案。

<!-- more -->

# 如何实现 SSO

&emsp;&emsp;我们都知道目前的 http 协议是无状态的，也就是第一次请求和第二次请求是完全独立，不相关的，但现实中我们的业务逻辑都是有状态的，这样就引入了 cookie-session 的机制来维护状态，浏览器端存储一个 sessionId,后台存储跟该 sessionId 相关的数据。每次向后台发起请求时都携带此 sessionId 就能维持状态了。然后就有了 cookie，浏览器在发送请求时自动将 cookie 中的数据放到请求中，发给服务端，无需手动设置。

&emsp;&emsp;然后我们可以考虑考虑实现 SSO 的核心是什么？答案就是如何让一个平台 A 登录后，其他的平台也能获取到平台 A 的登录信息（在 cookie-session 机制中就是 sessionId）。

## 方案一 共享 cookie

&emsp;&emsp;基于 cookie-session 机制的系统中，登录系统后会返回一个 sessionId 存储在 cookie 中，如果我们能够让另外一个系统也能获取到这个 cookie，不就获取到凭证信息了，无需再次登录。刚好浏览器的 cookie 可以实现这样的效果(详见[web 跨域及 cookie 学习](https://blog.fleyx.com/blog/detail/2019-03-05-13-41))。

&emsp;&emsp;cookie 允许同域名(或者父子域名)的不同端口中共享 cookie,这点和 http 的同域策略不一样（http 请求只要协议、域名、端口不完全相同便认为跨域）。因此只需将多个应用前台页面部署到相同的域名（或者父子域名），然后共享 session 便能够实现单点登录。架构如下：

![共享session架构](http://qiniupic.fleyx.com/blog/20201027204149.png)

&emsp;&emsp;上面方案显而易见的限制就是不仅前台页面需要共享 cookie，后台也需要共享 session（可以用`jwt`来干掉 session，但是又会引入新的问题，这里不展开）.这个方案太简单了，不作进一步说明。

## 方案二 基于回调实现

&emsp;&emsp;通过上文可以知道，要实现单点登录只需将用户的身份凭证共享给各个系统，让后台知道现在是`谁`在访问。就能实现一次登录，到处访问的效果，实在是非常方便的。在 session 机制中是共享 sessionId，然后多个后台使用同一个 session 源即可。这里我们用一种新的基于 JWT 的 token 方式来实现，不了解 JWT 的可以看这篇：[java-jwt 生成与校验](http://blog.fleyx.com/blog/detail/2019-02-28-15-50),简单来说 jwt 可以携带无法篡改的信息（一段篡改就会校验失败),所以我们可以将用户 id 等非敏感信息直接放到 jwt 中，干掉了后台的 session。然后我们要做的就是将 jwt 共享给各个平台页面即可。系统架构如下：

![基于jwt的回调](http://qiniupic.fleyx.com/blog/20201027204300.png)

&emsp;&emsp;此架构中，业务系统 A 和业务系统 B 之间不需要有任何联系，他们都只和 SSO 认证平台打交道，因此可以任意部署，没有同域的限制。你可能就要问了这样要怎么共享身份凭证（也就是 jwt 字符串）？这里就要通过 url 参数来进行骚操作了。文字总结来说是这样的：jwt 存到认证平台前端的 localStore(不一定是 localStore，cookie，sessionStore 都可以),然后业务平台携带自己的回调地址跳转到认证中心的前台，认证中心的前台再将 ujwt 作为 url 参数，跳回到那个回调地址上，这样就完成了 jwt 的共享。

&emsp;&emsp;文字很可能看不懂，下面是整个过程的路程图：

![流程图](http://qiniupic.fleyx.com/blog/20201027204342.png)

相信通过上面的流程图你应该能大概看明白，jwt 是如何共享了的吧，还看不懂的继续看下来，下面上一个 spring boot 实现的简易 SSO 认证。主要有两个系统：SSO 认证中心，系统 A（系统 A 换不同端口运行就是系统 A、B、C、D 了).

## 实战

### 实现 SSO 认证中心

&emsp;&emsp;spring boot 框架先搭起来，由于是简易项目，除 spring boot web 基本依赖，只需要如下的额外依赖：

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.4</version>
</dependency>
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.7.0</version>
</dependency>
```

完整的 POM 文件，请到 github 上查看.

#### 后台实现

&emsp;&emsp;后台做的事情并不多，只有以下 5 个方法：

- `/login` : 登录成功后签发一个 jwt token<br/>在 demo 中只是简单对比用户名密码如果是一样的认为登录成功，返回 token
- `/checkJwt` : 检查 jwt 的有效性<br/>检查传来的 jwt-token 是否有效，返回失效的 jwt 列表
- `/refreshjwt` : 刷新 jwt<br/>判断该 jwt 是否快要过期，如果快要过期，生成一个新的 jwt 返回
- `/inValid` : 让某个 jwt 失效<br/>jwt 如何失效一直是一个比较麻烦的问题，各有利弊。本例中采用的是为每个 jwt 生成一个随机的秘钥 secret，将 jwt--secret 保存到 redis 中，想要让某个 jwt 失效，只需将该记录在 redis 中删除即可（这样在解密时便无法获取到 secret）。但是这样让无状态的认证机制变成有状态了（记录了 jwt 和 secret 的对应关系）。

&emsp;&emsp;总结来说 SSO 后台主要只做了两件事:验证用户名密码返回 jwt；验证 jwt 是否合法。具体代码查看 github 上 sso 目录下的代码。

#### 前台实现

&emsp;&emsp;前台的逻辑较为复杂，不是那么容易理解，不明白的多看几遍上面的流程图。

&emsp;&emsp;再次回到 SSO 的重点：分享登录状态。要如何在前台将登录状态(在这里就是 jwt 字符串)分享出去呢？由于浏览器的限制，除了 cookie 外没有直接共享数据的办法。既然没有直接共享，那肯定是有间接的办法的！

&emsp;&emsp;这个办法就是**回调**。系统 A 的前台在跳转到 SSO 的前台时，将当前路径作为 url 参数传递给 sso 前台，sso 前台在获取到 jwt 后，再跳转到系统 A 传过来的 url 路径上，并带上 jwt 作为 url 参数。这就完成了 jwt 的一次共享，从 sso 共享到系统 A。

打个比方：你点了个外卖，别人要怎么把外卖给你呢？显然你会留下的地址，让别人带上饭送到这个地址，然后你就能享用美食了。这和 jwt 的传递非常相识了。

系统 A 说：我要 jwt，快把它送到http://localhost:8081/test1/这个地址上。

SSO 说：好嘞，这个地址是合法的可以送 jwt 过去，这就跳转过去：http://localhost:8081/test1/?jwt=abcdefj.asdf.asdfasf

系统 A 说：不错不错，真香。

&emsp;&emsp;要注意这里**有个坑**就是：如果另外一个恶意系统 C 安装相同的格式跳转到 SSO，想要获取 jwt，这显然是不应该给它的。所以在回跳回去的时候要判断一下这个回调地址是不是合法的，能不能给 jwt 给它，可以向后台请求判断也可以在 sso 前台直接写死合法的地址。在 demo 是没有这个判断过程的。

### 实现业务系统

&emsp;&emsp;业务系统代码非常简单，主要是用了一个拦截器，拦截 http 请求，提取出 token 向 sso 认证中心验证 token 是否有效，有效放行，否则返回错误给前端。太简单也不贴代码了，到 github 上看看就明白了。

# 效果

&emsp;&emsp;上面说了一大串都是原理了，其实这个难也就难在原理部分，代码实现并没有那么复杂。这里就不贴代码了，有需要直接到 github 上看。

&emsp;&emsp;这里上几个效果图：

- 系统 A 首次登陆系统
  ![系统A登陆gif](http://qiniupic.fleyx.com/blog/20201027205546.gif)

可以看到首次登陆是需要跳到 sso 认证中心输入用户名密码进行登陆验证的。登陆成功回跳后接口请求成功。

- 将 A 的启动端口改为 8082 后再次启动，当作系统 B
  ![系统B登陆gif](http://qiniupic.fleyx.com/blog/20201028091355.gif)

可以看到这次是无需登陆的，跳到认证中心后就马上跳回了，如果去掉 alert 一般是看不出跳转过程的。

最后在任意一个系统注销，都会让所有的系统推出登陆。

可以看到，在系统 A 登录系统后，系统 B，系统 C 都不再需要输入用户名密码进行登录。如果速度足够快甚至都注意不到调到 SSO 再跳回来的过程。

**源码：**[github](https://github.com/FleyX/demo-project/tree/master/1.SSO%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95)

