technology-note/java/web相关/2.spring boot SSO单点登录.md
2021-07-20 15:13:03 +08:00

142 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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://sqiniupic.fleyx.com/blog/20201027201713.png)
  最近我们组要给负责的一个管理系统 A 集成另外一个系统 B为了让用户使用更加便捷避免多个系统重复登录希望能够达到这样的效果——用户只需登录一次就能够在这两个系统中进行操作。很明显这就是**单点登录(Single Sign-On)**达到的效果,正好可以明目张胆的学一波单点登录知识。
本篇主要内容如下:
- SSO 介绍
- SSO 的几种实现方式对比
- 基于 JWT 的 spring boot 单点登录实战
**注意:**
  SSO 这个概念已经出现很久很久了,目前各种平台都有非常成熟的实现,比如`OpenSSO``OpenAM``Kerberos``CAS`等,当然很多时候成熟意味着复杂。本文不讨论那些成熟方案的使用,也不考虑 SSO 在 CS 应用中的使用。
# 什么是 SSO
  单点点说就是一次登录后可免登陆访问其他的可信平台。比如我们登录淘宝网后再打开天猫首页可以发现已经是登录状态了。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 学习](http://tapme.top/blog/detail/2019-03-05-13-41))。
&emsp;&emsp;cookie 允许同域名(或者父子域名)的不同端口中共享 cookie,这点和 http 的同域策略不一样http 请求只要协议、域名、端口不完全相同便认为跨域)。因此只需将多个应用前台页面部署到相同的域名(或者父子域名),然后共享 session 便能够实现单点登录。架构如下:
![共享session架构](http://sqiniupic.fleyx.com/blog/20201027204149.png)
&emsp;&emsp;上面方案显而易见的限制就是不仅前台页面需要共享 cookie后台也需要共享 session可以用`jwt`来干掉 session但是又会引入新的问题这里不展开.这个方案太简单了,不作进一步说明。
## 方案二 基于回调实现
&emsp;&emsp;通过上文可以知道,要实现单点登录只需将用户的身份凭证共享给各个系统,让后台知道现在是`谁`在访问。就能实现一次登录,到处访问的效果,实在是非常方便的。在 session 机制中是共享 sessionId然后多个后台使用同一个 session 源即可。这里我们用一种新的基于 JWT 的 token 方式来实现,不了解 JWT 的可以看这篇:[java-jwt 生成与校验](http://www.tapme.top/blog/detail/2019-02-28-15-50),简单来说 jwt 可以携带无法篡改的信息(一段篡改就会校验失败),所以我们可以将用户 id 等非敏感信息直接放到 jwt 中,干掉了后台的 session。然后我们要做的就是将 jwt 共享给各个平台页面即可。系统架构如下:
![基于jwt的回调](http://sqiniupic.fleyx.com/blog/20201027204300.png)
&emsp;&emsp;此架构中,业务系统 A 和业务系统 B 之间不需要有任何联系,他们都只和 SSO 认证平台打交道,因此可以任意部署,没有同域的限制。你可能就要问了这样要怎么共享身份凭证(也就是 jwt 字符串)?这里就要通过 url 参数来进行骚操作了。文字总结来说是这样的jwt 存到认证平台前端的 localStore(不一定是 localStorecookiesessionStore 都可以),然后业务平台携带自己的回调地址跳转到认证中心的前台,认证中心的前台再将 ujwt 作为 url 参数,跳回到那个回调地址上,这样就完成了 jwt 的共享。
&emsp;&emsp;文字很可能看不懂,下面是整个过程的路程图:
![流程图](http://sqiniupic.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://sqiniupic.fleyx.com/blog/20201027205546.gif)
可以看到首次登陆是需要跳到 sso 认证中心输入用户名密码进行登陆验证的。登陆成功回跳后接口请求成功。
- 将 A 的启动端口改为 8082 后再次启动,当作系统 B
![系统B登陆gif](http://sqiniupic.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)
**本篇原创发布于:**[www.tapme.top/blog/detail/2019-03-01-18-52](https://www.tapme.top/blog/detail/2019-03-01-18-52)