将 sso 模块从 core 包下拆分出来,并细分 sso 模块异常

This commit is contained in:
click33
2022-04-26 00:47:31 +08:00
parent f5cbe0616e
commit a3c8b2ade2
39 changed files with 511 additions and 117 deletions

13
sa-token-plugin/sa-token-sso/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
target/
node_modules/
bin/
.settings/
unpackage/
.classpath
.project
.factorypath
.idea/
.iml

View File

@@ -0,0 +1,30 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-plugin</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<packaging>jar</packaging>
<name>sa-token-sso</name>
<artifactId>sa-token-sso</artifactId>
<description>sa-token realization sso</description>
<dependencies>
<!-- sa-token-core -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,365 @@
package cn.dev33.satoken.config;
import java.io.Serializable;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-Token SSO 单点登录模块 配置类 Model
* @author kong
*
*/
public class SaSsoConfig implements Serializable {
private static final long serialVersionUID = -6541180061782004705L;
// ----------------- Server端相关配置
/**
* Ticket有效期 (单位: 秒)
*/
public long ticketTimeout = 60 * 5;
/**
* 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
*/
public String allowUrl = "*";
/**
* 是否打开单点注销功能
*/
public Boolean isSlo = true;
/**
* 是否打开模式三(此值为 true 时将使用 http 请求校验ticket值、单点注销、获取userinfo
*/
public Boolean isHttp = false;
/**
* 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验)
*/
public String secretkey;
// ----------------- Client端相关配置
/**
* 配置 Server 端单点登录授权地址
*/
public String authUrl;
/**
* 是否打开单点注销功能
*/
// public Boolean isSlo = true; // 同上
/**
* 是否打开模式三(此值为 true 时将使用 http 请求校验ticket值、单点注销、获取userinfo
*/
// public Boolean isHttp = false; // 同上
/**
* 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验)
*/
// public String secretkey; // 同上
/**
* 配置 Server 端的 ticket 校验地址
*/
public String checkTicketUrl;
/**
* 配置 Server 端查询 userinfo 地址
*/
public String userinfoUrl;
/**
* 配置 Server 端单点注销地址
*/
public String sloUrl;
/**
* 配置当前 Client 端的单点注销回调URL (为空时自动获取)
*/
public String ssoLogoutCall;
/**
* @return Ticket有效期 (单位: 秒)
*/
public long getTicketTimeout() {
return ticketTimeout;
}
/**
* @param ticketTimeout Ticket有效期 (单位: 秒)
* @return 对象自身
*/
public SaSsoConfig setTicketTimeout(long ticketTimeout) {
this.ticketTimeout = ticketTimeout;
return this;
}
/**
* @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
*/
public String getAllowUrl() {
return allowUrl;
}
/**
* @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
* @return 对象自身
*/
public SaSsoConfig setAllowUrl(String allowUrl) {
this.allowUrl = allowUrl;
return this;
}
/**
* @return 是否打开单点注销功能
*/
public Boolean getIsSlo() {
return isSlo;
}
/**
* @param isSlo 是否打开单点注销功能
* @return 对象自身
*/
public SaSsoConfig setIsSlo(Boolean isSlo) {
this.isSlo = isSlo;
return this;
}
/**
* @return isHttp 是否打开模式三(此值为 true 时将使用 http 请求校验ticket值、单点注销、获取userinfo
*/
public Boolean getIsHttp() {
return isHttp;
}
/**
* @param isHttp 是否打开模式三(此值为 true 时将使用 http 请求校验ticket值、单点注销、获取userinfo
* @return 对象自身
*/
public SaSsoConfig setIsHttp(Boolean isHttp) {
this.isHttp = isHttp;
return this;
}
/**
* @return 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验)
*/
public String getSecretkey() {
return secretkey;
}
/**
* @param secretkey 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验)
* @return 对象自身
*/
public SaSsoConfig setSecretkey(String secretkey) {
this.secretkey = secretkey;
return this;
}
/**
* @return 配置的 Server 端单点登录授权地址
*/
public String getAuthUrl() {
return authUrl;
}
/**
* @param authUrl 配置 Server 端单点登录授权地址
* @return 对象自身
*/
public SaSsoConfig setAuthUrl(String authUrl) {
this.authUrl = authUrl;
return this;
}
/**
* @return 配置的 Server 端的 ticket 校验地址
*/
public String getCheckTicketUrl() {
return checkTicketUrl;
}
/**
* @param checkTicketUrl 配置 Server 端的 ticket 校验地址
* @return 对象自身
*/
public SaSsoConfig setCheckTicketUrl(String checkTicketUrl) {
this.checkTicketUrl = checkTicketUrl;
return this;
}
/**
* @return 配置的 Server 端查询 userinfo 地址
*/
public String getUserinfoUrl() {
return userinfoUrl;
}
/**
* @param userinfoUrl 配置 Server 端查询 userinfo 地址
* @return 对象自身
*/
public SaSsoConfig setUserinfoUrl(String userinfoUrl) {
this.userinfoUrl = userinfoUrl;
return this;
}
/**
* @return 配置 Server 端单点注销地址
*/
public String getSloUrl() {
return sloUrl;
}
/**
* @param sloUrl 配置 Server 端单点注销地址
* @return 对象自身
*/
public SaSsoConfig setSloUrl(String sloUrl) {
this.sloUrl = sloUrl;
return this;
}
/**
* @return 配置当前 Client 端的单点注销回调URL (为空时自动获取)
*/
public String getSsoLogoutCall() {
return ssoLogoutCall;
}
/**
* @param ssoLogoutCall 配置当前 Client 端的单点注销回调URL (为空时自动获取)
* @return 对象自身
*/
public SaSsoConfig setSsoLogoutCall(String ssoLogoutCall) {
this.ssoLogoutCall = ssoLogoutCall;
return this;
}
@Override
public String toString() {
return "SaSsoConfig [ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", isSlo=" + isSlo
+ ", isHttp=" + isHttp + ", secretkey=" + secretkey + ", authUrl=" + authUrl + ", checkTicketUrl="
+ checkTicketUrl + ", userinfoUrl=" + userinfoUrl + ", sloUrl=" + sloUrl + ", ssoLogoutCall="
+ ssoLogoutCall + "]";
}
/**
* 以数组形式写入允许的授权回调地址
* @param url 所有集合
* @return 对象自身
*/
public SaSsoConfig setAllow(String ...url) {
this.allowUrl = SaFoxUtil.arrayJoin(url);
return this;
}
// -------------------- SaSsoHandle 所有回调函数 --------------------
/**
* SSO-Server端未登录时返回的View
*/
public Supplier<Object> notLoginView = () -> "当前会话在SSO-Server认证中心尚未登录";
/**
* SSO-Server端登录函数
*/
public BiFunction<String, String, Object> doLoginHandle = (name, pwd) -> SaResult.error();
/**
* SSO-Client端自定义校验Ticket返回值的处理逻辑 每次从认证中心获取校验Ticket的结果后调用
*/
public BiFunction<Object, String, Object> ticketResultHandle = null;
/**
* SSO-Client端发送Http请求的处理函数
*/
public Function<String, Object> sendHttp = url -> {throw new SaTokenException("请配置Http处理器");};
/**
* @param notLoginView SSO-Server端未登录时返回的View
* @return 对象自身
*/
public SaSsoConfig setNotLoginView(Supplier<Object> notLoginView) {
this.notLoginView = notLoginView;
return this;
}
/**
* @return 函数 SSO-Server端未登录时返回的View
*/
public Supplier<Object> getNotLoginView() {
return notLoginView;
}
/**
* @param doLoginHandle SSO-Server端登录函数
* @return 对象自身
*/
public SaSsoConfig setDoLoginHandle(BiFunction<String, String, Object> doLoginHandle) {
this.doLoginHandle = doLoginHandle;
return this;
}
/**
* @return 函数 SSO-Server端登录函数
*/
public BiFunction<String, String, Object> getDoLoginHandle() {
return doLoginHandle;
}
/**
* @param ticketResultHandle SSO-Client端自定义校验Ticket返回值的处理逻辑 每次从认证中心获取校验Ticket的结果后调用
* @return 对象自身
*/
public SaSsoConfig setTicketResultHandle(BiFunction<Object, String, Object> ticketResultHandle) {
this.ticketResultHandle = ticketResultHandle;
return this;
}
/**
* @return 函数 SSO-Client端自定义校验Ticket返回值的处理逻辑 每次从认证中心获取校验Ticket的结果后调用
*/
public BiFunction<Object, String, Object> getTicketResultHandle() {
return ticketResultHandle;
}
/**
* @param sendHttp SSO-Client端发送Http请求的处理函数
* @return 对象自身
*/
public SaSsoConfig setSendHttp(Function<String, Object> sendHttp) {
this.sendHttp = sendHttp;
return this;
}
/**
* @return 函数 SSO-Client端发送Http请求的处理函数
*/
public Function<String, Object> getSendHttp() {
return sendHttp;
}
}

View File

@@ -0,0 +1,86 @@
package cn.dev33.satoken.sso;
/**
* Sa-Token-SSO模块相关常量
* @author kong
*
*/
public class SaSsoConsts {
/**
* 所有API接口
* @author kong
*/
public static final class Api {
/** SSO-Server端授权地址 */
public static String ssoAuth = "/sso/auth";
/** SSO-Server端RestAPI 登录接口 */
public static String ssoDoLogin = "/sso/doLogin";
/** SSO-Server端校验ticket 获取账号id */
public static String ssoCheckTicket = "/sso/checkTicket";
/** SSO-Server端 (and Client端):单点注销地址 */
public static String ssoLogout = "/sso/logout";
/** SSO-Client端登录地址 */
public static String ssoLogin = "/sso/login";
/** SSO-Client端单点注销的回调 */
public static String ssoLogoutCall = "/sso/logoutCall";
}
/**
* 所有参数名称
* @author kong
*/
public static final class ParamName {
/** redirect参数名称 */
public static String redirect = "redirect";
/** ticket参数名称 */
public static String ticket = "ticket";
/** back参数名称 */
public static String back = "back";
/** mode参数名称 */
public static String mode = "mode";
/** loginId参数名称 */
public static String loginId = "loginId";
/** secretkey参数名称 */
public static String secretkey = "secretkey";
/** Client端单点注销时-回调URL 参数名称 */
public static String ssoLogoutCall = "ssoLogoutCall";
public static String name = "name";
public static String pwd = "pwd";
}
/** Client端单点注销回调URL的Set集合存储在Session中使用的key */
public static final String SLO_CALLBACK_SET_KEY = "SLO_CALLBACK_SET_KEY_";
/** 表示OK的返回结果 */
public static final String OK = "ok";
/** 表示自己 */
public static final String SELF = "self";
/** 表示简单模式SSO模式一 */
public static final String MODE_SIMPLE = "simple";
/** 表示ticket模式SSO模式二和模式三 */
public static final String MODE_TICKET = "ticket";
/** 表示请求没有得到任何有效处理 {msg: "not handle"} */
public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}";
}

View File

@@ -0,0 +1,367 @@
package cn.dev33.satoken.sso;
import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.sso.SaSsoConsts.Api;
import cn.dev33.satoken.sso.SaSsoConsts.ParamName;
import cn.dev33.satoken.sso.exception.SaSsoException;
import cn.dev33.satoken.sso.exception.SaSsoExceptionCode;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-Token-SSO 单点登录请求处理类封装
* @author kong
*
*/
public class SaSsoHandle {
/**
* 处理Server端所有请求
* @return 处理结果
*/
public static Object serverRequest() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaSsoConfig cfg = SaSsoManager.getConfig();
// ------------------ 路由分发 ------------------
// SSO-Server端授权地址
if(req.isPath(Api.ssoAuth)) {
return ssoAuth();
}
// SSO-Server端RestAPI 登录接口
if(req.isPath(Api.ssoDoLogin)) {
return ssoDoLogin();
}
// SSO-Server端校验ticket 获取账号id
if(req.isPath(Api.ssoCheckTicket) && cfg.getIsHttp()) {
return ssoCheckTicket();
}
// SSO-Server端单点注销 [模式一] (不带loginId参数)
if(req.isPath(Api.ssoLogout) && cfg.getIsSlo() && req.hasParam(ParamName.loginId) == false) {
return ssoServerLogoutType1();
}
// SSO-Server端单点注销 [模式三] (带loginId参数)
if(req.isPath(Api.ssoLogout) && cfg.getIsHttp() && cfg.getIsSlo() && req.hasParam(ParamName.loginId)) {
return ssoServerLogout();
}
// 默认返回
return SaSsoConsts.NOT_HANDLE;
}
/**
* SSO-Server端授权地址
* @return 处理结果
*/
public static Object ssoAuth() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
SaSsoConfig cfg = SaSsoManager.getConfig();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// ---------- 此处有两种情况分开处理:
// ---- 情况1在SSO认证中心尚未登录需要先去登录
if(stpLogic.isLogin() == false) {
return cfg.getNotLoginView().get();
}
// ---- 情况2在SSO认证中心已经登录需要重定向回 Client 端,而这又分为两种方式:
String mode = req.getParam(ParamName.mode, "");
// 方式1直接重定向回Client端 (mode=simple)
if(mode.equals(SaSsoConsts.MODE_SIMPLE)) {
String redirect = req.getParam(ParamName.redirect);
SaSsoUtil.checkRedirectUrl(redirect);
return res.redirect(redirect);
} else {
// 方式2带着ticket参数重定向回Client端 (mode=ticket)
String redirectUrl = SaSsoUtil.buildRedirectUrl(stpLogic.getLoginId(), req.getParam(ParamName.redirect));
return res.redirect(redirectUrl);
}
}
/**
* SSO-Server端RestAPI 登录接口
* @return 处理结果
*/
public static Object ssoDoLogin() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaSsoConfig cfg = SaSsoManager.getConfig();
// 处理
return cfg.getDoLoginHandle().apply(req.getParam(ParamName.name), req.getParam(ParamName.pwd));
}
/**
* SSO-Server端校验ticket 获取账号id
* @return 处理结果
*/
public static Object ssoCheckTicket() {
// 获取对象
SaRequest req = SaHolder.getRequest();
// 获取参数
String ticket = req.getParam(ParamName.ticket);
String sloCallback = req.getParam(ParamName.ssoLogoutCall);
// 校验ticket获取对应的账号id
Object loginId = SaSsoUtil.checkTicket(ticket);
// 注册此客户端的单点注销回调URL
SaSsoUtil.registerSloCallbackUrl(loginId, sloCallback);
// 返回给Client端
return loginId;
}
/**
* SSO-Server端单点注销 [模式一]
* @return 处理结果
*/
public static Object ssoServerLogoutType1() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 开始处理
stpLogic.logout();
// 返回
return ssoLogoutBack(req, res);
}
/**
* SSO-Server端单点注销 [模式三]
* @return 处理结果
*/
public static Object ssoServerLogout() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaSsoConfig cfg = SaSsoManager.getConfig();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 获取参数
String loginId = req.getParam(ParamName.loginId);
String secretkey = req.getParam(ParamName.secretkey);
// 遍历通知Client端注销会话
// step.1 校验秘钥
SaSsoUtil.checkSecretkey(secretkey);
// step.2 遍历通知Client端注销会话
SaSsoUtil.forEachSloUrl(loginId, url -> cfg.getSendHttp().apply(url));
// step.3 Server端注销
stpLogic.logout(loginId);
// 完成
return SaSsoConsts.OK;
}
/**
* 处理Client端所有请求
* @return 处理结果
*/
public static Object clientRequest() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaSsoConfig cfg = SaSsoManager.getConfig();
// ------------------ 路由分发 ------------------
// ---------- SSO-Client端登录地址
if(req.isPath(Api.ssoLogin)) {
return ssoLogin();
}
// ---------- SSO-Client端单点注销 [模式二]
if(req.isPath(Api.ssoLogout) && cfg.getIsSlo() && cfg.getIsHttp() == false) {
return ssoLogoutType2();
}
// ---------- SSO-Client端单点注销 [模式三]
if(req.isPath(Api.ssoLogout) && cfg.getIsSlo() && cfg.getIsHttp()) {
return ssoLogoutType3();
}
// ---------- SSO-Client端单点注销的回调 [模式三]
if(req.isPath(Api.ssoLogoutCall) && cfg.getIsSlo() && cfg.getIsHttp()) {
return ssoLogoutCall();
}
// 默认返回
return SaSsoConsts.NOT_HANDLE;
}
/**
* SSO-Client端登录地址
* @return 处理结果
*/
public static Object ssoLogin() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
SaSsoConfig cfg = SaSsoManager.getConfig();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 获取参数
String back = req.getParam(ParamName.back, "/");
String ticket = req.getParam(ParamName.ticket);
// 如果当前Client端已经登录则无需访问SSO认证中心可以直接返回
if(stpLogic.isLogin()) {
return res.redirect(back);
}
/*
* 此时有两种情况:
* 情况1ticket无值说明此请求是Client端访问需要重定向至SSO认证中心
* 情况2ticket有值说明此请求从SSO认证中心重定向而来需要根据ticket进行登录
*/
if(ticket == null) {
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back);
return res.redirect(serverAuthUrl);
} else {
// ------- 1、校验ticket获取账号id
Object loginId = checkTicket(ticket, Api.ssoLogin);
// Be: 如果开发者自定义了处理逻辑
if(cfg.getTicketResultHandle() != null) {
return cfg.getTicketResultHandle().apply(loginId, back);
}
// ------- 2、如果loginId有值说明ticket有效进行登录并重定向至back地址
if(loginId != null ) {
stpLogic.login(loginId);
return res.redirect(back);
} else {
// 如果ticket无效:
throw new SaSsoException("无效ticket" + ticket).setCode(SaSsoExceptionCode.CODE_20004);
}
}
}
/**
* SSO-Client端单点注销 [模式二]
* @return 处理结果
*/
public static Object ssoLogoutType2() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 开始处理
stpLogic.logout();
// 返回
return ssoLogoutBack(req, res);
}
/**
* SSO-Client端单点注销 [模式三]
* @return 处理结果
*/
public static Object ssoLogoutType3() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
SaSsoConfig cfg = SaSsoManager.getConfig();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 如果未登录,则无需注销
if(stpLogic.isLogin() == false) {
return SaResult.ok();
}
// 调用SSO-Server认证中心API进行注销
String url = SaSsoUtil.buildSloUrl(stpLogic.getLoginId());
String body = String.valueOf(cfg.getSendHttp().apply(url));
if(SaSsoConsts.OK.equals(body) == false) {
return SaResult.error("单点注销失败");
}
// 返回
return ssoLogoutBack(req, res);
}
/**
* SSO-Client端单点注销的回调 [模式三]
* @return 处理结果
*/
public static Object ssoLogoutCall() {
// 获取对象
SaRequest req = SaHolder.getRequest();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 获取参数
String loginId = req.getParam(ParamName.loginId);
String secretkey = req.getParam(ParamName.secretkey);
SaSsoUtil.checkSecretkey(secretkey);
stpLogic.logout(loginId);
return SaSsoConsts.OK;
}
/**
* 封装:单点注销成功后返回结果
* @param req SaRequest对象
* @param res SaResponse对象
* @return 返回结果
*/
public static Object ssoLogoutBack(SaRequest req, SaResponse res) {
/*
* 三种情况:
* 1. 有back参数值为SELF -> 回退一级并刷新
* 2. 有back参数值为url -> 跳转到此url地址
* 3. 无back参数 -> 返回json数据
*/
String back = req.getParam(ParamName.back);
if(SaFoxUtil.isNotEmpty(back)) {
if(back.equals(SaSsoConsts.SELF)) {
return "<script>if(document.referrer != location.href){ location.replace(document.referrer || '/'); }</script>";
}
return res.redirect(back);
} else {
return SaResult.ok("单点注销成功");
}
}
/**
* 封装校验ticket取出loginId
* @param ticket ticket码
* @param currUri 当前路由的uri用于计算单点注销回调地址
* @return loginId
*/
public static Object checkTicket(String ticket, String currUri) {
SaSsoConfig cfg = SaSsoManager.getConfig();
// --------- 两种模式
if(cfg.getIsHttp()) {
// 模式三使用http请求校验ticket
String ssoLogoutCall = null;
if(cfg.getIsSlo()) {
ssoLogoutCall = SaHolder.getRequest().getUrl().replace(currUri, Api.ssoLogoutCall);
}
String checkUrl = SaSsoUtil.buildCheckTicketUrl(ticket, ssoLogoutCall);
Object body = cfg.getSendHttp().apply(checkUrl);
return (SaFoxUtil.isEmpty(body) ? null : body);
} else {
// 模式二直连Redis校验ticket
return SaSsoUtil.checkTicket(ticket);
}
}
}

View File

@@ -0,0 +1,31 @@
package cn.dev33.satoken.sso;
import cn.dev33.satoken.config.SaSsoConfig;
/**
* Sa-Token-SSO 模块 总控类
*
* @author kong
*
*/
public class SaSsoManager {
/**
* Sso 配置 Bean
*/
private static SaSsoConfig config;
public static SaSsoConfig getConfig() {
if (config == null) {
synchronized (SaSsoManager.class) {
if (config == null) {
setConfig(new SaSsoConfig());
}
}
}
return config;
}
public static void setConfig(SaSsoConfig config) {
SaSsoManager.config = config;
}
}

View File

@@ -0,0 +1,431 @@
package cn.dev33.satoken.sso;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.sso.SaSsoConsts.ParamName;
import cn.dev33.satoken.sso.exception.SaSsoException;
import cn.dev33.satoken.sso.exception.SaSsoExceptionCode;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Sa-Token-SSO 单点登录模块
* @author kong
*
*/
public class SaSsoTemplate {
/**
* 单点登录模块使用的 StpLogic 对象
*/
public StpLogic stpLogic;
public SaSsoTemplate(StpLogic stpLogic) {
this.stpLogic = stpLogic;
}
// ---------------------- Ticket 操作 ----------------------
/**
* 根据 账号id 创建一个 Ticket码
* @param loginId 账号id
* @return Ticket码
*/
public String createTicket(Object loginId) {
// 创建 Ticket
String ticket = randomTicket(loginId);
// 保存 Ticket
saveTicket(ticket, loginId);
saveTicketIndex(ticket, loginId);
// 返回 Ticket
return ticket;
}
/**
* 保存 Ticket
* @param ticket ticket码
* @param loginId 账号id
*/
public void saveTicket(String ticket, Object loginId) {
long ticketTimeout = SaSsoManager.getConfig().getTicketTimeout();
SaManager.getSaTokenDao().set(splicingTicketSaveKey(ticket), String.valueOf(loginId), ticketTimeout);
}
/**
* 保存 Ticket 索引
* @param ticket ticket码
* @param loginId 账号id
*/
public void saveTicketIndex(String ticket, Object loginId) {
long ticketTimeout = SaSsoManager.getConfig().getTicketTimeout();
SaManager.getSaTokenDao().set(splicingTicketIndexKey(loginId), String.valueOf(ticket), ticketTimeout);
}
/**
* 删除 Ticket
* @param ticket Ticket码
*/
public void deleteTicket(String ticket) {
if(ticket == null) {
return;
}
SaManager.getSaTokenDao().delete(splicingTicketSaveKey(ticket));
}
/**
* 删除 Ticket索引
* @param loginId 账号id
*/
public void deleteTicketIndex(Object loginId) {
if(loginId == null) {
return;
}
SaManager.getSaTokenDao().delete(splicingTicketIndexKey(loginId));
}
/**
* 根据 Ticket码 获取账号id如果Ticket码无效则返回null
* @param ticket Ticket码
* @return 账号id
*/
public Object getLoginId(String ticket) {
if(SaFoxUtil.isEmpty(ticket)) {
return null;
}
return SaManager.getSaTokenDao().get(splicingTicketSaveKey(ticket));
}
/**
* 根据 Ticket码 获取账号id并转换为指定类型
* @param <T> 要转换的类型
* @param ticket Ticket码
* @param cs 要转换的类型
* @return 账号id
*/
public <T> T getLoginId(String ticket, Class<T> cs) {
return SaFoxUtil.getValueByType(getLoginId(ticket), cs);
}
/**
* 查询 指定账号id的 Ticket值
* @param loginId 账号id
* @return Ticket值
*/
public String getTicketValue(Object loginId) {
if(loginId == null) {
return null;
}
return SaManager.getSaTokenDao().get(splicingTicketIndexKey(loginId));
}
/**
* 校验ticket码获取账号id如果此ticket是有效的则立即删除
* @param ticket Ticket码
* @return 账号id
*/
public Object checkTicket(String ticket) {
Object loginId = getLoginId(ticket);
if(loginId != null) {
deleteTicket(ticket);
deleteTicketIndex(loginId);
}
return loginId;
}
/**
* 随机一个 Ticket码
* @param loginId 账号id
* @return Ticket码
*/
public String randomTicket(Object loginId) {
return SaFoxUtil.getRandomString(64);
}
// ---------------------- 构建URL ----------------------
/**
* 构建URLServer端 单点登录地址
* @param clientLoginUrl Client端登录地址
* @param back 回调路径
* @return [SSO-Server端-认证地址 ]
*/
public String buildServerAuthUrl(String clientLoginUrl, String back) {
// 服务端认证地址
String serverUrl = SaSsoManager.getConfig().getAuthUrl();
// 对back地址编码
back = (back == null ? "" : back);
back = SaFoxUtil.encodeUrl(back);
// 开始拼接 sso 统一认证地址形如serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com
/*
* 部分 Servlet 版本 request.getRequestURL() 返回的 url 带有 query 参数形如http://domain.com?id=1
* 如果不加判断会造成最终生成的 serverAuthUrl 带有双 back 参数 ,这个 if 判断正是为了解决此问题
*/
if(clientLoginUrl.indexOf(ParamName.back + "=" + back) == -1) {
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, ParamName.back, back);
}
String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, ParamName.redirect, clientLoginUrl);
// 返回
return serverAuthUrl;
}
/**
* 构建URLServer端向Client下放ticke的地址
* @param loginId 账号id
* @param redirect Client端提供的重定向地址
* @return see note
*/
public String buildRedirectUrl(Object loginId, String redirect) {
// 校验 重定向地址 是否合法
checkRedirectUrl(redirect);
// 删掉 旧Ticket
deleteTicket(getTicketValue(loginId));
// 创建 新Ticket
String ticket = createTicket(loginId);
// 构建 授权重定向地址 Server端 根据此地址向 Client端 下放Ticket
return SaFoxUtil.joinParam(encodeBackParam(redirect), ParamName.ticket, ticket);
}
/**
* 校验重定向url合法性
* @param url 下放ticket的url地址
*/
public void checkRedirectUrl(String url) {
// 1、是否是一个有效的url
if(SaFoxUtil.isUrl(url) == false) {
throw new SaSsoException("无效redirect" + url).setCode(SaSsoExceptionCode.CODE_20001);
}
// 2、截取掉?后面的部分
int qIndex = url.indexOf("?");
if(qIndex != -1) {
url = url.substring(0, qIndex);
}
// 3、是否在[允许地址列表]之中
List<String> authUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(","));
if(SaStrategy.me.hasElement.apply(authUrlList, url) == false) {
throw new SaSsoException("非法redirect" + url).setCode(SaSsoExceptionCode.CODE_20002);
}
// 校验通过 √
return;
}
/**
* 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
* @return see note
*/
public String getAllowUrl() {
// 默认从配置文件中返回
return SaSsoManager.getConfig().getAllowUrl();
}
/**
* 对url中的back参数进行URL编码, 解决超链接重定向后参数丢失的bug
* @param url url
* @return 编码过后的url
*/
public String encodeBackParam(String url) {
// 获取back参数所在位置
int index = url.indexOf("?" + ParamName.back + "=");
if(index == -1) {
index = url.indexOf("&" + ParamName.back + "=");
if(index == -1) {
return url;
}
}
// 开始编码
int length = ParamName.back.length() + 2;
String back = url.substring(index + length);
back = SaFoxUtil.encodeUrl(back);
// 放回url中
url = url.substring(0, index + length) + back;
return url;
}
/**
* 构建URLServer端 账号资料查询地址
* @param loginId 账号id
* @return Server端 账号资料查询地址
*/
public String buildUserinfoUrl(Object loginId) {
// 拼接
String userinfoUrl = SaSsoManager.getConfig().getUserinfoUrl();
userinfoUrl = SaFoxUtil.joinParam(userinfoUrl, ParamName.loginId, loginId);
userinfoUrl = SaFoxUtil.joinParam(userinfoUrl, ParamName.secretkey, SaSsoManager.getConfig().getSecretkey());
// 返回
return userinfoUrl;
}
// ------------------- SSO 模式三相关 -------------------
/**
* 校验secretkey秘钥是否有效
* @param secretkey 秘钥
*/
public void checkSecretkey(String secretkey) {
if(secretkey == null || secretkey.isEmpty() || secretkey.equals(SaSsoManager.getConfig().getSecretkey()) == false) {
throw new SaSsoException("无效秘钥:" + secretkey).setCode(SaSsoExceptionCode.CODE_20003);
}
}
/**
* 构建URL校验ticket的URL
* <p> 在模式三下Client端拿到Ticket后根据此地址向Server端发送请求获取账号id
* @param ticket ticket码
* @param ssoLogoutCallUrl 单点注销时的回调URL
* @return 构建完毕的URL
*/
public String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
// 裸地址
String url = SaSsoManager.getConfig().getCheckTicketUrl();
// 拼接ticket参数
url = SaFoxUtil.joinParam(url, ParamName.ticket, ticket);
// 拼接单点注销时的回调URL
if(ssoLogoutCallUrl != null) {
url = SaFoxUtil.joinParam(url, ParamName.ssoLogoutCall, ssoLogoutCallUrl);
}
// 返回
return url;
}
/**
* 为指定账号id注册单点注销回调URL
* @param loginId 账号id
* @param sloCallbackUrl 单点注销时的回调URL
*/
public void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) {
if(loginId == null || sloCallbackUrl == null || sloCallbackUrl.isEmpty()) {
return;
}
SaSession session = stpLogic.getSessionByLoginId(loginId);
Set<String> urlSet = session.get(SaSsoConsts.SLO_CALLBACK_SET_KEY, ()-> new HashSet<String>());
urlSet.add(sloCallbackUrl);
session.set(SaSsoConsts.SLO_CALLBACK_SET_KEY, urlSet);
}
/**
* 循环调用Client端单点注销回调
* @param loginId 账号id
* @param fun 调用方法
*/
public void forEachSloUrl(Object loginId, CallSloUrlFunction fun) {
SaSession session = stpLogic.getSessionByLoginId(loginId, false);
if(session == null) {
return;
}
String secretkey = SaSsoManager.getConfig().getSecretkey();
Set<String> urlSet = session.get(SaSsoConsts.SLO_CALLBACK_SET_KEY, () -> new HashSet<String>());
for (String url : urlSet) {
// 拼接login参数、秘钥参数
url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId);
url = SaFoxUtil.joinParam(url, ParamName.secretkey, secretkey);
// 调用
fun.run(url);
}
}
/**
* 构建URL单点注销URL
* @param loginId 要注销的账号id
* @return 单点注销URL
*/
public String buildSloUrl(Object loginId) {
SaSsoConfig ssoConfig = SaSsoManager.getConfig();
String url = ssoConfig.getSloUrl();
url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId);
url = SaFoxUtil.joinParam(url, ParamName.secretkey, ssoConfig.getSecretkey());
return url;
}
/**
* 指定账号单点注销
* @param secretkey 校验秘钥
* @param loginId 指定账号
* @param fun 调用方法
*/
public void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) {
// step.1 校验秘钥
checkSecretkey(secretkey);
// step.2 遍历通知Client端注销会话
forEachSloUrl(loginId, fun);
// step.3 Server端注销
// StpUtil.logoutByLoginId(loginId);
stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId));
}
/**
* 获取:账号资料
* @param loginId 账号id
* @return 账号资料
*/
public Object getUserinfo(Object loginId) {
String url = buildUserinfoUrl(loginId);
return SaSsoManager.getConfig().getSendHttp().apply(url);
}
// ------------------- 返回相应key -------------------
/**
* 拼接keyTicket 查 账号Id
* @param ticket ticket值
* @return key
*/
public String splicingTicketSaveKey(String ticket) {
return SaManager.getConfig().getTokenName() + ":ticket:" + ticket;
}
/**
* 拼接key账号Id 反查 Ticket
* @param id 账号id
* @return key
*/
public String splicingTicketIndexKey(Object id) {
return SaManager.getConfig().getTokenName() + ":id-ticket:" + id;
}
/**
* 单点注销回调函数
* @author kong
*/
@FunctionalInterface
public static interface CallSloUrlFunction{
/**
* 调用function
* @param url 注销回调URL
*/
public void run(String url);
}
}

View File

@@ -0,0 +1,189 @@
package cn.dev33.satoken.sso;
import cn.dev33.satoken.sso.SaSsoTemplate.CallSloUrlFunction;
import cn.dev33.satoken.stp.StpUtil;
/**
* Sa-Token-SSO 单点登录模块 工具类
* @author kong
*
*/
public class SaSsoUtil {
/**
* 底层 SaSsoTemplate 对象
*/
public static SaSsoTemplate saSsoTemplate = new SaSsoTemplate(StpUtil.stpLogic);
// ---------------------- Ticket 操作 ----------------------
/**
* 根据 账号id 创建一个 Ticket码
* @param loginId 账号id
* @return Ticket码
*/
public static String createTicket(Object loginId) {
return saSsoTemplate.createTicket(loginId);
}
/**
* 删除 Ticket
* @param ticket Ticket码
*/
public static void deleteTicket(String ticket) {
saSsoTemplate.deleteTicket(ticket);
}
/**
* 删除 Ticket索引
* @param loginId 账号id
*/
public static void deleteTicketIndex(Object loginId) {
saSsoTemplate.deleteTicketIndex(loginId);
}
/**
* 根据 Ticket码 获取账号id如果Ticket码无效则返回null
* @param ticket Ticket码
* @return 账号id
*/
public static Object getLoginId(String ticket) {
return saSsoTemplate.getLoginId(ticket);
}
/**
* 根据 Ticket码 获取账号id并转换为指定类型
* @param <T> 要转换的类型
* @param ticket Ticket码
* @param cs 要转换的类型
* @return 账号id
*/
public static <T> T getLoginId(String ticket, Class<T> cs) {
return saSsoTemplate.getLoginId(ticket, cs);
}
/**
* 校验ticket码获取账号id如果此ticket是有效的则立即删除
* @param ticket Ticket码
* @return 账号id
*/
public static Object checkTicket(String ticket) {
return saSsoTemplate.checkTicket(ticket);
}
// ---------------------- 构建URL ----------------------
/**
* 构建URLServer端 单点登录地址
* @param clientLoginUrl Client端登录地址
* @param back 回调路径
* @return [SSO-Server端-认证地址 ]
*/
public static String buildServerAuthUrl(String clientLoginUrl, String back) {
return saSsoTemplate.buildServerAuthUrl(clientLoginUrl, back);
}
/**
* 构建URLServer端向Client下放ticke的地址
* @param loginId 账号id
* @param redirect Client端提供的重定向地址
* @return see note
*/
public static String buildRedirectUrl(Object loginId, String redirect) {
return saSsoTemplate.buildRedirectUrl(loginId, redirect);
}
/**
* 校验重定向url合法性
* @param url 下放ticket的url地址
*/
public static void checkRedirectUrl(String url) {
saSsoTemplate.checkRedirectUrl(url);
}
/**
* 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
* @return see note
*/
public static String getAllowUrl() {
return saSsoTemplate.getAllowUrl();
}
/**
* 构建URLServer端 账号资料查询地址
* @param loginId 账号id
* @return Server端 账号资料查询地址
*/
public static String buildUserinfoUrl(Object loginId) {
return saSsoTemplate.buildUserinfoUrl(loginId);
}
// ------------------- SSO 模式三 -------------------
/**
* 校验secretkey秘钥是否有效
* @param secretkey 秘钥
*/
public static void checkSecretkey(String secretkey) {
saSsoTemplate.checkSecretkey(secretkey);
}
/**
* 构建URL校验ticket的URL
* @param ticket ticket码
* @param ssoLogoutCallUrl 单点注销时的回调URL
* @return 构建完毕的URL
*/
public static String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
return saSsoTemplate.buildCheckTicketUrl(ticket, ssoLogoutCallUrl);
}
/**
* 为指定账号id注册单点注销回调URL
* @param loginId 账号id
* @param sloCallbackUrl 单点注销时的回调URL
*/
public static void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) {
saSsoTemplate.registerSloCallbackUrl(loginId, sloCallbackUrl);
}
/**
* 循环调用Client端单点注销回调
* @param loginId 账号id
* @param fun 调用方法
*/
public static void forEachSloUrl(Object loginId, CallSloUrlFunction fun) {
saSsoTemplate.forEachSloUrl(loginId, fun);
}
/**
* 构建URL单点注销URL
* @param loginId 要注销的账号id
* @return 单点注销URL
*/
public static String buildSloUrl(Object loginId) {
return saSsoTemplate.buildSloUrl(loginId);
}
/**
* 指定账号单点注销
* @param secretkey 校验秘钥
* @param loginId 指定账号
* @param fun 调用方法
*/
public static void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) {
saSsoTemplate.singleLogout(secretkey, loginId, fun);
}
/**
* 获取:账号资料
* @param loginId 账号id
* @return 账号资料
*/
public static Object getUserinfo(Object loginId) {
return saSsoTemplate.getUserinfo(loginId);
}
}

View File

@@ -0,0 +1,56 @@
package cn.dev33.satoken.sso.exception;
import cn.dev33.satoken.exception.SaTokenException;
/**
* 一个异常:代表 SSO 认证流程错误
*
* @author kong
*/
public class SaSsoException extends SaTokenException {
/**
* 序列化版本号
*/
private static final long serialVersionUID = 6806129545290130114L;
/**
* 一个异常:代表 SSO 认证流程错误
* @param message 异常描述
*/
public SaSsoException(String message) {
super(message);
}
/**
* 一个异常:代表 SSO 认证流程错误
* @param code 异常细分状态码
* @param message 异常描述
*/
public SaSsoException(int code, String message) {
super(code, message);
}
/**
* 写入异常细分状态码
* @param code 异常细分状态码
* @return 对象自身
*/
public SaSsoException setCode(int code) {
super.setCode(code);
return this;
}
/**
* 如果flag==true则抛出message异常
* @param flag 标记
* @param message 异常信息
*/
public static void throwBy(boolean flag, String message) {
if(flag) {
throw new SaSsoException(message);
}
}
}

View File

@@ -0,0 +1,25 @@
package cn.dev33.satoken.sso.exception;
/**
* 定义所有 SSO 异常细分状态码
*
* @author kong
* @date: 2022-4-25
*/
public class SaSsoExceptionCode {
/** redirect 重定向 url 是一个无效地址 */
public static final int CODE_20001 = 20001;
/** redirect 重定向 url 不在 allowUrl 允许的范围内 */
public static final int CODE_20002 = 20002;
/** 接口调用方提供的 secretkey 秘钥无效 */
public static final int CODE_20003 = 20003;
/** 提供的 ticket 是无效的 */
public static final int CODE_20004 = 20004;
}