介绍
举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。
单点登录——就是为了解决这个问题而生!
简而言之,单点登录可以做到: 在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。
—Sa-Token
官方文档:https://sa-token.cc/doc.html#/sso/readme
模式一:采用共享 Cookie 来做到前端 Token 的共享,从而达到后端的 Session 会话共享。
模式二:采用 URL 重定向,以 ticket 码为授权中介,做到多个系统间的会话传播。
模式三:采用 Http 请求主动查询会话,做到 Client 端与 Server 端的会话同步。
搭建统一认证中心 SSO-Server 服务端
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.44.0</sa-token.version>
</properties>
<dependencies>
<!-- SpringBoot Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 插件:整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Sa-Token 插件:整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa-token.version}</version>
</dependency>
</dependencies>
认证中心配置文件
# 端口
# 端口
server:
port: 9000
# Sa-Token 配置
sa-token:
# 打印操作日志
is-log: true
# SSO-Server 配置
sso-server:
# Ticket有效期 (单位: 秒),默认五分钟
ticket-timeout: 300
# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址
# home-route: /home
# 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数)
allow-anon-client: true
# 所有允许的授权回调地址 (匿名 client 使用)
allow-url: "*"
# API 接口调用秘钥 (全局默认 + 匿名 client 使用)
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# 应用列表:配置接入的应用信息
clients:
# 应用 sso-client1:采用模式一对接 (同域、同Redis)
sso-client1:
client: sso-client1
allow-url: "*"
# 应用 sso-client2:采用模式二对接 (跨域、同Redis)
sso-client2:
client: sso-client2
# allow-url: "*" #生产模式禁止使用
allow-url: http://127.0.0.1:82/sso-login.html , http://127.0.0.1:81/sso-login.html
secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
spring:
# Redis配置 (SSO模式一和模式二使用 Redis 来同步会话)
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
forest:
# 关闭 forest 请求日志打印
log-enabled: false
跨域配置
处理统一认证中心的前端跨域问题
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
*/
@Configuration
public class SaTokenConfigure {
/**
* CORS 跨域处理策略
*/
@Bean
public SaCorsHandleFunction corsHandle() {
return (req, res, sto) -> {
res.
// 允许指定域访问跨域资源
setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> {})
.back();
};
}
}
异常处理
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
登录接口
在统一身份认证点击登录后会进入该方法
@RestController
public class SsoServerController {
/**
* SSO-Server端:处理所有SSO相关请求
* http://{host}:{port}/sso/auth -- 单点登录授权地址
* http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd
* http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开)
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoServerProcessor.instance.dister();
}
// 配置SSO相关参数
@Autowired
private void configSso(SaSsoServerTemplate ssoServerTemplate) {
ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {
//可以在方法里通过 SaHolder.getRequest().getParam("xxx") 来获取前端提交的其它参数。
// 此处仅做模拟登录,真实环境应该查询数据库进行登录
if("sa".equals(name) && "123456".equals(pwd)) {
String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32));
StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId));
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
};
// 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口)
ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> {
System.out.println("收到消息:" + message);
// 自定义返回结果(模拟)
return SaResult.ok()
.set("id", message.get("loginId"))
.set("name", "LinXiaoYu")
.set("sex", "女")
.set("age", 18);
});
}
}
以上就是统一认证中心的后端搭建
子系统登录认证
搭建好了统一认证中心的后端后,其他的系统就可以与他进行对接。
依赖
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.44.0</sa-token.version>
</properties>
<dependencies>
<!-- SpringBoot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<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>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 插件:整合 Forest 请求工具 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa-token.version}</version>
</dependency>
</dependencies>
配置文件
# 端口
server:
port: 9002
# sa-token配置
sa-token:
# 打印操作日志
is-log: true
# SSO-相关配置
sso-client:
# 应用标识
client: sso-client2
# SSO-Server 端主机地址
server-url: http://sa-sso-server.com:9000
# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在)
auth-url: http://127.0.0.1/sso-auth.html #统一认证中心的前端地址
# API 接口调用秘钥 (单点注销时会用到)
secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis)
# 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖
alone-redis:
# Redis数据库索引
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
forest:
# 关闭 forest 请求日志打印
log-enabled: false
子系统信息接口
如果不需要前后端分离架构下集成SSO,可删除此包下所有代码
/**
* 前后台分离架构下集成SSO所需的代码 (SSO-Client端)
* <p>(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)</p> *
*/
@RestController
public class H5Controller {
// 判断当前是否登录
@RequestMapping("/sso/isLogin")
public Object isLogin() {
return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull());
}
// 返回SSO认证中心登录地址
@RequestMapping("/sso/getSsoAuthUrl")
public SaResult getSsoAuthUrl(String clientLoginUrl) {
String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, "");
return SaResult.data(serverAuthUrl);
}
// 根据 ticket 进行登录
@RequestMapping("/sso/doLoginByTicket")
public SaResult doLoginByTicket(String ticket) {
SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);
StpUtil.login(ctr.loginId, new SaLoginParameter()
.setTimeout(ctr.remainTokenTimeout)
.setDeviceId(ctr.deviceId)
);
return SaResult.data(StpUtil.getTokenValue());
}
}
此外 跨域-异常处理 均与服务端的一致 !!!
子系统登录接口
/**
* Sa-Token-SSO Client端 Controller
* @author click33
*/
@RestController
public class SsoClientController {
/*
* SSO-Client端:处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client 端登录地址
* http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开)
* http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoClientProcessor.instance.dister();
}
// 配置SSO相关参数
@Autowired
private void configSso(SaSsoClientTemplate ssoClientTemplate) {
}
// 当前应用独自注销 (不退出其它应用)
@RequestMapping("/sso/logoutByAlone")
public Object logoutByAlone() {
StpUtil.logout();
return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());
}
// 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端
@RequestMapping("/sso/myInfo")
public Object myInfo() {
// 如果尚未登录
if( ! StpUtil.isLogin()) {
return "尚未登录,无法获取";
}
// 获取本地 loginId
Object loginId = StpUtil.getLoginId();
// 推送消息
SaSsoMessage message = new SaSsoMessage();
message.setType("userinfo");
message.set("loginId", loginId);
SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);
// 返回给前端
return result;
}
}
前端请求顺序
官方流程图