sso 单点登录
什么是单点登录
举个场景,假设我们的系统被切割为 N 个部分:系统 A、系统 B、系统 C、办公系统…… 如果用户每访问一个系统都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这 N 个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。
单点登录——就是为了解决这个问题而生!
简而言之,单点登录 SSO 是整合企业系统的解决方案之一,单点登录可以做到:在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。
如何实现
我们先来分析一下,当有一个单点登录服务端和客户端时会遇到哪些困难:
1.Client 端无法直连 Server Redis 校验 ticket,取出账号 id。
2.Client 端无法与 Server 端共用一套会话,需要自行维护子会话。
3.由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销。
此时,我们需要通过 Http 请求获取会话
引入依赖
txt
<!-- Sa-Token整合Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
</dependency>
配置单点登录
txt
# sa-token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: access_token
# 关闭输出banner
is-print: false
# SSO-相关配置
sso:
# SSO-Server端 统一认证地址
auth-url: /sso/auth
# 使用Http请求校验ticket
is-http: true
# SSO-Server端 ticket校验地址
check-ticket-url: /sso/checkTicket
# 打开单点注销功能
is-slo: true
# 单点注销地址
slo-url: /sso/signout
# 接口调用秘钥
secretkey: unknown
# SSO-Server端 查询userinfo地址
userinfo-url: /sso/userinfo
# Server 端主机总地址
server-url: http://localhost:3000
# 配置客户端
client: cbb_admin
cookie:
httpOnly: true
接口通信
客户端
序号 | 客户端接口 | 说明 |
---|---|---|
1 | /sso/login | SSO-Client 端:登录地址 |
2 | /sso/logout | SSO-Client 端:单点注销地址 |
3 | /sso/logoutCall | SSO-Client 端:单点注销的回调 |
java
/**
* sso客户端控制器
*
* @author tanmignlin
* @date 2023/02/21 09:00:10
*/
@RestController
@AllArgsConstructor
@Api(tags = "单点登录客户端控制器")
public class SsoClientController {
private final AuthService authService;
private final UserService userService;
/**
* sso请求统一处理 SSO-Client端:处理所有SSO相关请求 http://{host}:{port}/sso/login --
* Client端登录地址,接受参数:back=登录后的跳转地址 http://{host}:{port}/sso/logout --
* Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址 http://{host}:{port}/sso/logoutCall
* -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
* @return {@link Object}
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.clientDister();
}
/**
* 返回SSO认证中心登录地址
* @param clientLoginUrl 客户端登录网址
* @return {@link SaResult }
* @author tanminglin
* @date 2023/02/21 13:54:31
*/
@RequestMapping("/sso/authUrl")
public R getSsoAuthUrl(String clientLoginUrl, String back) {
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, back);
return R.data(serverAuthUrl);
}
/**
* 根据ticket进行登录
* @param ticket 票
* @return {@link SaResult }
* @author tanminglin
* @date 2023/02/21 15:39:04
*/
@RequestMapping("/sso/doLoginByTicket")
public R doLoginByTicket(String ticket) {
Object loginId = SaSsoProcessor.instance.checkTicket(ticket, WebUtils.getRequest().getRequestURI());
if (loginId != null) {
// 执行B端用户登录
SysLoginUser sysLoginUser = userService.getUserById(Convert.toInt(loginId));
// 执行B端登录
SaTokenInfo saTokenInfo = authService.execLoginB(sysLoginUser, AuthDeviceTypeEnum.SSO.getValue());
return R.data(saTokenInfo);
}
return R.fail("无效ticket:" + ticket);
}
/**
* 配置SSO相关参数
* @param sso sso配置文件
* @author tanminglin
* @date 2023/02/21 20:34:07
*/
@Autowired
private void configSso(SaSsoConfig sso) {
// 配置Http请求处理器
sso.setSendHttp(url -> {
System.out.println("------ 发起请求:" + url);
return Forest.get(url).executeAsString();
});
}
}
服务端
序号 | 客户端接口 | 说明 |
---|---|---|
1 | /sso/auth | SSO-Server 端:授权地址 |
2 | /sso/doLogin | SSO-Server 端:RestAPI 登录接口 |
3 | /sso/checkTicket | SSO-Server 端:校验 ticket 获取账号 id |
4 | /sso/signout | SSO-Server 端:单点注销地址 |
java
/**
* Sa-Token-SSO Server端 Controller
*
* @author kong
*
*/
@RestController
@AllArgsConstructor
public class SsoServerController {
private final UserService userService;
private final AppClientService appClientService;
private final SsoApplicationMapper ssoApplicationMapper;
private final JdbcService jdbcService;
private final String loginPage = "login.html";
/**
* SSO-Server端:处理所有SSO相关请求 http://{host}:{port}/sso/auth --
* 单点登录授权地址,接受参数:redirect=授权重定向地址 http://{host}:{port}/sso/doLogin --
* 账号密码登录接口,接受参数:name、pwd http://{host}:{port}/sso/checkTicket --
* Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选]
* http://{host}:{port}/sso/signout --
* 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、secretkey=接口调用秘钥
* @return {@link Object}
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
// 判断是否是以前基于oauth2的协议请求
return SaSsoProcessor.instance.serverDister();
}
/**
* 配置SSO相关参数
* @param sso sso
*/
@Autowired
private void configSso(SaSsoConfig sso) {
// 配置:未登录时返回的View
sso.setNotLoginView(() -> {
SaRequest saRequest = SaHolder.getRequest();
// 根据请求参数判断返回的登录视图 loginPageCode
// 未登录的视图
return new ModelAndView(loginPage).addObject("loginCode", saRequest.getParam("loginCode"));
});
// 配置:登录处理函数
sso.setDoLoginHandle((name, pwd) -> {
StpUtil.login("1", "PC");
return R.success("登录成功");
});
// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
sso.setSendHttp(url -> {
try {
// 发起 http 请求
// System.out.println("------ 发起请求:" + url);
return Forest.get(url).executeAsString();
}
catch (Exception e) {
e.printStackTrace();
return null;
}
});
}
/**
* 同步组织切换
* @return {@link Object}
*/
@PostMapping("/sso/org/change")
public Object syncOrgChange(PersonRecentData personRecentData) {
// 验证参数
SaSsoUtil.checkSign(SaHolder.getRequest());
// 验证参数
CbbOauth2Cache.putPersonRecentData(personRecentData);
// 生成code,并且返回,这个code有效期30秒
return R.data(CbbOauth2Cache.putPersonRecentDataIndex(personRecentData.getPersonId()));
}
/**
* 获取Userinfo信息:昵称、头像、性别等等
* @return {@link UserView}
*/
@RequestMapping("/sso/userinfo")
public R<UserView> userinfo() {
// 验证参数
SaSsoUtil.checkSign(SaHolder.getRequest());
Object loginId = StpUtil.getLoginId();
// 账号信息
UserView userView = userService.getUserVoById(Integer.parseInt(String.valueOf(loginId)));
return R.data(userView);
}
/**
* 清空 client信息
* @return {@link R}
*/
@RequestMapping("/sso/client/change")
public R clearOauthClient() {
SaSsoUtil.checkSign(SaHolder.getRequest());
appClientService.syncOauthClientCache();
return R.success("清空成功");
}
}