简化单点登录集成步骤

This commit is contained in:
click33
2021-07-08 01:24:42 +08:00
parent 82f7d7f78c
commit 922e746eb1
33 changed files with 790 additions and 1136 deletions

View File

@@ -1,6 +1,12 @@
package cn.dev33.satoken.config;
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
@@ -39,6 +45,10 @@ public class SaSsoConfig {
*/
public String sloUrl;
/**
* SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取)
*/
public String ssoLogoutCall;
/**
@@ -98,9 +108,11 @@ public class SaSsoConfig {
/**
* @param authUrl SSO-Server端 单点登录地址
* @return 对象自身
*/
public void setAuthUrl(String authUrl) {
public SaSsoConfig setAuthUrl(String authUrl) {
this.authUrl = authUrl;
return this;
}
/**
@@ -113,8 +125,9 @@ public class SaSsoConfig {
/**
* @param checkTicketUrl SSO-Server端Ticket校验地址
*/
public void setCheckTicketUrl(String checkTicketUrl) {
public SaSsoConfig setCheckTicketUrl(String checkTicketUrl) {
this.checkTicketUrl = checkTicketUrl;
return this;
}
/**
@@ -126,15 +139,34 @@ public class SaSsoConfig {
/**
* @param sloUrl SSO-Server端单点注销地址
* @return 对象自身
*/
public void setSloUrl(String sloUrl) {
public SaSsoConfig setSloUrl(String sloUrl) {
this.sloUrl = sloUrl;
return this;
}
/**
* @return SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取)
*/
public String getSsoLogoutCall() {
return ssoLogoutCall;
}
/**
* @param ssoLogoutCall SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取)
* @return 对象自身
*/
public SaSsoConfig setSsoLogoutCall(String ssoLogoutCall) {
this.ssoLogoutCall = ssoLogoutCall;
return this;
}
@Override
public String toString() {
return "SaSsoConfig [ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", secretkey=" + secretkey
+ ", authUrl=" + authUrl + ", checkTicketUrl=" + checkTicketUrl + ", sloUrl=" + sloUrl + "]";
+ ", authUrl=" + authUrl + ", checkTicketUrl=" + checkTicketUrl + ", sloUrl=" + sloUrl
+ ", ssoLogoutCall=" + ssoLogoutCall + ", isHttp=" + isHttp + ", isSlo=" + isSlo + "]";
}
@@ -147,5 +179,115 @@ public class SaSsoConfig {
this.allowUrl = SaFoxUtil.arrayJoin(url);
return this;
}
// -------------------- SaSsoHandle 相关配置 --------------------
/**
* 是否使用http请求校验ticket值
*/
public Boolean isHttp = false;
/**
* 是否打开单点注销
*/
public Boolean isSlo = false;
/**
* @return isHttp 是否使用http请求校验ticket值
*/
public Boolean getIsHttp() {
return isHttp;
}
/**
* @param isHttp 是否使用http请求校验ticket值
* @return 对象自身
*/
public SaSsoConfig setIsHttp(Boolean isHttp) {
this.isHttp = isHttp;
return this;
}
/**
* @return 是否打开单点注销
*/
public Boolean getIsSlo() {
return isSlo;
}
/**
* @param isSlo 是否打开单点注销
* @return 对象自身
*/
public SaSsoConfig setIsSlo(Boolean isSlo) {
this.isSlo = isSlo;
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无效时返回的View
*/
public Function<String, Object> ticketInvalidView = (ticket) -> {
// 此处向客户端提示ticket无效即可不要重定向到SSO认证中心否则容易引起无限重定向
return "ticket无效: " + ticket;
};
/**
* 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;
}
/**
* @param doLoginHandle SSO-Server端登录函数
* @return 对象自身
*/
public SaSsoConfig setDoLoginHandle(BiFunction<String, String, Object> doLoginHandle) {
this.doLoginHandle = doLoginHandle;
return this;
}
/**
* @param ticketInvalidView SSO-Client端Ticket无效时返回的View
* @return 对象自身
*/
public SaSsoConfig setTicketInvalidView(Function<String, Object> ticketInvalidView) {
this.ticketInvalidView = ticketInvalidView;
return this;
}
/**
* @param sendHttp SSO-Client端发送Http请求的处理函数
* @return 对象自身
*/
public SaSsoConfig setSendHttp(Function<String, Object> sendHttp) {
this.sendHttp = sendHttp;
return this;
}
}

View File

@@ -1,5 +1,7 @@
package cn.dev33.satoken.context.model;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Request 包装类
* @author kong
@@ -20,6 +22,20 @@ public interface SaRequest {
*/
public String getParameter(String name);
/**
* 在 [请求体] 里获取一个值,值为空时返回默认值
* @param name 键
* @param defaultValue 值为空时的默认值
* @return 值
*/
public default String getParameter(String name, String defaultValue) {
String value = getParameter(name);
if(SaFoxUtil.isEmpty(value)) {
return defaultValue;
}
return value;
}
/**
* 在 [请求头] 里获取一个值
* @param name 键
@@ -52,4 +68,12 @@ public interface SaRequest {
*/
public String getMethod();
/**
* 此请求是否为Ajax请求
* @return see note
*/
public default boolean isAjax() {
return getHeader("X-Requested-With") != null;
}
}

View File

@@ -46,4 +46,11 @@ public interface SaResponse {
return this.setHeader("Server", value);
}
/**
* 重定向
* @param url 重定向地址
* @return 任意值
*/
public Object redirect(String url);
}

View File

@@ -7,25 +7,65 @@ package cn.dev33.satoken.sso;
*/
public class SaSsoConsts {
/** redirect参数名称 */
public static final String REDIRECT_NAME = "redirect";
/**
* 所有API接口
* @author kong
*/
public static final class Api {
/** SSO-Server端授权地址 */
public static String ssoAuth = "/ssoAuth";
/** SSO-Server端RestAPI 登录接口 */
public static String ssoDoLogin = "/ssoDoLogin";
/** SSO-Server端校验ticket 获取账号id */
public static String ssoCheckTicket = "/ssoCheckTicket";
/** SSO-Server端 (and Client端):单点注销 */
public static String ssoLogout = "/ssoLogout";
/** SSO-Client端登录地址 */
public static String ssoLogin = "/ssoLogin";
/** SSO-Client端单点注销的回调 */
public static String ssoLogoutCall = "/ssoLogoutCall";
}
/** ticket参数名称 */
public static final String TICKET_NAME = "ticket";
/**
* 所有参数名称
* @author kong
*/
public static final class ParamName {
/** back参数名称 */
public static final String BACK_NAME = "back";
/** redirect参数名称 */
public static String redirect = "redirect";
/** ticket参数名称 */
public static String ticket = "ticket";
/** loginId参数名称 */
public static final String LOGIN_ID_NAME = "loginId";
/** back参数名称 */
public static String back = "back";
/** secretkey参数名称 */
public static final String SECRETKEY = "secretkey";
/** Client端单点注销时-回调URL 参数名称 */
public static final String SLO_CALLBACK_NAME = "sloCallback";
/** loginId参数名称 */
public static String loginId = "loginId";
/** secretkey参数名称 */
public static String secretkey = "secretkey";
/** Client端单点注销时-回调URL 参数名称 */
public static String ssoLogoutCall = "ssoLogoutCall";
}
/** 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 NOT_HANDLE = "not handle";
}

View File

@@ -0,0 +1,188 @@
package cn.dev33.satoken.sso;
import cn.dev33.satoken.SaManager;
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.router.SaRouter;
import cn.dev33.satoken.sso.SaSsoConsts.Api;
import cn.dev33.satoken.sso.SaSsoConsts.ParamName;
import cn.dev33.satoken.stp.StpUtil;
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();
SaResponse res = SaHolder.getResponse();
SaSsoConfig sso = SaManager.getConfig().getSso();
// ---------- SSO-Server端单点登录授权地址
if(match(Api.ssoAuth)) {
// ---------- 此处两种情况分开处理:
// 情况1在SSO认证中心尚未登录则先去登登录
if(StpUtil.isLogin() == false) {
return sso.notLoginView.get();
}
// 情况2在SSO认证中心已经登录开始构建授权重定向地址下放ticket
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), req.getParameter(ParamName.redirect));
return res.redirect(redirectUrl);
}
// ---------- SSO-Server端RestAPI 登录接口
if(match(Api.ssoDoLogin)) {
return sso.doLoginHandle.apply(req.getParameter("name"), req.getParameter("pwd"));
}
// ---------- SSO-Server端校验ticket 获取账号id
if(match(Api.ssoCheckTicket) && sso.isHttp) {
String ticket = req.getParameter(ParamName.ticket);
String sloCallback = req.getParameter(ParamName.ssoLogoutCall);
// 校验ticket获取对应的账号id
Object loginId = SaSsoUtil.checkTicket(ticket);
// 注册此客户端的单点注销回调URL
SaSsoUtil.registerSloCallbackUrl(loginId, sloCallback);
// 返回给Client端
return loginId;
}
// ---------- SSO-Server端单点注销
if(match(Api.ssoLogout) && sso.isSlo) {
String loginId = req.getParameter(ParamName.loginId);
String secretkey = req.getParameter(ParamName.secretkey);
// 遍历通知Client端注销会话
SaSsoUtil.singleLogout(secretkey, loginId, url -> sso.sendHttp.apply(url));
// 完成
return SaSsoConsts.OK;
}
// 默认返回
return SaSsoConsts.NOT_HANDLE;
}
/**
* 处理Client端请求
* @return 处理结果
*/
public static Object clientRequest() {
// 获取变量
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
SaSsoConfig sso = SaManager.getConfig().getSso();
// ---------- SSO-Client端登录地址
if(match(Api.ssoLogin)) {
String back = req.getParameter(ParamName.back, "/");
String ticket = req.getParameter(ParamName.ticket);
// 如果当前Client端已经登录则无需访问SSO认证中心可以直接返回
if(StpUtil.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 = null;
if(sso.isHttp) {
// 方式1使用http请求校验ticket
String ssoLogoutCall = null;
if(sso.isSlo) {
ssoLogoutCall = SaHolder.getRequest().getUrl().replace(Api.ssoLogin, Api.ssoLogoutCall);
}
String checkUrl = SaSsoUtil.buildCheckTicketUrl(ticket, ssoLogoutCall);
Object body = sso.sendHttp.apply(checkUrl);
loginId = (SaFoxUtil.isEmpty(body) ? null : body);
} else {
// 方式2直连Redis校验ticket
loginId = SaSsoUtil.checkTicket(ticket);
}
// ------- 2、如果loginId有值说明ticket有效进行登录并重定向至back地址
if(loginId != null ) {
StpUtil.login(loginId);
return res.redirect(back);
} else {
// 如果ticket无效:
return sso.ticketInvalidView.apply(ticket);
}
}
}
// ---------- SSO-Client端单点注销 [模式二]
if(match(Api.ssoLogout) && sso.isSlo && sso.isHttp == false) {
StpUtil.logout();
if(req.getParameter(ParamName.back) == null) {
return SaResult.ok("单点注销成功");
} else {
return res.redirect(req.getParameter(ParamName.back, "/"));
}
}
// ---------- SSO-Client端单点注销 [模式三]
if(match(Api.ssoLogout) && sso.isSlo && sso.isHttp) {
// 如果未登录,则无需注销
if(StpUtil.isLogin() == false) {
return SaResult.ok();
}
// 调用SSO-Server认证中心API
String url = SaSsoUtil.buildSloUrl(StpUtil.getLoginId());
String body = String.valueOf(sso.sendHttp.apply(url));
if(SaSsoConsts.OK.equals(body)) {
if(req.getParameter(ParamName.back) == null) {
return SaResult.ok("单点注销成功");
} else {
return res.redirect(req.getParameter(ParamName.back, "/"));
}
}
return SaResult.error("单点注销失败");
}
// ---------- SSO-Client端单点注销的回调 [模式三]
if(match(Api.ssoLogoutCall) && sso.isSlo && sso.isHttp) {
String loginId = req.getParameter(ParamName.loginId);
String secretkey = req.getParameter(ParamName.secretkey);
SaSsoUtil.checkSecretkey(secretkey);
StpUtil.logoutByTokenValue(StpUtil.getTokenValueByLoginId(loginId));
return SaSsoConsts.OK;
}
// 默认返回
return SaSsoConsts.NOT_HANDLE;
}
/**
* 路由匹配算法
* @param pattern 路由表达式
* @return 是否可以匹配
*/
static boolean match(String pattern) {
return SaRouter.isMatchCurrURI(pattern);
}
}

View File

@@ -8,6 +8,7 @@ import java.util.Set;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.sso.SaSsoConsts.ParamName;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
@@ -69,7 +70,7 @@ public interface SaSsoInterface {
// 构建 授权重定向地址
redirect = encodeBackParam(redirect);
String redirectUrl = SaFoxUtil.joinParam(redirect, SaSsoConsts.TICKET_NAME, ticket);
String redirectUrl = SaFoxUtil.joinParam(redirect, ParamName.ticket, ticket);
return redirectUrl;
}
@@ -152,8 +153,8 @@ public interface SaSsoInterface {
back = SaFoxUtil.encodeUrl(back);
// 拼接最终地址格式示例serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, SaSsoConsts.BACK_NAME, back);
String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, SaSsoConsts.REDIRECT_NAME, clientLoginUrl);
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, ParamName.back, back);
String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, ParamName.redirect, clientLoginUrl);
// 返回
return serverAuthUrl;
@@ -167,16 +168,16 @@ public interface SaSsoInterface {
public default String encodeBackParam(String url) {
// 获取back参数所在位置
int index = url.indexOf("?" + SaSsoConsts.BACK_NAME + "=");
int index = url.indexOf("?" + ParamName.back + "=");
if(index == -1) {
index = url.indexOf("&" + SaSsoConsts.BACK_NAME + "=");
index = url.indexOf("&" + ParamName.back + "=");
if(index == -1) {
return url;
}
}
// 开始编码
int length = SaSsoConsts.BACK_NAME.length() + 2;
int length = ParamName.back.length() + 2;
String back = url.substring(index + length);
back = SaFoxUtil.encodeUrl(back);
@@ -210,16 +211,16 @@ public interface SaSsoInterface {
/**
* 构建URL校验ticket的URL
* @param ticket ticket码
* @param sloCallbackUrl 单点注销时的回调URL
* @param ssoLogoutCallUrl 单点注销时的回调URL
* @return 构建完毕的URL
*/
public default String buildCheckTicketUrl(String ticket, String sloCallbackUrl) {
public default String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
String url = SaManager.getConfig().getSso().getCheckTicketUrl();
// 拼接ticket参数
url = SaFoxUtil.joinParam(url, SaSsoConsts.TICKET_NAME, ticket);
// 拼接ticket参数
url = SaFoxUtil.joinParam(url, ParamName.ticket, ticket);
// 拼接单点注销时的回调URL
if(sloCallbackUrl != null) {
url = SaFoxUtil.joinParam(url, SaSsoConsts.SLO_CALLBACK_NAME, sloCallbackUrl);
if(ssoLogoutCallUrl != null) {
url = SaFoxUtil.joinParam(url, ParamName.ssoLogoutCall, ssoLogoutCallUrl);
}
// 返回
return url;
@@ -251,8 +252,8 @@ public interface SaSsoInterface {
for (String url : urlSet) {
// 拼接login参数、秘钥参数
url = SaFoxUtil.joinParam(url, SaSsoConsts.LOGIN_ID_NAME, loginId);
url = SaFoxUtil.joinParam(url, SaSsoConsts.SECRETKEY, secretkey);
url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId);
url = SaFoxUtil.joinParam(url, ParamName.secretkey, secretkey);
// 调用
fun.run(url);
}
@@ -266,8 +267,8 @@ public interface SaSsoInterface {
public default String buildSloUrl(Object loginId) {
SaSsoConfig ssoConfig = SaManager.getConfig().getSso();
String url = ssoConfig.getSloUrl();
url = SaFoxUtil.joinParam(url, SaSsoConsts.LOGIN_ID_NAME, loginId);
url = SaFoxUtil.joinParam(url, SaSsoConsts.SECRETKEY, ssoConfig.getSecretkey());
url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId);
url = SaFoxUtil.joinParam(url, ParamName.secretkey, ssoConfig.getSecretkey());
return url;
}

View File

@@ -102,11 +102,11 @@ public class SaSsoUtil {
/**
* 构建URL校验ticket的URL
* @param ticket ticket码
* @param sloCallbackUrl 单点注销时的回调URL (如果不需要单点注销功能此值可以填null)
* @param ssoLogoutCallUrl 单点注销时的回调URL
* @return 构建完毕的URL
*/
public static String buildCheckTicketUrl(String ticket, String sloCallbackUrl) {
return saSsoInterface.buildCheckTicketUrl(ticket, sloCallbackUrl);
public static String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
return saSsoInterface.buildCheckTicketUrl(ticket, ssoLogoutCallUrl);
}
/**
@@ -145,5 +145,5 @@ public class SaSsoUtil {
public static void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) {
saSsoInterface.singleLogout(secretkey, loginId, fun);
}
}

View File

@@ -54,11 +54,11 @@ public class SaFoxUtil {
}
/**
* 指定字符串是否为null或者空字符串
* @param str 指定字符串
* 指定元素是否为null或者空字符串
* @param str 指定元素
* @return 是否为null或者空字符串
*/
public static boolean isEmpty(String str) {
public static boolean isEmpty(Object str) {
return str == null || "".equals(str);
}

View File

@@ -0,0 +1,96 @@
package cn.dev33.satoken.util;
import java.io.Serializable;
/**
* 对Ajax请求返回Json格式数据的简易封装
* @author kong
*
*/
public class SaResult implements Serializable{
private static final long serialVersionUID = 1L;
public static final int CODE_SUCCESS = 200;
public static final int CODE_ERROR = 500;
/**
* 状态码
*/
public int code;
/**
* 描述信息
*/
public String msg;
/**
* 携带对象
*/
public Object data;
/**
* 给code赋值连缀风格
*/
public SaResult setCode(int code) {
this.code = code;
return this;
}
/**
* 给msg赋值连缀风格
*/
public SaResult setMsg(String msg) {
this.msg = msg;
return this;
}
/**
* 给data赋值连缀风格
*/
public SaResult setData(Object data) {
this.data = data;
return this;
}
// ============================ 构建 ==================================
public SaResult(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 构建成功
public static SaResult ok() {
return new SaResult(CODE_SUCCESS, "ok", null);
}
public static SaResult ok(String msg) {
return new SaResult(CODE_SUCCESS, msg, null);
}
public static SaResult data(Object data) {
return new SaResult(CODE_SUCCESS, "ok", data);
}
// 构建失败
public static SaResult error() {
return new SaResult(CODE_ERROR, "error", null);
}
public static SaResult error(String msg) {
return new SaResult(CODE_ERROR, msg, null);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "{"
+ "\"code\": " + this.code
+ ", \"msg\": \"" + this.msg + "\""
+ ", \"data\": \"" + this.data + "\""
+ "}";
}
}