目录
设计思路
举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以访问其它所有系统。
我们需要搭建一个sso认证中心,就是不管那个系统需要登录认证都跳转到这个认证中心进行登录和认证。把SSO看作是一个单独的认证服务。不处理业务逻辑,只是做用户信息的管理以及授权给第三方应用。
单点登录的问题:
首先我们分析一下多个系统之间,为什么无法同步登录状态?实现单点登录就要解决这两个问题
- 前端的
Token
无法在多个系统下共享。 - 后端的
Session
无法在多个系统间共享。
前端同域情况下:
- 使用
共享Cookie
来解决 Token 共享问题。 - 使用
Redis
来解决 Session 共享问题。
所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名stp.com
下的Cookie,在s1.stp.com
、s2.stp.com
等子域名都是可以共享访问的。 所以后端在设置cookie的作用域的时候可以将其写入父级域名比如stp.com
。
我们在satoken框架配置文件中设置cookie的作用域为主域,同时后端服务器连接同一个redis
# 配置 Cookie 作用域 sa-token.cookie.domain=stp.com
之后,比如在s1.stp.com
的子系统1登录之后返回一个在父域名stp.com
下的Cookie 的token,在s2.stp.com
的子系统2去访问的时候带着这个token,就不需要再进行登录,从而实现了单点登录。
搭建sso认证中心
引入依赖
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version> </dependency> <!-- Sa-Token 插件:整合SSO --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-sso</artifactId> <version>1.34.0</version> </dependency> <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.34.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
开放处理认证的接口
新建 SsoServerController
,所有对系统资源的请求都先到这里的接口进行处理:
/** * Sa-Token-SSO Server端 Controller */ @RestController public class SsoServerController { /* * SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口) */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoProcessor.instance.serverDister(); } /** * 配置SSO相关参数 */ @Autowired private void configSso(SaSsoConfig sso) { // 配置:未登录时返回的去登录的界面 sso.setNotLoginView(() -> { String msg = "当前会话在SSO-Server端尚未登录,请先访问" + "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>" + "进行登录之后,刷新页面开始授权"; return msg; }); // 配置:登录处理函数 sso.setDoLoginHandle((name, pwd) -> { // 此处仅做模拟登录,真实环境应该查询数据进行登录 if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }); } }
配置信息
# 端口 server.port=9000 ################## Sa-Token 配置 ################## # ------- SSO-模式一相关配置 (非模式一不需要配置) # 配置 Cookie 作用域为主域 sa-token.cookie.domain=stp.com ################## Redis配置 (使用Redis来同步会话) ################## # Redis数据库索引(默认为0) spring.redis.database=1 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 关闭 forest 请求日志打印 forest.log-enabled: false
创建启动类
@SpringBootApplication public class SaSsoServerApplication { public static void main(String[] args) { SpringApplication.run(SaSsoServerApplication.class, args); System.out.println("\n------ Sa-Token-SSO 认证中心启动成功"); } }
至此我们可以通过http://localhost:9000/sso/auth进行登录,但真实项目中我们是不会直接从浏览器访问 /sso/auth 授权地址的,我们需要在 Client 端点击登录按钮重定向而来。
搭建业务系统
引入依赖
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version> </dependency> <!-- Sa-Token 插件:整合SSO --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-sso</artifactId> <version>1.34.0</version> </dependency> <!-- Sa-Token 整合redis (使用jackson序列化方式) --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.34.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- Sa-Token插件:权限缓存与业务缓存分离 --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-alone-redis</artifactId> <version>1.34.0</version> </dependency>
创建业务系统的controller
依然是用StpUtil.isLogin()来判断是否登录
/** * Sa-Token-SSO Client端 Controller * @author kong */ @RestController public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") public String index() { String authUrl = SaSsoManager.getConfig().splicingAuthUrl(); String solUrl = SaSsoManager.getConfig().splicingSloUrl(); String str = "<h2>Sa-Token SSO-Client 应用端</h2>" + "<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" + "<p><a href=\"javascript:location.href='" + authUrl + "?mode=simple&redirect=' + encodeURIComponent(location.href);\">登录</a> " + "<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>"; return str; } // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } }
配置拦截器
我们还是用StpUtil.isLogin()或StpUtil.checkLogin()来拦截接口,若没有登录就会被拦截下来,跳转到sso认证中心进行登录,当在sso认证中心登录后StpUtil.isLogin()的结果就为true
// 登录校验 -- 用是否登录拦截所有路由,
// 在最下面的.excludePathPatterns中并排除登录等接口的拦截
SaRouter.match("/**", r -> StpUtil.checkLogin());
配置信息
# 端口 server.port=9001 ######### Sa-Token 配置 ######### # SSO-Server端-单点登录授权地址 sa-token.sso.auth-url=http://sso.stp.com:9000/sso/auth # SSO-Server端-单点注销地址 sa-token.sso.slo-url=http://sso.stp.com:9000/sso/signout # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) # Redis数据库索引 sa-token.alone-redis.database=1 # Redis服务器地址 sa-token.alone-redis.host=127.0.0.1 # Redis服务器连接端口 sa-token.alone-redis.port=6379 # Redis服务器连接密码(默认为空) sa-token.alone-redis.password= # 连接超时时间 sa-token.alone-redis.timeout=10s
业务系统启动类
/** * SSO模式一,Client端 Demo */ @SpringBootApplication public class SaSso1ClientApplication { public static void main(String[] args) { SpringApplication.run(SaSso1ClientApplication.class, args); System.out.println("\nSa-Token SSO模式一 Client端启动成功"); } }
测试
启动项目,依次访问三个应用端:
- http://s1.stp.com:9001/
- http://s2.stp.com:9001/
- http://s3.stp.com:9001/
返回的结果都是(此时业务系统的controller里的StpUtil.isLogin()结果是false)
当我们点击登录按钮,会根据配置文件里的url转到我们之前搭建好的sso认证中心
当我们在sso认证中心登录后框架会自动将信息同步到同一个redis里面,这时候刷新原来的下面三个网址,业务系统的controller里的StpUtil.isLogin()结果都是true
- http://s1.stp.com:9001/
- http://s2.stp.com:9001/
- http://s3.stp.com:9001/
前端不同域情况下:
- 使用
url重定向传播
来解决 Token 共享问题。 - 使用
Redis
来解决 Session 共享问题。
- 用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
- SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
- SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 A 接口
- 系统 A 校验 ticket,成功后正常处理业务请求
- 此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
- SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 B 接口
对浏览器来说,SSO 域下返回的数据要怎么存,才能在访问 A 的时候带上?浏览器对跨域有严格限制,cookie、localStorage 等方式都是有域限制的。
- 在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定
- 浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
- 这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
- callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
- 在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
- 访问 B 系统也是一样