From d2e00b341d4e5596428f1d8b8fc416af20e457e2 Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Tue, 29 Jun 2021 23:32:35 +0800 Subject: [PATCH] =?UTF-8?q?SSO=20=E4=B8=89=E7=A7=8D=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/dev33/satoken/config/SaSsoConfig.java | 63 ++++- .../cn/dev33/satoken/sso/SaSsoConsts.java | 12 + .../cn/dev33/satoken/sso/SaSsoInterface.java | 148 ++++++++++- .../java/cn/dev33/satoken/sso/SaSsoUtil.java | 81 +++++- .../java/cn/dev33/satoken/util/SaFoxUtil.java | 15 ++ .../com/pj/test/StressTestController.java | 5 - .../main/java/com/pj/sso/GlobalException.java | 39 --- .../.gitignore | 0 sa-token-demo/sa-token-demo-sso1/pom.xml | 40 +++ .../main/java/com/pj/SaSsoApplication.java | 16 ++ .../main/java/com/pj/sso/SsoController.java} | 4 +- .../src/main/java/com/pj/util/AjaxJson.java | 0 .../src/main/resources/application.yml | 9 + .../.gitignore | 0 .../pom.xml | 4 +- .../java/com/pj/SaSsoClientApplication.java | 5 - .../java/com/pj/sso/SsoClientController.java | 26 +- .../src/main/java/com/pj/util/AjaxJson.java | 0 .../src/main/resources/application.yml | 6 +- .../sa-token-demo-sso2-server/.gitignore | 12 + .../pom.xml | 2 +- .../java/com/pj/SaSsoServerApplication.java | 5 - .../main/java/com/pj/sso/GlobalException.java | 21 ++ .../java/com/pj/sso/SsoServerController.java | 49 ++++ .../src/main/java/com/pj/util/AjaxJson.java | 162 ++++++++++++ .../src/main/resources/application.yml | 4 +- .../resources/static/sa-res/jquery.min.js | 0 .../resources/static/sa-res/layer/layer.js | 0 .../static/sa-res/layer/mobile/layer.js | 0 .../static/sa-res/layer/mobile/need/layer.css | 0 .../sa-res/layer/theme/default/icon-ext.png | Bin .../sa-res/layer/theme/default/icon.png | Bin .../sa-res/layer/theme/default/layer.css | 0 .../sa-res/layer/theme/default/loading-0.gif | Bin .../sa-res/layer/theme/default/loading-1.gif | Bin .../sa-res/layer/theme/default/loading-2.gif | Bin .../main/resources/static/sa-res/login.css | 0 .../src/main/resources/static/sa-res/login.js | 0 .../main/resources/templates/sa-login.html | 0 .../sa-token-demo-sso3-client/.gitignore | 12 + .../sa-token-demo-sso3-client/pom.xml | 61 +++++ .../java/com/pj/SaSsoClientApplication.java | 14 + .../java/com/pj/sso/SsoClientController.java | 71 +++++ .../com/pj/sso/SsoClientLogoutController.java | 50 ++++ .../src/main/java/com/pj/util/AjaxJson.java | 162 ++++++++++++ .../src/main/resources/application.yml | 51 ++++ .../sa-token-demo-sso3-server/.gitignore | 12 + .../sa-token-demo-sso3-server/pom.xml | 66 +++++ .../java/com/pj/SaSsoServerApplication.java | 14 + .../main/java/com/pj/sso/GlobalException.java | 21 ++ .../java/com/pj/sso/SsoServerController.java | 24 +- .../com/pj/sso/SsoServerLogoutController.java | 28 ++ .../src/main/java/com/pj/util/AjaxJson.java | 162 ++++++++++++ .../src/main/resources/application.yml | 44 ++++ .../resources/static/sa-res/jquery.min.js | 2 + .../resources/static/sa-res/layer/layer.js | 2 + .../static/sa-res/layer/mobile/layer.js | 2 + .../static/sa-res/layer/mobile/need/layer.css | 1 + .../sa-res/layer/theme/default/icon-ext.png | Bin 0 -> 5911 bytes .../sa-res/layer/theme/default/icon.png | Bin 0 -> 11493 bytes .../sa-res/layer/theme/default/layer.css | 1 + .../sa-res/layer/theme/default/loading-0.gif | Bin 0 -> 5793 bytes .../sa-res/layer/theme/default/loading-1.gif | Bin 0 -> 701 bytes .../sa-res/layer/theme/default/loading-2.gif | Bin 0 -> 1787 bytes .../main/resources/static/sa-res/login.css | 59 +++++ .../src/main/resources/static/sa-res/login.js | 65 +++++ .../main/resources/templates/sa-login.html | 45 ++++ sa-token-doc/doc/_sidebar.md | 7 +- sa-token-doc/doc/index.html | 7 +- sa-token-doc/doc/sso/readme.md | 29 ++- sa-token-doc/doc/sso/sso-type1.md | 114 +++----- sa-token-doc/doc/sso/sso-type2.md | 56 ++-- sa-token-doc/doc/sso/sso-type3.md | 244 +++++++++++++++++- 73 files changed, 1908 insertions(+), 246 deletions(-) delete mode 100644 sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalException.java rename sa-token-demo/{sa-token-demo-sso-client => sa-token-demo-sso1}/.gitignore (100%) create mode 100644 sa-token-demo/sa-token-demo-sso1/pom.xml create mode 100644 sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java rename sa-token-demo/{sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java => sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java} (94%) rename sa-token-demo/{sa-token-demo-sso-client => sa-token-demo-sso1}/src/main/java/com/pj/util/AjaxJson.java (100%) create mode 100644 sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-client}/.gitignore (100%) rename sa-token-demo/{sa-token-demo-sso-client => sa-token-demo-sso2-client}/pom.xml (96%) rename sa-token-demo/{sa-token-demo-sso-client => sa-token-demo-sso2-client}/src/main/java/com/pj/SaSsoClientApplication.java (85%) rename sa-token-demo/{sa-token-demo-sso-client => sa-token-demo-sso2-client}/src/main/java/com/pj/sso/SsoClientController.java (73%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-client}/src/main/java/com/pj/util/AjaxJson.java (100%) rename sa-token-demo/{sa-token-demo-sso-client => sa-token-demo-sso2-client}/src/main/resources/application.yml (85%) create mode 100644 sa-token-demo/sa-token-demo-sso2-server/.gitignore rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/pom.xml (97%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/java/com/pj/SaSsoServerApplication.java (85%) create mode 100644 sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/GlobalException.java create mode 100644 sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java create mode 100644 sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/util/AjaxJson.java rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/application.yml (88%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/jquery.min.js (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/layer.js (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/mobile/layer.js (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/mobile/need/layer.css (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/theme/default/icon.png (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/theme/default/layer.css (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/login.css (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/static/sa-res/login.js (100%) rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso2-server}/src/main/resources/templates/sa-login.html (100%) create mode 100644 sa-token-demo/sa-token-demo-sso3-client/.gitignore create mode 100644 sa-token-demo/sa-token-demo-sso3-client/pom.xml create mode 100644 sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java create mode 100644 sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java create mode 100644 sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientLogoutController.java create mode 100644 sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/util/AjaxJson.java create mode 100644 sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml create mode 100644 sa-token-demo/sa-token-demo-sso3-server/.gitignore create mode 100644 sa-token-demo/sa-token-demo-sso3-server/pom.xml create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/GlobalException.java rename sa-token-demo/{sa-token-demo-sso-server => sa-token-demo-sso3-server}/src/main/java/com/pj/sso/SsoServerController.java (72%) create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerLogoutController.java create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/util/AjaxJson.java create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/layer.js create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/layer.js create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon.png create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/layer.css create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.css create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.js create mode 100644 sa-token-demo/sa-token-demo-sso3-server/src/main/resources/templates/sa-login.html diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java index 219d0c4d..c6992683 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java @@ -25,23 +25,21 @@ public class SaSsoConfig { public String secretkey; /** - * SSO-Server端授权地址 + * SSO-Server端 单点登录地址 */ - public String serverUrl; + public String authUrl; /** - * @return SSO-Server端授权地址 + * SSO-Server端 Ticket校验地址 [模式三专用配置] */ - public String getServerUrl() { - return serverUrl; - } + public String checkTicketUrl; /** - * @param serverUrl SSO-Server端授权地址 + * SSO-Server端 单点注销地址 [模式三专用配置] */ - public void setServerUrl(String serverUrl) { - this.serverUrl = serverUrl; - } + public String sloUrl; + + /** * @return Ticket有效期 (单位: 秒) @@ -91,13 +89,54 @@ public class SaSsoConfig { return this; } + /** + * @return SSO-Server端 单点登录地址 + */ + public String getAuthUrl() { + return authUrl; + } + + /** + * @param authUrl SSO-Server端 单点登录地址 + */ + public void setAuthUrl(String authUrl) { + this.authUrl = authUrl; + } + + /** + * @return SSO-Server端Ticket校验地址 + */ + public String getCheckTicketUrl() { + return checkTicketUrl; + } + + /** + * @param checkTicketUrl SSO-Server端Ticket校验地址 + */ + public void setCheckTicketUrl(String checkTicketUrl) { + this.checkTicketUrl = checkTicketUrl; + } + + /** + * @return SSO-Server端单点注销地址 + */ + public String getSloUrl() { + return sloUrl; + } + + /** + * @param sloUrl SSO-Server端单点注销地址 + */ + public void setSloUrl(String sloUrl) { + this.sloUrl = sloUrl; + } + @Override public String toString() { return "SaSsoConfig [ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", secretkey=" + secretkey - + ", serverUrl=" + serverUrl + "]"; + + ", authUrl=" + authUrl + ", checkTicketUrl=" + checkTicketUrl + ", sloUrl=" + sloUrl + "]"; } - /** * 以数组形式写入允许的授权回调地址 diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java index aeac0479..53225407 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java @@ -15,5 +15,17 @@ public class SaSsoConsts { /** back参数名称 */ public static final String BACK_NAME = "back"; + + /** loginId参数名称 */ + public static final String LOGIN_ID_NAME = "loginId"; + + /** secretkey参数名称 */ + public static final String SECRETKEY = "secretkey"; + + /** Client端单点注销时-回调URL 参数名称 */ + public static final String SLO_CALLBACK_NAME = "sloCallback"; + + /** Client端单点注销回调URL的Set集合,存储在Session中使用的key */ + public static final String SLO_CALLBACK_SET_KEY = "SLO_CALLBACK_SET_KEY_"; } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoInterface.java index f83cb192..bd15f250 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoInterface.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoInterface.java @@ -1,10 +1,14 @@ 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.exception.SaTokenException; +import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; /** @@ -41,14 +45,14 @@ public interface SaSsoInterface { } /** - * 根据 账号id & 重定向地址,构建[SSO-Client端-重定向地址] + * 构建URL:Server端向Client下放ticke的地址 * @param loginId 账号id - * @param redirect 重定向地址 - * @return [SSO-Client端-重定向地址] + * @param redirect Client端提供的重定向地址 + * @return see note */ public default String buildRedirectUrl(Object loginId, String redirect) { - // 校验授权地址 - checkAuthUrl(redirect); + // 校验重定向地址 + checkRedirectUrl(redirect); // 删掉旧ticket String oldTicket = SaManager.getSaTokenDao().get(splicingKeyIdToTicket(loginId)); @@ -61,7 +65,7 @@ public interface SaSsoInterface { // 构建 授权重定向地址 redirect = encodeBackParam(redirect); - String redirectUrl = SaFoxUtil.joinParam(redirect, SaSsoConsts.TICKET_NAME + "=" + ticket); + String redirectUrl = SaFoxUtil.joinParam(redirect, SaSsoConsts.TICKET_NAME, ticket); return redirectUrl; } @@ -87,10 +91,23 @@ public interface SaSsoInterface { } /** - * 校验url合法性 - * @param url 地址 + * 校验ticket码,获取账号id,如果ticket可以有效,则立刻删除 + * @param ticket Ticket码 + * @return 账号id */ - public default void checkAuthUrl(String url) { + public default Object checkTicket(String ticket) { + Object loginId = getLoginId(ticket); + if(loginId != null) { + deleteTicket(ticket); + } + return loginId; + } + + /** + * 校验重定向url合法性 + * @param url 下放ticket的url地址 + */ + public default void checkRedirectUrl(String url) { // 1、是否是一个有效的url if(SaFoxUtil.isUrl(url) == false) { @@ -122,15 +139,15 @@ public interface SaSsoInterface { */ public default String buildServerAuthUrl(String clientLoginUrl, String back) { // 服务端认证地址 - String serverUrl = SaManager.getConfig().getSso().getServerUrl(); + String serverUrl = SaManager.getConfig().getSso().getAuthUrl(); // 对back地址编码 back = (back == null ? "" : back); 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); + // 拼接最终地址,格式示例: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); // 返回 return serverAuthUrl; @@ -171,6 +188,101 @@ public interface SaSsoInterface { return SaFoxUtil.getRandomString(64); } + + // ------------------- SSO 模式三 ------------------- + + /** + * 校验secretkey秘钥是否有效 + * @param secretkey 秘钥 + */ + public default void checkSecretkey(String secretkey) { + if(secretkey == null || secretkey.isEmpty() || secretkey.equals(SaManager.getConfig().getSso().getSecretkey()) == false) { + throw new SaTokenException("无效秘钥:" + secretkey); + } + } + + /** + * 构建URL:校验ticket的URL + * @param ticket ticket码 + * @param sloCallbackUrl 单点注销时的回调URL + * @return 构建完毕的URL + */ + public default String buildCheckTicketUrl(String ticket, String sloCallbackUrl) { + String url = SaManager.getConfig().getSso().getCheckTicketUrl(); + // 拼接ticket参数 + url = SaFoxUtil.joinParam(url, SaSsoConsts.TICKET_NAME, ticket); + // 拼接单点注销时的回调URL + if(sloCallbackUrl != null) { + url = SaFoxUtil.joinParam(url, SaSsoConsts.SLO_CALLBACK_NAME, sloCallbackUrl); + } + // 返回 + return url; + } + + /** + * 为指定账号id注册单点注销回调URL + * @param loginId 账号id + * @param sloCallbackUrl 单点注销时的回调URL + */ + public default void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) { + if(loginId == null || sloCallbackUrl == null || sloCallbackUrl.isEmpty()) { + return; + } + Set urlSet = StpUtil.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY, ()-> new HashSet()); + urlSet.add(sloCallbackUrl); + StpUtil.getSessionByLoginId(loginId).set(SaSsoConsts.SLO_CALLBACK_SET_KEY, urlSet); + } + + /** + * 循环调用Client端单点注销回调 + * @param loginId 账号id + * @param fun 调用方法 + */ + public default void forEachSloUrl(Object loginId, CallSloUrlFunction fun) { + String secretkey = SaManager.getConfig().getSso().getSecretkey(); + Set urlSet = StpUtil.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY, + () -> new HashSet()); + + for (String url : urlSet) { + // 拼接:login参数、秘钥参数 + url = SaFoxUtil.joinParam(url, SaSsoConsts.LOGIN_ID_NAME, loginId); + url = SaFoxUtil.joinParam(url, SaSsoConsts.SECRETKEY, secretkey); + // 调用 + fun.run(url); + } + } + + /** + * 构建URL:单点注销URL + * @param loginId 要注销的账号id + * @return 单点注销URL + */ + 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()); + return url; + } + + /** + * 指定账号单点注销 + * @param secretkey 校验秘钥 + * @param loginId 指定账号 + * @param fun 调用方法 + */ + public default void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) { + // step.1 校验秘钥 + checkSecretkey(secretkey); + + // step.2 遍历通知Client端注销会话 + forEachSloUrl(loginId, fun); + + // step.3 Server端注销 + StpUtil.logoutByLoginId(loginId); + } + + // ------------------- 返回相应key ------------------- @@ -193,4 +305,14 @@ public interface SaSsoInterface { } + @FunctionalInterface + static interface CallSloUrlFunction{ + /** + * 调用function + * @param url 注销回调URL + */ + public void run(String url); + } + + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java index 26172a9c..e8ccac00 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java @@ -1,5 +1,7 @@ package cn.dev33.satoken.sso; +import cn.dev33.satoken.sso.SaSsoInterface.CallSloUrlFunction; + /** * Sa-Token-SSO 单点登录工具类 * @author kong @@ -30,10 +32,10 @@ public class SaSsoUtil { } /** - * 根据 账号id & 重定向地址,构建[SSO-Client端-重定向地址] + * 构建URL:Server端向Client下放ticke的地址 * @param loginId 账号id - * @param redirect 重定向地址 - * @return [SSO-Client端-重定向地址] + * @param redirect Client端提供的重定向地址 + * @return see note */ public static String buildRedirectUrl(Object loginId, String redirect) { return saSsoInterface.buildRedirectUrl(loginId, redirect); @@ -56,13 +58,22 @@ public class SaSsoUtil { public static T getLoginId(String ticket, Class cs) { return saSsoInterface.getLoginId(ticket, cs); } + + /** + * 校验ticket码,获取账号id,如果ticket可以有效,则立刻删除 + * @param ticket Ticket码 + * @return 账号id + */ + public static Object checkTicket(String ticket) { + return saSsoInterface.checkTicket(ticket); + } /** - * 校验url合法性 - * @param url 地址 + * 校验重定向url合法性 + * @param url 下放ticket的url地址 */ public static void checkAuthUrl(String url) { - saSsoInterface.checkAuthUrl(url); + saSsoInterface.checkRedirectUrl(url); } /** @@ -74,5 +85,63 @@ public class SaSsoUtil { public static String buildServerAuthUrl(String clientLoginUrl, String back) { return saSsoInterface.buildServerAuthUrl(clientLoginUrl, back); } + + + // ------------------- SSO 模式三 ------------------- + + /** + * 校验secretkey秘钥是否有效 + * @param secretkey 秘钥 + */ + public static void checkSecretkey(String secretkey) { + saSsoInterface.checkSecretkey(secretkey); + } + + /** + * 构建URL:校验ticket的URL + * @param ticket ticket码 + * @param sloCallbackUrl 单点注销时的回调URL (如果不需要单点注销功能,此值可以填null) + * @return 构建完毕的URL + */ + public static String buildCheckTicketUrl(String ticket, String sloCallbackUrl) { + return saSsoInterface.buildCheckTicketUrl(ticket, sloCallbackUrl); + } + + /** + * 为指定账号id注册单点注销回调URL + * @param loginId 账号id + * @param sloCallbackUrl 单点注销时的回调URL + */ + public static void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) { + saSsoInterface.registerSloCallbackUrl(loginId, sloCallbackUrl); + } + + /** + * 循环调用Client端单点注销回调 + * @param loginId 账号id + * @param fun 调用方法 + */ + public static void forEachSloUrl(Object loginId, CallSloUrlFunction fun) { + saSsoInterface.forEachSloUrl(loginId, fun); + } + + /** + * 构建URL:单点注销URL + * @param loginId 要注销的账号id + * @return 单点注销URL + */ + public static String buildSloUrl(Object loginId) { + return saSsoInterface.buildSloUrl(loginId); + } + + /** + * 指定账号单点注销 + * @param secretkey 校验秘钥 + * @param loginId 指定账号 + * @param fun 调用方法 + */ + public static void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) { + saSsoInterface.singleLogout(secretkey, loginId, fun); + } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java index ccd03e7d..94dc03c7 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java @@ -226,6 +226,21 @@ public class SaFoxUtil { // 正常情况下, 代码不可能执行到此 return url; } + + /** + * 在url上拼接上kv参数并返回 + * @param url url + * @param key 参数名称 + * @param value 参数值 + * @return 拼接后的url字符串 + */ + public static String joinParam(String url, String key, Object value) { + // 如果参数为空, 直接返回 + if(isEmpty(url) || isEmpty(key) || isEmpty(String.valueOf(value))) { + return url; + } + return joinParam(url, key + "=" + value); + } /** * 将数组的所有元素使用逗号拼接在一起 diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java index dcef7384..5c0b3ce1 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java @@ -20,7 +20,6 @@ import cn.dev33.satoken.stp.StpUtil; @RequestMapping("/s-test/") public class StressTestController { - // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") @@ -59,8 +58,4 @@ public class StressTestController { return AjaxJson.getSuccess(); } - - - - } diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalException.java b/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalException.java deleted file mode 100644 index 52ab707a..00000000 --- a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalException.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.pj.sso; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; - -import com.pj.util.AjaxJson; - -/** - * 全局异常处理 - */ -@ControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin") -public class GlobalException { - - // 全局异常拦截(拦截项目中的所有异常) - @ResponseBody - @ExceptionHandler - public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) - throws Exception { - - // 打印堆栈,以供调试 - System.out.println("全局异常---------------"); - e.printStackTrace(); - - // 不同异常返回不同状态码 - AjaxJson aj = AjaxJson.getError(e.getMessage()); - - // 返回给前端 - return aj; - - // 输出到客户端 -// response.setContentType("application/json; charset=utf-8"); // http说明,我要返回JSON对象 -// response.getWriter().print(new ObjectMapper().writeValueAsString(aj)); - } - -} diff --git a/sa-token-demo/sa-token-demo-sso-client/.gitignore b/sa-token-demo/sa-token-demo-sso1/.gitignore similarity index 100% rename from sa-token-demo/sa-token-demo-sso-client/.gitignore rename to sa-token-demo/sa-token-demo-sso1/.gitignore diff --git a/sa-token-demo/sa-token-demo-sso1/pom.xml b/sa-token-demo/sa-token-demo-sso1/pom.xml new file mode 100644 index 00000000..a50e7d8f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso1/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso1 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + + 1.20.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token-version} + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java new file mode 100644 index 00000000..c80e20a4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java @@ -0,0 +1,16 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +@SpringBootApplication +public class SaSsoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSsoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java similarity index 94% rename from sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java rename to sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java index a723c7d6..d5e59d57 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java +++ b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java @@ -1,4 +1,4 @@ -package com.pj.test; +package com.pj.sso; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -14,7 +14,7 @@ import cn.dev33.satoken.stp.StpUtil; */ @RestController @RequestMapping("/sso/") -public class SSOController { +public class SsoController { // 测试:进行登录 @RequestMapping("doLogin") diff --git a/sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/util/AjaxJson.java similarity index 100% rename from sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/util/AjaxJson.java rename to sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/util/AjaxJson.java diff --git a/sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml new file mode 100644 index 00000000..95704ba0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml @@ -0,0 +1,9 @@ +# 端口 +server: + port: 8081 + +spring: + sa-token: + # 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie + cookie-domain: stp.com + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso-server/.gitignore b/sa-token-demo/sa-token-demo-sso2-client/.gitignore similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/.gitignore rename to sa-token-demo/sa-token-demo-sso2-client/.gitignore diff --git a/sa-token-demo/sa-token-demo-sso-client/pom.xml b/sa-token-demo/sa-token-demo-sso2-client/pom.xml similarity index 96% rename from sa-token-demo/sa-token-demo-sso-client/pom.xml rename to sa-token-demo/sa-token-demo-sso2-client/pom.xml index bf44231e..89dc1230 100644 --- a/sa-token-demo/sa-token-demo-sso-client/pom.xml +++ b/sa-token-demo/sa-token-demo-sso2-client/pom.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 cn.dev33 - sa-token-demo-sso-client + sa-token-demo-sso2-client 0.0.1-SNAPSHOT @@ -53,7 +53,7 @@ sa-token-alone-redis 1.20.0 - + diff --git a/sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/SaSsoClientApplication.java b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/SaSsoClientApplication.java similarity index 85% rename from sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/SaSsoClientApplication.java rename to sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/SaSsoClientApplication.java index 744ebcf9..cbafaa0e 100644 --- a/sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/SaSsoClientApplication.java +++ b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/SaSsoClientApplication.java @@ -3,11 +3,6 @@ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -/** - * Sa-Token整合SpringBoot 示例 - * @author kong - * - */ @SpringBootApplication public class SaSsoClientApplication { diff --git a/sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java similarity index 73% rename from sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/sso/SsoClientController.java rename to sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java index ca5823bb..63a887c0 100644 --- a/sa-token-demo/sa-token-demo-sso-client/src/main/java/com/pj/sso/SsoClientController.java +++ b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java @@ -20,38 +20,40 @@ public class SsoClientController { public String index() { String str = "

Sa-Token SSO-Client 应用端

" + "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录

"; + "

登录

"; return str; } // SSO-Client端:登录地址 @RequestMapping("ssoLogin") - public Object login(String back, String ticket) { + public Object ssoLogin(String back, String ticket) { // 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回 if(StpUtil.isLogin()) { return new ModelAndView("redirect:" + back); } /* * 接下来两种情况: - * ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 * ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心 + * ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 */ - if(ticket != null) { - Object loginId = SaSsoUtil.getLoginId(ticket); + if(ticket == null) { + String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back); + return new ModelAndView("redirect:" + serverAuthUrl); + } else { + Object loginId = checkTicket(ticket); if(loginId != null ) { - // 如果ticket是有效的 (可以获取到值),需要就此登录 且清除此ticket + // loginId有值,说明ticket有效 StpUtil.login(loginId); - SaSsoUtil.deleteTicket(ticket); - // 最后重定向回back地址 return new ModelAndView("redirect:" + back); } // 此处向客户端提示ticket无效即可,不要重定向到SSO认证中心,否则容易引起无限重定向 return "ticket无效: " + ticket; } - - // 重定向至 SSO-Server端 认证地址 - String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back); - return new ModelAndView("redirect:" + serverAuthUrl); } + // SSO-Client端:校验ticket,获取账号id + private Object checkTicket(String ticket) { + return SaSsoUtil.checkTicket(ticket); + } + } diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/util/AjaxJson.java similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/util/AjaxJson.java rename to sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/util/AjaxJson.java diff --git a/sa-token-demo/sa-token-demo-sso-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso2-client/src/main/resources/application.yml similarity index 85% rename from sa-token-demo/sa-token-demo-sso-client/src/main/resources/application.yml rename to sa-token-demo/sa-token-demo-sso2-client/src/main/resources/application.yml index cd2b1e80..3273d6cd 100644 --- a/sa-token-demo/sa-token-demo-sso-client/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-sso2-client/src/main/resources/application.yml @@ -13,10 +13,8 @@ spring: token-style: uuid # SSO-相关配置 sso: - # SSO-Server端授权地址 - server-url: http://sa-sso-server.com:9000/ssoAuth - # 接口调用秘钥(模式三才会用到此参数) - # secret-key: + # SSO-Server端 单点登录地址 + auth-url: http://sa-sso-server.com:9000/ssoAuth # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) alone-redis: diff --git a/sa-token-demo/sa-token-demo-sso2-server/.gitignore b/sa-token-demo/sa-token-demo-sso2-server/.gitignore new file mode 100644 index 00000000..99a6e767 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso2-server/.gitignore @@ -0,0 +1,12 @@ +target/ + +node_modules/ +bin/ +.settings/ +unpackage/ +.classpath +.project + +.idea/ + +.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso-server/pom.xml b/sa-token-demo/sa-token-demo-sso2-server/pom.xml similarity index 97% rename from sa-token-demo/sa-token-demo-sso-server/pom.xml rename to sa-token-demo/sa-token-demo-sso2-server/pom.xml index 70475797..f519efb8 100644 --- a/sa-token-demo/sa-token-demo-sso-server/pom.xml +++ b/sa-token-demo/sa-token-demo-sso2-server/pom.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 cn.dev33 - sa-token-demo-sso-server + sa-token-demo-sso2-server 0.0.1-SNAPSHOT diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/SaSsoServerApplication.java similarity index 85% rename from sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java rename to sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/SaSsoServerApplication.java index e019d237..6b57b574 100644 --- a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java +++ b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/SaSsoServerApplication.java @@ -3,11 +3,6 @@ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -/** - * Sa-Token整合SpringBoot 示例 - * @author kong - * - */ @SpringBootApplication public class SaSsoServerApplication { diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/GlobalException.java b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/GlobalException.java new file mode 100644 index 00000000..1cf22b15 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/GlobalException.java @@ -0,0 +1,21 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java new file mode 100644 index 00000000..071caeb1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java @@ -0,0 +1,49 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.sso.SaSsoUtil; +import cn.dev33.satoken.stp.StpUtil; + +/** + * Sa-Token-SSO Server端 Controller + * @author kong + * + */ +@RestController +public class SsoServerController { + + // SSO-Server端:授权地址,跳转到登录页面 + @RequestMapping("ssoAuth") + public Object ssoAuth(String redirect) { + /* + * 此处两种情况分开处理: + * 1、如果在SSO认证中心尚未登录,则先去登登录 + * 2、如果在SSO认证中心尚已登录,则开始对redirect地址下放ticket引导授权 + */ + // 情况1:尚未登录 + if(StpUtil.isLogin() == false) { +// return "当前会话在SSO-Server端尚未登录,请先访问 doLogin登录 进行登录之后,刷新页面开始授权"; + return new ModelAndView("sa-login.html"); + } + // 情况2:已经登录,开始构建授权重定向地址,下放ticket + String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect); + return new ModelAndView("redirect:" + redirectUrl); + } + + // SSO-Server端:登录接口 + @RequestMapping("doLogin") + public AjaxJson doLogin(String name, String pwd) { + // 此处仅做模拟登录,真实环境应该查询数据进行登录 + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return AjaxJson.getSuccess("登录成功!"); + } + return AjaxJson.getError("登录失败!"); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 00000000..768d0578 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/application.yml similarity index 88% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/application.yml rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/application.yml index da617ca0..847d4cba 100644 --- a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/application.yml @@ -7,12 +7,10 @@ spring: sa-token: # SSO-相关配置 sso: - # Ticket有效期 (单位: 秒),默认三分钟 + # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 所有允许的授权回调地址 (此处为了方便测试配置为*,线上生产环境一定要配置为详细地地址) allow-url: http://sa-sso-client1.com:9001/ssoLogin, http://sa-sso-client2.com:9001/ssoLogin, http://sa-sso-client3.com:9001/ssoLogin - # 接口调用秘钥(模式三才会用到此参数) - # secret-key: # Redis配置 redis: diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/jquery.min.js b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/jquery.min.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/jquery.min.js rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/jquery.min.js diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/layer.js b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/layer.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/layer.js rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/layer.js diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/layer.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/layer.js diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon.png b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon.png similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon.png rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon.png diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/layer.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/layer.css diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.css b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.css rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.css diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.js diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/resources/templates/sa-login.html b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/templates/sa-login.html similarity index 100% rename from sa-token-demo/sa-token-demo-sso-server/src/main/resources/templates/sa-login.html rename to sa-token-demo/sa-token-demo-sso2-server/src/main/resources/templates/sa-login.html diff --git a/sa-token-demo/sa-token-demo-sso3-client/.gitignore b/sa-token-demo/sa-token-demo-sso3-client/.gitignore new file mode 100644 index 00000000..99a6e767 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/.gitignore @@ -0,0 +1,12 @@ +target/ + +node_modules/ +bin/ +.settings/ +unpackage/ +.classpath +.project + +.idea/ + +.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-client/pom.xml b/sa-token-demo/sa-token-demo-sso3-client/pom.xml new file mode 100644 index 00000000..5d90d620 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-client + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + + 1.20.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token-version} + + + + + cn.dev33 + sa-token-dao-redis-jackson + ${sa-token-version} + + + + + org.apache.commons + commons-pool2 + + + + + com.ejlchina + okhttps + 3.1.1 + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java new file mode 100644 index 00000000..cbafaa0e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java @@ -0,0 +1,14 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSsoClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSsoClientApplication.class, args); + System.out.println("\nSa-Token-SSO Client端启动成功"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 00000000..0dea88d4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,71 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import com.ejlchina.okhttps.OkHttps; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.SaSsoUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * Sa-Token-SSO Client端 Controller + * @author kong + */ +@RestController +public class SsoClientController { + + // SSO-Client端:首页 + @RequestMapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端

" + + "

当前会话是否登录:" + StpUtil.isLogin() + "

" + + "

登录" + + " 注销

"; + return str; + } + + // SSO-Client端:登录地址 + @RequestMapping("ssoLogin") + public Object ssoLogin(String back, String ticket) { + // 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回 + if(StpUtil.isLogin()) { + return new ModelAndView("redirect:" + back); + } + /* + * 接下来两种情况: + * ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心 + * ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 + */ + if(ticket == null) { + String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back); + return new ModelAndView("redirect:" + serverAuthUrl); + } else { + Object loginId = checkTicket(ticket); + if(loginId != null ) { + // loginId有值,说明ticket有效 + StpUtil.login(loginId); + return new ModelAndView("redirect:" + back); + } + // 此处向客户端提示ticket无效即可,不要重定向到SSO认证中心,否则容易引起无限重定向 + return "ticket无效: " + ticket; + } + } + + // SSO-Client端:校验ticket码,获取对应的账号id + private Object checkTicket(String ticket) { + // 构建单点注销的回调URL(不需要单点注销时此值可填null ) + String sloCallback = SaHolder.getRequest().getUrl().replace("/ssoLogin", "/sloCallback"); + + // 使用OkHttps请求SSO-Server端,校验ticket + String checkUrl = SaSsoUtil.buildCheckTicketUrl(ticket, sloCallback); + String loginId = OkHttps.sync(checkUrl).get().getBody().toString(); + + // 判断返回值是否为有效账号Id + return (SaFoxUtil.isEmpty(loginId) ? null : loginId); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientLogoutController.java b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientLogoutController.java new file mode 100644 index 00000000..d733cf7f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientLogoutController.java @@ -0,0 +1,50 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.ejlchina.okhttps.OkHttps; +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.sso.SaSsoUtil; +import cn.dev33.satoken.stp.StpUtil; + +/** + * Sa-Token-SSO Client端 单点注销 Controller + * @author kong + */ +@RestController +public class SsoClientLogoutController { + + // SSO-Client端:单端注销 (其它Client端会话不受影响) + @RequestMapping("logout") + public AjaxJson logout() { + StpUtil.logout(); + return AjaxJson.getSuccess(); + } + + // SSO-Client端:单点注销 (所有端一起下线) + @RequestMapping("ssoLogout") + public AjaxJson ssoLogout() { + // 如果未登录,则无需注销 + if(StpUtil.isLogin() == false) { + return AjaxJson.getSuccess(); + } + // 调用SSO-Server认证中心API + String url = SaSsoUtil.buildSloUrl(StpUtil.getLoginId()); + String res = OkHttps.sync(url).get().getBody().toString(); + if(res.equals("ok")) { + return AjaxJson.getSuccess("单点注销成功"); + } + return AjaxJson.getError("单点注销失败"); + } + + // 单点注销的回调 + @RequestMapping("sloCallback") + public String sloCallback(String loginId, String secretkey) { + SaSsoUtil.checkSecretkey(secretkey); + StpUtil.logoutByLoginId(loginId); + return "ok"; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 00000000..768d0578 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml new file mode 100644 index 00000000..adf2534a --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml @@ -0,0 +1,51 @@ +# 端口 +server: + port: 9001 + +spring: + # sa-token配置 + sa-token: + # Token名称 + token-name: satoken + # Token有效期 + timeout: 2592000 + # Token风格 + token-style: uuid + # SSO-相关配置 + sso: + # SSO-Server端 单点登录地址 + auth-url: http://sa-sso-server.com:9000/ssoAuth + # SSO-Server端 ticket校验地址 + check-ticket-url: http://sa-sso-server.com:9000/checkTicket + # SSO-Server端 单点注销地址 + slo-url: http://sa-sso-server.com:9000/ssoLogout + # 接口调用秘钥(用于SSO模式三的单点注销功能) + secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + + # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) + redis: + # Redis数据库索引 + database: 6 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/.gitignore b/sa-token-demo/sa-token-demo-sso3-server/.gitignore new file mode 100644 index 00000000..99a6e767 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/.gitignore @@ -0,0 +1,12 @@ +target/ + +node_modules/ +bin/ +.settings/ +unpackage/ +.classpath +.project + +.idea/ + +.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/pom.xml b/sa-token-demo/sa-token-demo-sso3-server/pom.xml new file mode 100644 index 00000000..84123fd5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-server + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + + 1.20.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token-version} + + + + + cn.dev33 + sa-token-dao-redis-jackson + ${sa-token-version} + + + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + com.ejlchina + okhttps + 3.1.1 + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java new file mode 100644 index 00000000..0e07f76d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java @@ -0,0 +1,14 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSsoServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSsoServerApplication.class, args); + System.out.println("\nSa-Token-SSO 认证中心启动成功"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/GlobalException.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/GlobalException.java new file mode 100644 index 00000000..036e2a56 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/GlobalException.java @@ -0,0 +1,21 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e){ + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerController.java similarity index 72% rename from sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java rename to sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerController.java index 672eea38..1a1da438 100644 --- a/sa-token-demo/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerController.java @@ -27,10 +27,6 @@ public class SsoServerController { */ // 情况1:尚未登录 if(StpUtil.isLogin() == false) { -// String msg = "当前会话在SSO-Server端尚未登录,请先访问" -// + " doLogin登录 " -// + "进行登录之后,刷新页面开始授权"; -// return msg; return new ModelAndView("sa-login.html"); } // 情况2:已经登录,开始构建授权重定向地址,下放ticket @@ -49,15 +45,17 @@ public class SsoServerController { return AjaxJson.getError("登录失败!"); } - // SSO-Server端:根据 Ticket 获取账号id - @RequestMapping("getLoginId") - public AjaxJson getLoginId(String ticket) { - Object loginId = SaSsoUtil.getLoginId(ticket); - if(loginId != null) { - SaSsoUtil.deleteTicket(ticket); - return AjaxJson.getSuccessData(loginId); - } - return AjaxJson.getError("无效ticket: " + ticket); + // SSO-Server端:校验ticket 获取账号id + @RequestMapping("checkTicket") + public Object checkTicket(String ticket, String sloCallback) { + // 校验ticket,获取对应的账号id + Object loginId = SaSsoUtil.checkTicket(ticket); + + // 注册此客户端的单点注销回调URL(不需要单点注销功能可删除此行代码) + SaSsoUtil.registerSloCallbackUrl(loginId, sloCallback); + + // 返回给Client端 + return loginId; } } diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerLogoutController.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerLogoutController.java new file mode 100644 index 00000000..4116633a --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerLogoutController.java @@ -0,0 +1,28 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.ejlchina.okhttps.OkHttps; + +import cn.dev33.satoken.sso.SaSsoUtil; + +/** + * Sa-Token-SSO Server端 单点注销 Controller + * @author kong + */ +@RestController +public class SsoServerLogoutController { + + // SSO-Server端:单点注销 + @RequestMapping("ssoLogout") + public String ssoLogout(String loginId, String secretkey) { + + // 遍历通知Client端注销会话 (为了提高响应速度这里可将sync换为async) + SaSsoUtil.singleLogout(secretkey, loginId, url -> OkHttps.sync(url).get()); + + // 完成 + return "ok"; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 00000000..768d0578 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml new file mode 100644 index 00000000..25264a48 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml @@ -0,0 +1,44 @@ +# 端口 +server: + port: 9000 + +spring: + # Sa-Token配置 + sa-token: + # SSO-相关配置 + sso: + # Ticket有效期 (单位: 秒),默认五分钟 + ticket-timeout: 300 + # 所有允许的授权回调地址 + allow-url: http://sa-sso-client1.com:9001/ssoLogin, http://sa-sso-client2.com:9001/ssoLogin, http://sa-sso-client3.com:9001/ssoLogin + # 接口调用秘钥(用于SSO模式三的单点注销功能) + secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + + # Redis配置 + redis: + # Redis数据库索引(默认为0) + database: 5 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js new file mode 100644 index 00000000..07c00cd2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 00;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
'+(f?r.title[0]:r.title)+"
":"";return r.zIndex=s,t([r.shade?'
':"",'
'+(e&&2!=r.type?"":u)+'
'+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
'+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
'+e+"
"}():"")+(r.resize?'':"")+"
"],u,i('
')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
    '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
  • '+(t[0].content||"no content")+"
  • ";i'+(t[i].content||"no content")+"";return a}()+"
",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
'+(u.length>1?'':"")+'
'+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/layer.js b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/layer.js new file mode 100644 index 00000000..f9cf6931 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/layer.js @@ -0,0 +1,2 @@ +/*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ + ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

'+(e?n.title[0]:n.title)+"

":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
'+e+"
"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

'+(n.content||"")+"

"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
':"")+'
"+l+'
'+n.content+"
"+c+"
",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbb669bb311514baa5db3a6a00b4644d0e280f1 GIT binary patch literal 5911 zcmY+I2Q(bf_s2JgAUe^aMOKL(VwGqSy<0@0i{8cRqDzD%ST(B#i!4FHDp8XlI?-*k z=$*)lUVhK-{LcTJ|C}>3XXea%^WJ^;-tXtWSbbeJ3NjWl2n0f*p{@)EcPu#VNQl8z z1kb_-ZbS$r4I>h8JSVYx1)fR0)Sn&qHr}8y{y+4^AUz zcYBDagvi~yB6shN>mfA37p#|G7`9y&Ggi_)mcoDUevwZ%`QQ+u`Spkp9gx zTYuuo_8p5IL4SGDE=2#lxUGErKvu^NZ*;4Tj}QBeHs#sycwNE47h{3wpZ|9emH((u z9sRflNhSr++WU1KOOW>%Hbg-aK-&p%Q&ht?^+2LRNG+S62f~|#IHbK7^Ddkcx)J1Q z0S7-})`HegD(zyqd3ie^Xb3L+7UdQyoXc9w+U)bw_5iL6R1v||XHI%*wrz$^Hxo(q z4GqONss`jwc1leu&Ie}C_iF{Y#ELuWnzl6x0$Yn+EWq{3{85roZ0UUaYXG0b)L=y?`*9JA#80I z3P(##E(C&bEKxAud)k68*!7p?g7>p#8~i=*Q(G^3Q}7`S4GptXIHeC{8;MWMNzpPwJM({dpXnId*kn{Y5EiD@N@df+QF z=ydO?XqznoUo&{Dudh#pk{Zx!=;*Y&!4i%`+VW%iA)5@ZRhS}sZ!`B~ge$$|!57kC z871jaeGcN{4!xWL0L6rzKKTQ{CGhEnft!6{hpBOL@H)dt#qvkFpkh)jIe7!-rRUdp>qgmJfFq zu+`PvIwEDAvWR8v{he98pdc9`A)$|^)nqNRdM+;OA7%#BqsQ#odE$E4*4F56+(4$K zsq)ctF_F`f6JI+gX1PU8^4qTgCGJRhvcGj(PEM?EXEz`bdS^_aKk8|n(uNonokkJ~ zag?3Cy}{$huW)WWtdtA*BPsuF*6i$TQs!XF8--%I1#}uhDYUHLC5;re$(42JWcdZfurd&*Jj(-wE3U z8p;?N6=YEnPf2Mh(w;fF3mu3Gk>_Afh;hsbd^z3VUpfT4cTeBcw1gC8&%6JByc1M_PomP9JdP7ad#I|Ex0?^gtOKU zS}xQ|ue9x;{3qE}?K*yG^rj{Yaj}ONmn%l7{4PRP*70t&`|8*tWxo=;xaG7+xv%q#ha*J2qI9~PFF+Y+mbgD ziF_c%s!C1d;_7;|oarfw($1iLFOrgTw4!h!ZC2}HY+qhlT7bpU=MJQQ!hAVj-Qaa4 ztn-@to@J1PBefH;Y?PA2+51Vcg88_?ZdMB3?h#8Dw#WxwQZV?AUM#rDa>_%p<#@Cr zV5@q3qN+M?E-Q5(z`GHQiIYXd@6&1Q{x96RE4Gcd^@@Dp0H{!lq1#bD?~a_Dm*Q zij@+o@!eV!xX}0P`~K7_22})mJWS+b4!ulcRWin!Wt8cVpc;Hqr*d2DTvsfl4fCH8L@O* z?nN!Gtd!cil@-W#fZt&-m@Ayz+%L8!Ypb3gd4tultdRWXkCO}`6}r;*rhLQ~`gtUh z^TTT>n8{S#Gs38Eic+i&zp&2q3=9N&QrY<`$_8z7Ucd220cZclG3DjNTmvSSmb%ZL z-Sw!=EH5u7nq6yM^W@bgu~@%V;3it{vqlSY`a^mZyC)7qXbs>g$_68iBg9c4k?3+# z|2}BBkXz}`Hr#-D&h+936cRcX2GJvg?ps5J?8M#X_*4Oty5~n?k(``8VmKU5(7cYi zbToq=exH@{G*rQ?#%-=Gmd<6mNGCI3x1CYq&OhsY{&hGNVRBb=m)-nEMa%N{7uQP~ zQ7BYzu0rm}h!H^qq>{Dt5A?Gdb0|sV*Lb%3LFyK8`1cay(mw&R0kS!v%{{AP6MePy zBdv;0=9_&t7)D1&qm^!bpA*$BPJVHnao$H}ltSB71!x2*{M8g?;F&95F1&b`Cm7%Owcs1q(qa=-&BynT$mBqLgRMzppzZQ zGpuq!MrCHzE;oR~WvpUi5Ho7&K}>wXxs#KV(!T5TKo&?M!v~$vK&S2)7Jc9~!^Vl7 zQmY`@?)!NycG6UPEOn>4O?eCu9p8-9HGN1`1B_(zKJM591)}l1I*9%D>vpSF`}YH6*luWP;=xh;*vXvvYM3cw6r2N6?VyfqweC zfh_5V4<8az<7zNVGhgm&>XoUV4XSZqd|M9NMLIh>)jO-&=6f53|B33O8Hgg**Ijh8 zW!k%vdDm7~)#K!b|0u4fq|ncV99U4Y%Xa$DhIDjrglU_ZnJMWmwegd*d7;^zi7xUq zv+sZ3pO37BAa-Wtp37Uoi89vWIY~f15M;O>L&^4Zy55&n$_rA3%NkK?~ zLzzoi1qd~pLeGvJ^V2ivO?my=3hu9(tjEVw+AqtcWk#K();BkwpRA_GT6GV_3hV}* z=%f4p8|`IfWfA}qbC&T(k%fhYR%!}#uUQ4AF@%4Dnhd=`@Bw_d##&9OY5} zR9^HdO;zWY_f6W76RDI=7RVIyX#8^5m?u|dpj78Jds8)n1 z2Yq>*5YkWp&Gx5WYfnYv3z`{DKb)3?8s*r2+LP$9A^t%)24vIF(lRIZ)dWtKT6T<{ zT0?B-6;F08jfRqyGBmCwzCV1Adygr+KrKO6I_&&(9=|dmat>q&BlyaWCKxjuL3(s_ zw10B2bFtP+rEuyR9DEYtah>aE6}~|p*&MA4GWho-ZY>8AgV4XpxxI&{_<>@z4O<~! z;;+piCu#A_;tpitt#j`JE_v7&&LVq>^sr#*uU^?>CKPT1Su>Q9`dg0>cwn_8G04XC= z&i-1sT32C@kxV;iDb-}V`QrSfx~b3-=;a=h)->roY)#Eeb72#EK)@CU-Isqkm8Tg@?m5|+yDr&~&N`L+;d>8ic!Cez8F!MA3&2Do0)UCg>? zsdO6Tl910D8zAxP*g076k+}?dkZM3wglA=Cp^-tK^1c$M)R&a-^9D(~z+3i)wCEx( zly1YX0R;|K$kQh&9_~6l!fWX1je|jKgJcBNaM?`k?Y$)AfsaqBRyQ}be;xj8V%A^3 zdY$1k09z>U^;@y<5gG~;%Dy6lV#=zvhOv&M?DRSlb$4w{O4YL163^TSdF?3{td2j`{98*`gzmLzKc1Ek8 zgM)d*Nq6}8tbr$hR2Xi0zRqwY^amgL%V6=Mv4Y+bRCkc=tLp{0nUX*w;*Ge3hFUWepyi@hQ*CCmG zKg>Lv+8YD$K%6p?gP?g|vBJJrNRv!szktd`I^-CeL3-V~KTBHnXfYY6RNsKH09;a1 z693D!;@Qc*J4AwfVpvb%?c~;v6+HK$E{EulzBQp{2pFhA>hbSyQNdWQYMh&DnmsUb z84oR4OzYy}Vq$uFF%Ruf{fJ*fHXvn~$5f}}>~lip843U~kFie3qM-H1(F7YN>%cz^ zQh&Kr7rCmq1SBE~i;7+z9|uXuwPT%!-${D1=kvKV7lTyn)F(u z|Bhvv;FEk*j?AOHuRfTQ2VGo~a!7rE8}n_kV2!A%a37DZGO4TsSMTobK3p%Y2=Bb# zT5i#BxTY5t*Rh~cH}aYMD$EF@#^U7g0Y1QH6MS1K_KnZKb>sE*b!wsrFDdOuj~GBI zF`*;njv6`GnO*U3Ibj182QgP`=_LcX;VPrG*fuULGA%^^?l!Uee&TV%PIJT0CO9%^ zcfNg1IX*$!_UG~^gQW3UK!Dd7g*i27D+QC0$Zz>7uP;$B-4s>4AJmkRnrdLe_=E+> zs{3ROx2&|ItWw0k#QKA4%YB)}ZN0CI`9zJ^kMJuy&K@4;{s)=>V=Ny%s^JSlF&DsM z-X^Jk$jiG_u|`XgNY>WVzQ~&Yfo0Xhk%7l*O zL`+veGywua{JNb>@JS`K!M|{P!`L#$wwf}F);$@pldcY+-Df*g_h2x7n&f-P;c;tG z&Nwa|9UUwd3p5>+&c(yA!)qfxRAuiM@A@=MpYGSTEd6+UQ&D-{cVi60+^m}U_! zdvLnEuPNsIh~-`zK>X@S(SuHl`&*OuBqX?Xh~P^qez;0|?RTONgf9N}hyZ$kINu40YZOS$tn2wQJX^7$k4DA;4ji%`qluAKwb<#ej4=0in_3s zRmcF_LB4M0j~{oUHIj``o>O%XEG)7!!c;c+)+R&GHms^ZTvs>N*Jl96qa`64aeGpr zBN*LJCWF01G{;y322+FzG_WL~^x6j>KjAX0HC9n~~2pkZca2HkLym^VL1 zUBc0tT_}LtJ9q9F^yp9%)wX|B7yzhcq1yJgo*E`Uk z_r{ozHjg13O8PfI*2mZPv&$$ypw!~DT&ZV~0Q{Vk9GIH_+q`qrN9NfVb97-LW?>aX z%kad+2jN&(HkIW|paoF+VW}g5!x2zABqNdeB`;PO58=aEcf_-4fy$mi%Z{RJ=K!eM zLoF?>q0UXe2C$6tsV0^-qb0^JM}TZ6s$J9TSJ-Najxu514T!?RG!kbk4>Vqt(|H)mToz#peQ#y6|Tp}<1aBrlW#nk?aP zxRaC9Zy4f*msc+bDkP*c zt&&cDoo5<=IM`F#-RzqQgC<_9Kl9Lu%*PBeZwFJExsI+T!yQ(co4 z*NNxQl&YkNJ{{IxohMt4Xj2wBt&54T| zEcW>k&M}v52(;l3DO6>670t4m?eP8DsiK?xBPK#weB$4C-5+@?#$mgfmK;1u@!!8i z4dX)J+d|(`DBko+QYSX!UOQz|4K>nQxuBui%JcO}N?pvg9U5GFDU9vE{o?;$+ApsB YZmOLxGt_1UThtH@6k?11;06>$MlhS}5=b&FE!8cRn$r(cw*CLxiM=BH4${Ax7y;K}kc|Yw?S8cxq>*aTjQ8v6{l9 zH@7H!N#68nTt6@Ke%^biXL^`i@jn0X<)XWz$A8Lq$~~VEnG#-}VqFJzNf^EZy%>C= zMyiaYN(V?`C+9Cg<@d(R?s~NOh)Eo9=rHo+pjFHxhYXrg^73Z%^+_lwD9|%9Qd3i#YxykZI|Z~vLdZp9dfJo4{E6+H zF#v8l=-CkIBL%vW9G!&UW-M+~AocB*r{|SjyFIaBPFZ1V{{8cEP2_y%-%EWo{d{Bb zIG`27vEtj&PbVCyOJ8WQQ|z3@Z2eHm9*q|AOTLhn=4vLi-pVvvwozD5%Rv^X)R&#D zHDz&f1ap3R-j!NtejVLjdeOLGqBl?Hf9~@6u{4i*wh`TChcR|sp61YuGtR~Ylmhpa z*|28&7zZ;!n`0mKzF~Q?i9k9Kc9B?vYgx?nazH;7eI3-XHR5u7=;W`I6|woD+IX zlV2>vWkhg~SJMcY_iWH^>5a36RP2nrsz~zA&Kl=t$Q{@ZEccpPZ9d=QPs=6aV!}?h zdP4%PbYGO|X7PR$GS-XnS|Wg>Ep4t*lIA(pjL>28Na-tbt_mFf1UKWA)qVgNt$vCd zclrZ*kxA09#G@w-9@uImTl7R)<$~ik|B`+CVWj+HX)_0nBf7+~I4W0BhdnZ=N{v)d zeFLrcG*<+}s8_%F!+k|iUU*?uRg9|WYg%h7&-KmC7e>aC($X*}oSJ_9V$V_nZ8)8I z3F=h;fMPB?JNxJiwKYjvTH4TS)shL=0QjFIQsPM~R<@Qu{JB?PeC!?g z`0?LRSgT!q-rM_T*z-B>jB>sV7+3cz(1$j=YhakTiS*$?5<%ntP)PFUR5FVlu!@Z8iJWo#ozHZdhwx z5MuDrOHzfP7u&K{pX2JyqsE;f%N$)R%Bs`J>U7RsD2W#$c_s#);iUI|_^yKdq>QAVh{H@LGf_q?EJd3oxYoh(YbOawAerPz0_A zMR`+*CXc^7Z}D)uaR^RmQDYbme{v5pn&G1OCe_sZl;$(fEl@YYVCt)aB~sM1H9NVv ziJl81nqhU2TsJ$|tm%Ia;^_`M>}JmV?Sgacy%GAg7kA`fWthRyL9^JfU1QeM@2*z&1n&>irCh-+N(t--^jFyZ2gW1TAo%{WL@L4?4XQW+ zS4li@%6{Q&krye&OglNvx7H)O2yapNt5nTMpQ3ZVM3vu}bmhhh;wd^bWKEt3P6WE& zRhHBimj^e0tAx?G8ab(Zm@~oGEgPGe4!=_d?r)R^`=YrWJjT~rxC=!1q9irzztAOa ziw1qdBw%1on0>{3n0^TpTShrz_4^b!iX+!?Lu@YxcHmm&r5F`hcw^8SHco=it~rhB zn38C4T;sXB+?sB(90xXe@u8mNWfeMl!K3#(zERwQ1FWSI+$2ka3id7 z?mQeBR_;P6hsoE8(z44*qe+(SdPAk~3Q>X+6?r85Z`jCxOcH+30daI z?fo?T-%uINCKCKO&2^=vK)Q95^}LW?!l$S(AyTh`TH{)SwkC&Fj=J`P?1f=&2#_|q zsp)TFPh(>;)ChBMaL}``B+wrQT{0U68z=79`LowQog5h+uDU0|KiHxFb>{n?n-}VI zG7)7q?R~io`E_|c@^I=4y6VW>&BmIga+Q9vfNvQ0&7FSA8C|wyo7RFw{V3nU`*-b~ z4?M?e2D(*Gc?H#3yF*9=u(x{YW>tQZeqnyYkk>^_>y|JEK| zcY~ZJS@)xVW*A>FbAM87LOH(mU%5OwsHra)Tn*pDX!TGywS6)P{MI~iT)oWGDoAmb*dC6oqE)-fc z9aDYYcxCQz3d=W@f#ehc=W@21NqZ|Bbjm~6Y69v;&scB2Y?xw`J$hY@Wn~c!+MF!! z&Q@!HjZ{TZ>rT7|tq$9-_gfW3MKHfsm7JUc?t^S?zr4P_=JLBEMD~l@+S-E3H1)Gx zUVVjaQR)Y-|2?xBN=X(%DH?b=_FW3jE|HlJjeVuWhM?j4VbvSNUY#-=@bnB43gp;rJ{|!m%o|YH&-~aL4;Q73l$6kY#B|#<)G}~Pvd7F3$exko zz`)B${AeE-%vyjhKuq%&5r?szhBFaLB60+#+J7P5UBK%NJ%r~_1sS$1CqRA9QSdG< z?74NywQn`X%saRM;t*UQ=6(40SRvENINIr&3(6lt4MWu&a>V8enSGL^micFX5l(Nr z1t@PxH@+diuZsQ^ZbyJtzy_}E0_BfBzW`RrA1v+6K9jR!Cr8LNQrloNK@)t zg4ffQPx!aIHOv@MyPlJy{?`ku^-CuiPyR?8^WU%IN99ukTRuV~+)-_3h{?%%oKB;a zF-YH;=i5-~EbD%T5#19)i5k2Zo)e?OP3O_)jhI|vPI>M_#8nQgjZy;`wd^fvP;KtN zj@9+miK_4N6Dp!TjiYJ{9cSx0uP|*o&gLd2SLH06`ao?qZbK5|~@(H&%pJXSB=tJ^U1}L2ZCf z^<5#@v3GPSf6~TXmomp{xK{UEbV98E9I1>IB)$|%;*pMYNr-TwTj+OU4pxZZXl}0# zDI^HLWI~S&?dT9Vn8-@?*tG7CKr{4Q)DAc`*xHF8cKUnKd3hi!`h?Ze(z38Td;mW( zI{I}gmmjdOxVY`Yr{>>5xbp1kvT40jNkg0qI3iio?I&nZVaX zhx6*#m3pKf&ILi?u88mXxuIKM9~-x3YC%+EN(+Z>26q7f=i zz8zO#o*NyM8$+2te2xFgs{LSnRSgqg&uB{#-&u2G(}5(>lfUhK$Kze2JO1khL^Jd2 zRPoYk|CBM~?+zk0SOPp_!oWC7O*X?;0)WZCpxkK@Tur6l9d^0X!r@SJP&#kkb>MgF z&Kgah>b2iu6RV)6!n<8vP5E~Pxi68&+p^Cc((=YBjvp8I`xZa*fcI;5@JAyEbqIVF zdGKk^K+E#MM!ZAzH?WD~pT^Yk^3Sl}0Jylg$i9i>qAC=arjOVASZm6kaiHAk>sqTL z^7-Lpj%-kn$ocs>7dJN)6sR!a&4aqbBGcJ$P-E^3+sg7ncjDT8OSkam&Ra7Fjys(` zMa~rtg+A-e^r^ajPRR+o@#}|Xd}S$HrvY96OyN2isH@IsI+Ssb0i1St&5>Wh{zdii zsk-Tp;y{Bt?{Zj+RB+Kbg2q~x|DQl`W$7q>Opzzzy<-#1i)$DP- z)uXXc-um}Fb}e08x1qU#8>uS%#eal=>@&-w&qCiz3qnd+WlXM7EX=Qpa9l284Z=$q zrKW&HHB;Ksii#+fmX`8|(H)(g?8C0l`1ts{UY$5#1E%zboB!z1JEY{udOB8c3Dv`! z^5uJBZtI|*xWpH7w z-KOyDbb74>0gU1tA3IQ?*I>SzrD_|Hy1l(*(g}i*AeF5Gc7{B776bXWLVu4AGCNsq z1G($SF6y4?NfwjpW+6(CW^Ya}X;E&J`9v9LWo5`4X%9t;ZeHl={$v#A*R+2MGxLKH z%4%`9W{Gl%^JQ!uW#*3AH++Z>1mDr+$=6^ochvQ>)i$_o=J08R$ct_%0yY5Z-*FT$ zBpT_OF(?O0I_w+tYtz!wN-eyLkRZTXotMdYb&QOnnd$skr@4hI@BV$onZ3MIKRdeg zlvbC~_E&t92(L^;t}x6*XmnnZ|7(IbV~DBZhsE1(Q~nR3jqcJ<4rDAZ5>i>mwjW*s zx0@P#(ygb=q^-^{YcQvwcxkRGL00ziUB@9)4)f8H#i3(HXNZW8hi{m7+OGqj$ITP_ zl(-SjD>c%E+8YY52kudyKDc~DN>AF^`J*TbEb0-V_j4To9Z8M1XP$KlGVGa`?^gG zQ$CCX^T#)ZcX!33sva+KBC}ak&I~hu?b!}jKz--4+fQHK+1Msx7ANmKGg)CYg?x|8{Y;{u53cE zLa^9&L;p|}_`Hkb*=}sImu~oLMZ7lm8o$WOzww0=JyCmP#+%)((th8)+l6$P5&m5^ z!w$^pi*rmQ`03tU74W`dQru{U1L|RNGj-0auJG^`cOdJFQO>>Pt)(iZEzY}fzpqmH zz%Y1obE-9&wt%0uUDpG&^O`4Llvd_<8@lf{IrkLIbr3B80+ z3VT67cQV-^aigg6(v>MhDTsIXqf+)?iU#o4-3w|#zI|7Xt*!ABFzHt;OB>G`MpY&% ziiy;EUMg$Lq7D+|@yNf`)#brA)nBn-DusCr>tC}%xP1wNqGYGWB&-Kt+%&LYzLLpjBo3O%pU@}KMEl+xgug?5#eeMZZ*M5pTIz@L2p=Xq6sTNQ zHJ{b+VnaDVZM~mV-(sqZU2q9KORaAy{J}YfIfYl+Jgb+Rj?_mO_g_V(*;L`^u0<|O zhyQIn@;nmKk6@dXSXnfek*~Y0*%&U2AL$UJEoP5=tPXZS8|_6l*YK>jpWG3$`>3Gu z&Pe*eH_&hDNLLZTqn#yUHkMA9#ns_Ib>}{!8*o9Q>Ha<8I$0LHyYn?!6%}+km0Y=3AWWz5 zL*c~aq%`O6D6wI^y|@L~e99GWO(PYxPcz3!oE)idDu8bZe@-EU zZlq(U5&l3W!DhD$CfK`@5#Pp~Q=r^?#CcZ~+}+BA;rhB)h;>TS(gqq4ZXI735S-`(JQw*2UNO>Ib&~cA z&9_@wsS$+!-g7oM<8Mk9Q0Bj4aQudxgUmiMqc5bVQRW0xUVtkJKw+3;?bF{D3NESy zL40aF+8RJ$)S_K{%s0ib)4I+CG-4jMz^B_ZM~b7`(877~NW`*7EiF{Tg+_sAf|Xx5 zCjVg4H0jj*{V^pdMmerQU4K(z!xd+ydr;+x{b%aA3Sh-_1+v_B;i0P2HUX&UKgM6Q zOK*RZF4Nw-Tg3Wz+naO^Xp`UPnU$>4E}-h7U%Ji*qnFA{-g0BA@WU7iY^Yw$G%`^# zHVcIixcKt~xBB^iGp z3@t5fb8~ZClsCB97AlOotvR;EkFX4AYG{0S()V2v$3dwYkMYja~K%b>bjg5E{lkmt*IRvYMM zkDPx`B|zB_hPD1KU?|4CXa9Z)<00tuvx7pgR2Js+;DJUnl)uo&=U~+>rO{a$P3NB? zWa|XQGuB`}#3CsBPT+HGN!>%7i25SUqvez#$UimFG}+EiH;B`Z8sT@{-8U5LHx z30FMSuqs?xDRPaaj()s1WCuHD`eQI$Rddg;EG-Sxy0W3D!sD7jJ8nP5pM?tw5aBLtT>Ezo~F{N9z31aC$`tOwx&-_siZR-;He}OGw1aExggDS?qn>kae!^ocJ;%-cARcbAHYopeQTFY z^t;gqb_t*}c{tr*pgzCZCN+y7v7ib&D|LooBp>Y@9!uGKtu8dspoZ1`hl_8n_w1-; zz*~OW^GQ>Razt;nG}sc&&5106|7LcQ4?n7^nTPXgRQ71BJgL>i)~A!_UggEQK+Ka| zo&ZF2AeR%9rUUye$U@WOY{jMWf||ZHe&qOO=3tX>(^yY$RF*tYN>)~O;?LqX#FJVY zDxWMaE`GwrRoC;q@K!KzLwl`%{jh)gGwP4_YYqS2%dPH+9>0wu9bK&n=WnF(z^z`; z*;ABX+I$7UN{etNmFXz|?0SlD!IoZ|`lW5+N5k#~!!di!3+u${lm5a)X$>ms8 zdK=m-CZo^4;&4Db=AXwo$FfrJNCP!5Z4Z^7#Tk?b*EtBfVhFWwNlXCy#~Az~{T@$sAr3&$MY> zZS6+i8!N~Nbz&5>TKfDl_+EMTzM$vHu+Due_)VdC3nX{j^K7+naoFJ%9cjSxk$cor zC!FC-p~r^k2+z99i@oqEH9Z()Su!GW=`ua zfic<-4J8bxat(%{#u1VF`w7bYxVKhb6q?tch9)4|d-w!er=Z!MnN!4!@Ihdzw5szln>D%zv?t-xGXgvX`#n(Ul#65nmJxQ%| zv%d$g4nJn_PhBu(RVTHReCHNpZj5spF#O?OGgA5~k~Qi%;R5^I*!!i4r9w>@a-SFn z#I55s#v}FKVtQJ7Re?VfhQvOdOK9ho^fPL03*KB7zt4NH&-fIm1)jSqc5FCp|MQcL z)Ycx1BoCqwdf6q9<=ym=u*x?;+Pz*c< z?V!kOU-3GGgs}eW@~YEGD`$RCbwuE1+ksEUG%R|!%|ZyUquB5Asz5aCTACdgO3;K4 z6S#&?27CiRkr7&4n!C&b^?BZx!>9q*Sd}NfG*R$j-+Oj#)dGJWw%p{)G%Cu^QyQ<>l(az?qz3h|Ff=628kWoA70-80dtfW@Nohfe5T094KY_5vh(gnzNrm!6B)AKSUR z^O~EV*BooEnzfN(wrrg9z~&d^g?4Mx4DIJ3sv(YB5Q0;An7nb(m+Ej`m?Iz6A(2G5H1EdN}>6QyuvI$ccCsaH63n1 zWS=y2G6o%;@j)?(iQ%Ff;V{OGOMq5D@q8`8gp!yVOylyT*)L%($%t#YPk0l{Q zAwP#PU13Z)U=HtmQg_qvYS=y;#ucG1o2z4!g}K8mpF&lv+1b%iSzBAy)`p)k9!?{l zIceqQ=2lr#LqkVr?IV2t$4)O@ROO$~j;r^r?_;xcFAs%#x#|Z~FkhBX>fdW@(sHhi zE59ALL}Xjkl7wvu4efvSDKy<|9tO(FXygG`;tzwC5Vo&<@O7OR0#*}Ixe8bk>rCU< zYoiIYc=r{Pa6+A1s@jSk*?X8u0@3#6Cyzt40%Jqc2&}_9WH@sKGNh8>rBx^RH1d9; zw;RVE*Vp5$^XuzE#R3&7d1FT28@S1)(nyV5>f>CwofRhAvyNn5c`>Yo+cUO*PcsPG z-`(K!C)>;3`~Uaqa&lEw=`bxRG+8iUFxUq&i0{g$p(w&h(%#+63aCtsIOa=Sf9HXJKW@F?1IC=PouT1bSJ9FJl;*>pZyYEU&P6|D8 z_q!B)??CFb@Vz0QK{Aaz&NoDC7XyI9E6nef!N5%_*p>OS!^lftq)MvK_nFZ7Y!^dv zZKRPGMGAZ$H+gdtr~24y^{df#zPu?=Nl>o+AkTf`TLkw3&m* z{Kms|qi=DPNGrAz+YCmOQroX>1)bj8UyY?3oC+d?^{f*9F2N=O5_NT$((UcM--VW> zf7SqBRR8p#1;u0MGQ4=!*E7WG@A$1%TMv)A74kIIaNtwD-VnuC3cBuyr3LEg?eo>@ zm)PyEzo7B zlFNrhOBDi5HP-0Z)4K+jS5=`{=+zO88Ew?fI3zlGqPKkUxUXiSpM@+iA7hzir&WJ_ z9Ybx3$M#o!rk48D_dFaRRyX4ZTS$rtiz?NC~=Yz_9>6yGfZ~2U59G!il{NtqaE^36RP%Z%n9Y&j0us#DtDaM_`QdB zd~??$$u5UlXcF4WGmUoXc@`14^X9mI9;0=IieGM=dK}cV(F%x>4 zC=QfLgncKfZ&8R?GZ7Zl9Qo$0^}|_?qn=H|`MFd+V=7I33rlVb08wbVhE7JEN^|gj z`M_m_qk~Y=Ob~bh%R=FGE7oPI8Ca{1#FG;beO}0pm*Kx5vYh zjsQvanv3K9w|SdbQ3L@?l!iM`y3@;shC->~jy2}A0~=_=D`jh~3}gWpuwUg~OmDkk-Iup>==y_L^Mt8Vg< zm7B}Pp%YnS_dKCxA1~W6joJqyQQ{)@LvWslNl?B>q?BHL=gMHb(-el!nH+lhcdFOv z#avca2KW+9FS8Ne-|qMtE$k^d_z7E@feN;vlll%{#^)SiaACTgWnFY>;X>twBcBiW z=r>*y!qCl(APrBU)yTKF*gIcgBBY3R#S=;eJ41hM#x<*&#g5qjz6D)WeK!o_C7g^n ze2GA~Nni9H)`uv>+X&kFEj^-rdd#XWkIE>(CeUb(KpXu$B_hH`HneI?F){|Ju;?el zSP*J3RGtXiGR@1+=R1@!HYT%Qt{XIKoLOIwn?EjXZcgL2Un*2gp|AvQEN!pvPDDFj z&N-k#$Cho1DUuZPCZ@Tja|7FrW;DrFlmdPV+FE6#bkPE%CDLU56P z8dF^2j6BoOBqqt4ibnKEhv}xPTph#9%OpI=-YEO@)Ea2daCsSjj!(sc%I zbVj~fZx5_6gMDI2XsPO{`pnDK#&4fl%czHKGEw!TAEmJzXPnxb%v^2q>Bkz`9{6d> zgNZz3q{Isyd>yqRL(4k2&RW`@))NxQ6!weEbgDXzLcUFQB{2kzuLN(cP>+8%bC{Bn zQpqh1fNq8YGT{*s6PJ*nP_pxrperaUnjdU3GB6N5r5uasrB_F|PiGaD(R1pDaiTQG zagJ-XJpA9bo*h~TxW{s_vxA*qBZ6#}>s8bD8JK&W)_Y*yNyZ0-k3t9tm`XVM+Dm)5 zOF1&qkj&lXM=9ks#IzT?Q)rmeEVC@f)6o(WOX}(pwq6c+U6-jnJv|{%ABSBsOYqB{ zmkZK^xf7nbjxE$YG9dcOJ?9R3Elo5qIos7DxqtX#5W?zoM9rxja{@l*a=HBOUhj?U zYX6N620I*SapB_4*K*K|Tg!_NwyF2Zn>ZU407s-ZY#QcZv!({J2o*0u_g1t+la`J} z12kP74HP2MH8W!eNwUcfK2oj!oySQAMSV89iKv60wJ=LL&nyM8QI&K7H54^5`fYmNm)BkdfCJcsL>ioSU8*&gp#q zHZx_n_-6{{#`LE8W7xnc0M5#Bp&HU`2cWUF_X$m!LP-0glFII5MwJ(-+O<4Xv!j9)hvSyF*$AI>L_ z*Z(zzUeMeqjQ(e&Bx9-Y@FLpZWtq+l+J8E#G;u`h7mud0s@RcN(>@iroQq(f>aFl1 zR;JL%WAugxLcOL0-{YQK@{b>b-S?Y{K|ySJ&6s@%6UmnCjy|j67pk=K8~~DYwTC-{ zx*NRtE-L1p?p29a^_bCDQ3*Nohqbi><80$&Q2!n-e%C;@RKScA2=DPrmMVk^A}g9{7`yI zcJP1PuyM}yXO$mZgLDejR`uM{($H*)tgAqthnE-CILGc#JT)h0s@^XvG*4GJim;;} zG`3|l^5ms{LJ-jO-IoA20kObCesMxi>|ZEchC0QxYoBNfLI0mbf0xtN9!EaV;Hz&7 zLZQr0qB$m%Pmy^6_fF^dE6JG?c$Owz`cwAOg1zNZ($17!0wtoB5uJD7@ckSL@c-XS z5{$+UBj`4>dWLIz0)&0r|DVJRw*1lb}G2peR}lqQ5=SV;(BqpU!Hu=ge)A|fDV zMnRUdimSG+R*hC$tpn_M)!Noy@U8Xr_u=>c@ykEC&%Mt%=lMS8oadZ--8@{aZCG}I z7BGebe);m?iyNnY9jWG}WkHcomKiq!H0N%y|Z(D%p z&!6kQMelS83UsFXxtN(!p&$SN%3Gm6eq;tq#8Up-Ib;Y}>;#U89L?Di$J?Q|JUypJ zj*Ho$7h>T<^$|F8xN+`TLQY`T&LDPH0^24=&%vH-mtmWcBuL_jSmcbPL|%$*#tuDJ z3_qGbmPg=R{n2;a?F<$EvXb!`@oxrOAC@qIqx7OHD=~%h?tzbwt*>(NaXeNcgU?P);_yTs zj2(JP5!0Bs7+botGlM~Q-9~dUqgq7#Gp{&N*f5hJVTgrf5z5FMWl!JeAk;7BEPN>W@@a$hKj`T51l^7Cg;pZxUr(T_hoeDMAKdy2bvZh!aft(!NlU;F0kt6yEY zeCgtarSs>$JbUK!sgoxb7v|??kIzgWJ1U==oERS){bJ+si%9kF8k6S;vF#VRPfpm*grT3@NznT%W8z&^0I1iq1 zj*rNPMweK>GA`*DO`eBg%K8@tCk?c27+bQEO&Y!{TcDfPVYTzqE~xuK?(#{@;q5>Z z(6*O&vWbUBr=^*b5ZMVnctozJ6vr0Fa!Ih#a2TP|i=bK;tE$nQ(AK`4AWPlf8`gJ_ zc4&a3(KOVGw;7>8JIB;|Btw|$ifL~`?LY(ngd6+}b_tvdCWJJz@PaeJ=fq39OK)AD zet^GQzaD}NN9L2P4?>GcKo>_f!+`{P`T9s1vR|?ip}2 z$a)n$tFoXul~e= zC^aO}=v3E2A(9YXvSti?^8ub|RLLgm__XSM=_GX2;V;VWJg%Mzb)q_wS?QlB1Vz10 zEaZwoiHuk>0!uvOy0Gv}h@Ckh%ITMlLXX@6$>8agNrP&iq3fppMEfDn^00{XD5fqt zN07P4tuS6yk6#iflv-EBSJ&3z8?2h5^uX4Dow$xIU6ECK`{@LDYC%(xZuEXmN-Fly z0bS6c7IqvL>Nc7_t_Nv_kbq-n#J+QaMNQN(=owYvReQSgiHoP)ptpVP(5MHSQTHGD zn$a>>e*4$USENtYa`T;!NPCG$x7Ll0M^WxNo9L4UKEqGkpX`D_*zP)d8cohTKdx=+ z>|K4o!F49`cch34`m-tZ@WT*K^3-p~Qc!XC6Aj6R^8D=OvlU*kB9}NkGbfuVHV7&R zMsBo`@Kmq}Q=p;}53p*Z>KhH4TNqWi_N>lGnQ3`1rY`|IaJa!Czoo2rywm}41e5D| zsB@nKC>JFY+QU1U-42izH!_|JG~xx+405hzOs% z%hYX0mWLKAKtaab9jC7){q~Tt#G1=0)2F^K?#}XKqo3P}>_~mj%(0n;$=C=(FwX^A zm|29N0A&^#70hh^YQx$(OhaQ-vqP&vX-By%s>-PYQ*cPFNMe}U(N)poU{f*#mkHRt z6h@hQQs%t>-dIR(=omQLRD_(4rG?UM?UE5eu^WN=z}@vA3h@|Wta$c(dF6#-O|PE& z2r;CZY_!EVMyi6;zm!tj;=JF=882^C$#?ypM0AIG)!wj4w^SIo){}H@7;CJk+s$F~ z$0HiB<6Hz8k*3x-%$lg#IW>1hL$)NpHj$Wa3w1?Eg#Yu$AbR9K=GVqv6CN#j6$+2 z?7TJHQrPyUkkIUeC>TLlz=k@|pd|@>d~_vSpij%Hj|d6GHMjWo7<6>WFg?8oum{^Q z%EKuncKAS>UUAq!S@{uvhYU<-y}KtKT*NYKB=u2)M4toDY5h~!Gm%&K5z$9u#6ge{ z*!XUD3^I)bFSrpz1Zn2x4;@kYSm@SYjpt=_h2vx%lCUN6?8rSaa;>aYe6#`KWU_aw zo`>UXh*Q7F(|{9=JcjlK3!VIpFtHX53cR&>=jGE2FU_onZ#*F%>haVl?9Uj<07toQ zh36fE)bGo)$K4O!#0-!xeqJ`178Vh?568-3>#OF6sU)VLOLC$C;}FZn6n80ddh z`pS6lW@7iZK7JY(zqp~G^)5pvpKrH0`_OY8I$dm%MfC)8g}n8EI2|jY212B4s7_jS zSZ$qY1-yyf+OG6D9<@JOr>ZZcv#X+U&|jx;M6KTxz?V%THgMY$W{AkiS^3BeW|6?! ze|bZ&Sk<-Pg9J$yB8+0&Lg7Z%U4bO@KDKDbPd5``=d~Pwm&@A5yUkwzg@dRgiOuB8 zbIOXeWpHi!Up~+)+YuLBY-vT}0R#(? zwh0aX2%gkHf0v2;X~(vLgmW);_=IDqm;SX{uxM)+tQ#L(uTUbZi;d(+W#EfPdLyZ~ zQZ&Z-%lJ*L98jQDrRFj+s(76xKFw+k?IWHYJh6pt*IhAU-7eD}ztzrIDXO@O1>^It ztuwkv(Yg4L_#}(~COKsDl`qhF?sSwGQ_P3zmPuVJs%rT4jc(22R<`b*j);VoZfqU7 zw}Svw`FEFmChABhWcnW3t22}rkE;}q7LRXf-~!&q>`(FK=DA_23k4VK`H1sQcm`Yn zKTbeLiILZCo1;-TP>+AQ4MF{i625r#`u8_FYo6^(A7GWO*Ml)6qGw<+AU>~qsSM;o z7M|L5%{1mM(v9(?e6OA}Wb<`9Z{v)@FcqOiQph8lF2yFgjr3)V(In+W$AjptiLAU$L)s!3F*;_q#rUVzQ0r%Z5$?`=3M&BB*c)sUz@#oimLOwh(AIeXOAN*j&Lv}5r#(cnGsoYp1ek4OY?XBBYe1%6G$ zg^zp~%7o-k0mh!f{Ci8|Y%XN+sh#eOmSfT*KL9Uzp!Q<{wA)i>?#;N@c>qU6UtI+ z4@9S;52Zx<59MLu#I77)e>~V8glKh&4Uaob2n@2MjCwmG0nE&*w?!2aRlKXTR1X(OR#DL`yw3Ai)jgd>n zg^GeLeSt29hc4*J0;peX0qf5{y&mF2^itzL1Kkn6BbnZ?oV^S}ez>^ELQ>*gCf$}> z=~+lksY%1dRPLe1Ns}S<7zZl4X4`IjduFGjlhl;}mcWunipGfOA#dbiKO88MYuL;| z78Y@6BWIJPudaUm&9&=VYl2++0HPq3$8ZbKiowDCTIV;j=?OTkU7U7fZn+FoMa~ZHJE)d>7*qHu zC>Y&Jgvnr=j)aUh;NzQzHp1KLJV=NN1RX$(v`@v&M)}h<(5k|V+7Q%36z?_(4G*I} zUOo-~8UAQR`Revs^Bt4RI&qgylU+xZi{6bqmwDql9u-#z}NQsG@MJ^i638u*#| bWW^%>x7XlHV^#Q1uOX^B?ki3(VEg|7!QHuF literal 0 HcmV?d00001 diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..db3a483e4b74971fbfb1cc0fb6499852cedfe650 GIT binary patch literal 701 zcmZ?wbhEHbRAo?Qn8?Ji_w)@mZNLBj|1&T!DE{a6a}5c0b_{Se(lcOY1PT3QVdY|A zV$cDyff`g97?`@J^shYqmS1s(LX_+4yGox$4el*+Jm?ved2^25GBru=T^dGm#<906 za&AQCZ08H8P;Bd&{NT;vl&}c_^L4%p?g_hjBu{YB29{c>Ob}p@z~Ks3xCw+@!HClp xtZ<(QPf3`00FNu+VbOvoEE+h73k#4LIKl$IE8t;)<_eUs!0QU6uz&iJhvXcHF*h)T1OnEW1i^?zgDfop1p?usL*#PMGT;HQkSO{q6FlJyb$PWkPf|h*eTST}7h8z$}MF(XD(aQ)ZLZ zM?v0rT<1C4XHn<6PbNA{XL@>1^)apdD_@tcYDrW#m`k#MmslI7p^P;Az74wGs`!SI zLs$GEZHsafXsu1i-WleMzAL(yw$-LK{0hv;6hrx8kx!!4$``dAyBnY9Jz&DqJo2$A z!(L$H=KqBeY~CF_viHPz^tTglc?D97CqEBjzUwH}7GI zapg8YZM~>2Wk%E$d&r@9ly9b4Q zJpM7T@}r63I(OExUlG%Xcjz3MU+9U^r!SkpjNThDtaP)7>j6L5z%o5|^hlVOyI*uY zt^UU6NTuY?(Lb4ZIU2Zb5Vz}Pb7KF%ivf&j^CL>$cDz?rMNTQQ|NqDVD7mhghUp%h zhIA{gi{S8y9YhIIbSv$`B!JiPi!0#4#Jge0)p&YVPHchWcyAn zQhvb8ggXGXs9;k`u9Uq*YB>O+Q3Rq=2hlLFcG{Q3ORH_}JnY8C+r%@}6|%ySP%bWG zV~mA;?P`Q2L_Ss})nrJ{$TmeA9Tt*4=}X5x%RioM@_?ZsKSEST-f+GBv~Ya)xX3O{ z8!d=YthI-13OI;RN~`>|6u5L{z20oBp%9MIj)n$!Aw{Wpq&Rtr4~*_74Gjo@3el>B zz(Rk;;>2lp73<2;d=r*8z%WkdsG=vRuG_fvxO#uN^El|+5Qoz^X!2MfxJ3m}vyi?> zMLLDi8+${Z6YbUg?8GNR>-+SwHKdFyr%HqWcs|X_l*-DAC^bG&KCqWg7-_`UlwQ`EdOp_LJkr`L$mHHs75uP?fSgVfsDjuE#ft2b8HDt0yFt!+;C zEgL=)G9ZFt4wa+N3Xg7FGc0~`&EEt6_%7tyzmnb9B_h1~7~GD4V-Bhx7~QKRkF>&aT>(-!Us@aJxAY@8E?HW$G8g zSz@7Jcp>iCp;lU1ieF6n7!oAa-1E!rS0 zF1lBFVS%G#ZO}b@*+bIk+7@Q|iG60vIDVpV%4tW8rKyzwRo_<25;8*Ky@n z-sX>W*b;M){5lB_Edc@m1`VHy0@dg$PTR9uE$O2&a?KAe?xRlCj&Z$iZYw + + + Sa-SSO-Server 认证中心-登录 + + + + + + +
+
+
+
+ +
+ +
+ This page is provided by Sa-Token-SSO +
+
+ + + + + + + + diff --git a/sa-token-doc/doc/_sidebar.md b/sa-token-doc/doc/_sidebar.md index 116916ca..9e6a4c54 100644 --- a/sa-token-doc/doc/_sidebar.md +++ b/sa-token-doc/doc/_sidebar.md @@ -33,7 +33,12 @@ - [集群、分布式](/senior/dcs) - [多账号验证](/use/many-account) - - [单点登录](/sso/readme) + +- **单点登录** + - [单点登录简述](/sso/readme) + - [SSO模式一 共享Cookie同步会话](/sso/sso-type1) + - [SSO模式二 URL重定向传播会话](/sso/sso-type2) + - [SSO模式三 Http请求获取会话](/sso/sso-type3) - **插件** - [AOP注解鉴权](/plugin/aop-at) diff --git a/sa-token-doc/doc/index.html b/sa-token-doc/doc/index.html index debc001f..42f7a71e 100644 --- a/sa-token-doc/doc/index.html +++ b/sa-token-doc/doc/index.html @@ -66,9 +66,10 @@ errorText: '错误', successText: '复制成功' }, + // sidebarDisplayLevel : 1 , // 设置侧边栏显示级别 // search: 'auto', // 搜索功能 alias: { - '/sso/_sidebar.md': '/sso/_sidebar.md', + // '/sso/_sidebar.md': '/sso/_sidebar.md', '/.*/_sidebar.md': '/_sidebar.md' }, // tab选项卡 @@ -125,6 +126,10 @@ + + + diff --git a/sa-token-doc/doc/sso/readme.md b/sa-token-doc/doc/sso/readme.md index 7abcf199..3e487afb 100644 --- a/sa-token-doc/doc/sso/readme.md +++ b/sa-token-doc/doc/sso/readme.md @@ -3,36 +3,37 @@ --- ### 什么是单点登录?解决什么问题? -举个场景:假设我们的系统被切割成N个部分:商城、论坛、直播、社交…… 如果用户每访问其中一个模块都要进行一次登录注册,那么用户将会疯掉, -为了不让用户疯掉,我们急需一套机制将这N个系统的授权进行共享,让用户在其中一个系统登录之后,便可以畅通无阻的访问其它系统 + +举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, +为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统 单点登录——就是为了解决这个问题而生! -简而言之,单点登录可以做到:**`在多个系统中,用户只需登录一次,就可以访问所有系统。`** +简而言之,单点登录可以做到:**`在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。`** ### 架构选型 -对于单点登录,网上教程很多,但大多数讲述的都是CAS重定向机制,相对来讲,CAS模式较为复杂且并不是实现单点登录的唯一方式。 +对于单点登录,网上教程大多以CAS模式为主,其实对于不同的系统架构,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式: -对于不同的系统架构来讲,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式 + -| 系统架构 | 采用模式 | 简介 | 文档链接 | -| :-------- | :-------- | :-------- | :-------- | -| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie + 子系统 [权限缓存与业务缓存分离] | [详情](/sso/sso-type1) | -| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [详情]() | -| 前端不同域 + 后端 不同Redis | 模式三 | SSO认证中心开放接口校验Ticket | [详情]() | +| 系统架构 | 采用模式 | 简介 | 文档链接 | +| :-------- | :-------- | :-------- | :-------- | +| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso1) | +| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso2-server) | +| 前端不同域 + 后端 不同Redis | 模式三 | Http请求获取会话 | [文档](/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso3-server) | 1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com` -2. 后端同Redis:就是指多个系统可以连接同一个Redis,或者其它的缓存数据中心。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](http://sa-token.dev33.cn/doc/index.html#/plugin/alone-redis) -3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,CAS模式(Sa-Token对CAS模式提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成) -4. 只有根据自己的系统架构合理选择,才可对症下药,事半功倍,否则只能是劳而无功,不得要领 +2. 后端同Redis:就是指多个系统可以连接同一个Redis,其它的缓存数据中心亦可。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳:[Alone独立Redis插件](http://sa-token.dev33.cn/doc/index.html#/plugin/alone-redis) +3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成) +4. 技术选型一定要根据系统架构对症下药,切不可胡乱选择 ### Sa-Token-SSO 特性 1. API简单易用,文档介绍详细,且提供直接可用的集成示例 2. 支持三种模式,不论是否跨域、是否共享Redis,都可以完美解决 -3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段 (文档讲述攻击原理和防御手段) +3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段(文档讲述攻击原理和防御手段) 4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面 5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合…… 6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发 diff --git a/sa-token-doc/doc/sso/sso-type1.md b/sa-token-doc/doc/sso/sso-type1.md index 1bc7c231..387137ad 100644 --- a/sa-token-doc/doc/sso/sso-type1.md +++ b/sa-token-doc/doc/sso/sso-type1.md @@ -1,12 +1,15 @@ # SSO模式一 共享Cookie同步会话 -如果我们的系统可以保证部署在同一个主域名之下,并且后端连接同一个Redis,那么便可以使用 `[共享Cookie同步会话]` 的方式做到单点登录 +如果我们的系统可以保证部署在同一个主域名之下,并且后端连接同一个Redis,那么便可以使用 **`[共享Cookie同步会话]`** 的方式做到单点登录 + +> Sa-Token整合同域单点登录非常简单,相比于正常的登录,你只需增加配置 `sa-token.cookie-domain=xxx.com` 指定一下Cookie写入时的父级域名即可
+> 整合示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso1/`,如遇到难点可结合源码进行测试学习 --- -### 解决思路? +### 0、解决思路? -首先我们分析一下多个系统之间为什么无法同步登录状态? +首先我们分析一下多个系统之间,为什么无法同步登录状态? 1. 前端的`Token`无法在多个系统下共享 2. 后端的`Session`无法在多个系统间共享 @@ -18,16 +21,14 @@ 而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](/plugin/alone-redis) -> PS:这里建议不要用B项目去连接A项目的Redis,也不要A项目连接B项目的Redis,而是抽离出一个单独的 SSO-Redis,A项目和B项目一起来连接这个 SSO-Redis +> PS:这里建议不要用B项目去连接A项目的Redis,也不要A项目连接B项目的Redis,而是抽离出一个单独的 SSO-Redis,A 和 B 一起连接这个 SSO-Redis OK,所有理论就绪,下面开始实战 -### 集成步骤 -Sa-Token整合同域下的单点登录非常简单,相比于正常的登录,你只需要在配置文件中增加配置 `sa-token.cookie-domain=xxx.com` 来指定一下Cookie写入时指定的父级域名即可,详细步骤示例如下: +### 1、准备工作 -#### 1. 准备工作 首先修改hosts文件`(C:\WINDOWS\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: ``` text 127.0.0.1 s1.stp.com @@ -35,8 +36,8 @@ Sa-Token整合同域下的单点登录非常简单,相比于正常的登录, 127.0.0.1 s3.stp.com ``` -#### 2. 指定Cookie的作用域 -常规情况下,在`s1.stp.com`域名访问服务器,其Cookie也只能写入到`s1.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要在配置文件中新增配置: +### 2、指定Cookie的作用域 +在`s1.stp.com`访问服务器,其Cookie也只能写入到`s1.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要新增配置: ``` yml spring: sa-token: @@ -44,8 +45,8 @@ spring: cookie-domain: stp.com ``` -#### 3. 新增测试Controller -新建`SSOController.java`控制器,写入代码: +### 3、新增测试Controller +新建`SsoController.java`控制器,写入代码: ``` java /** * 测试: 同域单点登录 @@ -53,7 +54,7 @@ spring: */ @RestController @RequestMapping("/sso/") -public class SSOController { +public class SsoController { // 测试:进行登录 @RequestMapping("doLogin") @@ -74,45 +75,40 @@ public class SSOController { } ``` -#### 4、访问测试 +``` java +// 启动类 +@SpringBootApplication +public class SaSsoApplication { + public static void main(String[] args) { + SpringApplication.run(SaSsoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } +} +``` + + +### 4、访问测试 启动项目,依次访问: - [http://s1.stp.com:8081/sso/isLogin](http://s1.stp.com:8081/sso/isLogin) - [http://s2.stp.com:8081/sso/isLogin](http://s2.stp.com:8081/sso/isLogin) - [http://s3.stp.com:8081/sso/isLogin](http://s3.stp.com:8081/sso/isLogin) 均返回以下结果: -``` js -{ - "code": 200, - "msg": "是否登录: false", - "data": null -} -``` -现在访问任意节点的登录接口: -- [http://s1.stp.com:8081/sso/doLogin](http://s1.stp.com:8081/sso/doLogin) +![sso-type1-wd.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type1-wd.png 's-w-sh') -``` js -{ - "code": 200, - "msg": "登录成功: 10001", - "data": null -} -``` +现在访问任意节点的登录接口:[http://s1.stp.com:8081/sso/doLogin](http://s1.stp.com:8081/sso/doLogin) + +![sso-type1-login.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type1-login.png 's-w-sh') 然后再次刷新上面三个测试接口,均可以得到以下结果: -``` js -{ - "code": 200, - "msg": "是否登录: true", - "data": null -} -``` + +![sso-type1-yd.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type1-yd.png 's-w-sh') 测试完毕 -### 跨域模式下的解决方案 +### 5、跨域模式下的解决方案 如上,我们使用极其简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制: @@ -122,50 +118,6 @@ public class SSOController { 且往下看,[SSO模式二:URL重定向传播会话](/sso/sso-type2) - - - diff --git a/sa-token-doc/doc/sso/sso-type2.md b/sa-token-doc/doc/sso/sso-type2.md index db02f701..b7e343ca 100644 --- a/sa-token-doc/doc/sso/sso-type2.md +++ b/sa-token-doc/doc/sso/sso-type2.md @@ -1,11 +1,11 @@ # SSO模式二 URL重定向传播会话 -如果我们的系统部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 [URL重定向传播会话] 的方式做到单点登录 +如果我们的系统部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 **`[URL重定向传播会话]`** 的方式做到单点登录 ### 0、解题思路 -首先我们再次复习一下多个系统之间为什么无法同步登录状态? +首先我们再次复习一下多个系统之间,为什么无法同步登录状态? 1. 前端的`Token`无法在多个系统下共享 2. 后端的`Session`无法在多个系统间共享 @@ -25,12 +25,12 @@ 整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击`[登录]`按钮,由于此用户在SSO认证中心已有会话登录, 所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。 -下面我们按照步骤依次完成上述步骤 +下面我们按照步骤依次完成上述过程 ### 1、搭建SSO-Server认证中心 -> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso-server/`,如遇到难点可结合源码进行测试学习 +> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso2-server/`,如遇到难点可结合源码进行测试学习 ##### 1.1、创建SSO-Server端项目 创建一个SpringBoot项目 `sa-token-demo-sso-server`(不会的同学自行百度或参考仓库示例),添加pom依赖: @@ -116,12 +116,10 @@ spring: sa-token: # SSO-相关配置 sso: - # Ticket有效期 (单位: 秒),默认三分钟 + # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 所有允许的授权回调地址 (此处为了方便测试配置为*,线上生产环境一定要配置为详细地地址) allow-url: "*" - # 接口调用秘钥(模式三才会用到此参数) - # secret-key: # Redis配置 redis: @@ -134,7 +132,7 @@ spring: # Redis服务器连接密码(默认为空) password: ``` -注意点:`allow-url`为了方便测试配置为*,线上生产环境一定要配置为详细URL地址 +注意点:`allow-url`为了方便测试配置为*,线上生产环境一定要配置为详细URL地址 (详见下方“配置域名校验”) ##### 1.4、创建SSO-Server端启动类 ``` java @@ -147,12 +145,10 @@ public class SaSsoServerApplication { } ``` -启动此项目,用作**`SSO-Server认证中心`** - ### 2、搭建SSO-Client应用端 -> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso-client/`,如遇到难点可结合源码进行测试学习 +> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso2-client/`,如遇到难点可结合源码进行测试学习 ##### 2.1、创建SSO-Client端项目 创建一个SpringBoot项目 `sa-token-demo-sso-client`,添加pom依赖: @@ -206,38 +202,40 @@ public class SsoClientController { public String index() { String str = "

Sa-Token SSO-Client 应用端

" + "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录

"; + "

登录

"; return str; } // SSO-Client端:登录地址 @RequestMapping("ssoLogin") - public Object login(String back, String ticket) { + public Object ssoLogin(String back, String ticket) { // 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回 if(StpUtil.isLogin()) { return new ModelAndView("redirect:" + back); } /* * 接下来两种情况: - * ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 * ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心 + * ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 */ - if(ticket != null) { - Object loginId = SaSsoUtil.getLoginId(ticket); + if(ticket == null) { + String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back); + return new ModelAndView("redirect:" + serverAuthUrl); + } else { + Object loginId = checkTicket(ticket); if(loginId != null ) { - // 如果ticket是有效的 (可以获取到值),需要就此登录 且清除此ticket + // loginId有值,说明ticket有效 StpUtil.login(loginId); - SaSsoUtil.deleteTicket(ticket); - // 最后重定向回back地址 return new ModelAndView("redirect:" + back); } // 此处向客户端提示ticket无效即可,不要重定向到SSO认证中心,否则容易引起无限重定向 return "ticket无效: " + ticket; } - - // 重定向至 SSO-Server端 认证地址 - String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back); - return new ModelAndView("redirect:" + serverAuthUrl); + } + + // SSO-Client端:校验ticket,获取账号id + private Object checkTicket(String ticket) { + return SaSsoUtil.checkTicket(ticket); } } @@ -255,8 +253,8 @@ spring: sa-token: # SSO-相关配置 sso: - # SSO-Server端授权地址 - server-url: http://sa-sso-server.com:9000/ssoAuth + # SSO-Server端 单点登录地址 + auth-url: http://sa-sso-server.com:9000/ssoAuth # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) alone-redis: @@ -377,7 +375,7 @@ public class SaSsoClientApplication { | 配置方式 | 举例 | 安全性 | 建议 | | :-------- | :-------- | :-------- | :-------- | -| 配置为* | `*` | | 禁止在生产环境下使用 | +| 配置为* | `*` | | **禁止在生产环境下使用** | | 配置到域名 | `http://sa-sso-client1.com/*` | | 不建议在生产环境下使用 | | 配置到详细地址| `http://sa-sso-client1.com:9001/ssoLogin` | | 可以在生产环境下使用 | @@ -389,6 +387,12 @@ Token作为长时间有效的会话凭证,在任何时候都不应该直接在 +### 6、跨Redis的单点登录 +以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享Redis来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录? + +这就要采用模式三了,且往下看:[Http请求获取会话](/sso/sso-type3) + + diff --git a/sa-token-doc/doc/sso/sso-type3.md b/sa-token-doc/doc/sso/sso-type3.md index 4ec5f6be..cf993d02 100644 --- a/sa-token-doc/doc/sso/sso-type3.md +++ b/sa-token-doc/doc/sso/sso-type3.md @@ -1,2 +1,244 @@ -# SSO模式三 SSO认证中心开放接口校验Ticket +# SSO模式三 Http请求获取会话 + +如果既无法做到前端同域,也无法做到后端同Redis,那么可以使用模式三完成单点登录 + +> 阅读本篇之前请务必先熟读SSO模式二!因为模式三仅仅属于模式二的一个特殊场景,熟读模式二有助于您快速理解本章内容 + + +### 0、问题分析 +我们先来分析一下,当后端不使用共享Redis时,会对架构发生哪些影响 + +1. Client端 无法直连 Redis 校验 ticket,取出账号id +2. Client端 无法与 Server端 共用一套会话,需要自行维护子会话 +3. 由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销 + +所以模式三的主要目标:也就是在 模式二的基础上 解决上述 三个难题 + +> 模式三的Demo示例地址:
+> SSO-Server端: `/sa-token-demo/sa-token-demo-sso3-server/` [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-sso3-server)
+> SSO-Client端: `/sa-token-demo/sa-token-demo-sso3-client/` [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-sso3-client)
+> 如遇难点可参考示例 + + +### 1、SSO-Server认证中心开放ticket校验接口 +既然Client端无法直连Redis校验ticket,那就在Server端开放ticket校验接口,然后Client端通过http请求获取数据 + +##### 1.1、添加依赖 +首先在Server端和Client端均添加以下依赖(如果不需要单点注销功能则Server端可不引入) +``` xml + + + com.ejlchina + okhttps + 3.1.1 + +``` +> OkHttps是一个轻量级http请求工具,详情参考:[OkHttps](https://gitee.com/ejlchina-zhxu/okhttps) + +##### 1.2、认证中心开放接口 +在SSO-Server端的`SsoServerController`中,新增以下接口: +``` java +// SSO-Server端:校验ticket 获取账号id +@RequestMapping("checkTicket") +public Object checkTicket(String ticket, String sloCallback) { + // 校验ticket,获取对应的账号id + Object loginId = SaSsoUtil.checkTicket(ticket); + + // 注册此客户端的单点注销回调URL(不需要单点注销功能可删除此行代码) + SaSsoUtil.registerSloCallbackUrl(loginId, sloCallback); + + // 返回给Client端 + return loginId; +} +``` +此接口的作用是让Client端通过http请求校验ticket,获取对应的账号id + +##### 1.3、Client端新增配置 +``` yml +spring: + sa-token: + sso: + # SSO-Server端 ticket校验地址 + check-ticket-url: http://sa-sso-server.com:9000/checkTicket +``` + +##### 1.4、修改校验ticket的逻辑 +在模式二的`SsoClientController`中,校验ticket的方法是: +``` java +// SSO-Client端:校验ticket,获取账号id +private Object checkTicket(String ticket) { + return SaSsoUtil.checkTicket(ticket); +} +``` +不能直连Redis后,上述方法也将无效,我们把它改为以下方式: +``` java +// SSO-Client端:校验ticket码,获取对应的账号id +private Object checkTicket(String ticket) { + // 构建单点注销的回调URL(不需要单点注销时此值可填null ) + String sloCallback = SaHolder.getRequest().getUrl().replace("/ssoLogin", "/sloCallback"); + + // 使用OkHttps请求SSO-Server端,校验ticket + String checkUrl = SaSsoUtil.buildCheckTicketUrl(ticket, sloCallback); + String loginId = OkHttps.sync(checkUrl).get().getBody().toString(); + + // 判断返回值是否为有效账号Id + return (SaFoxUtil.isEmpty(loginId) ? null : loginId); +} +``` + +##### 1.5 启动项目测试 +启动SSO-Server、SSO-Client,访问测试:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) +> 注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰 + + +### 2、无刷单点注销 + +有了单点登录就必然要有单点注销,网上给出的大多数解决方案是将注销请求重定向至SSO-Server中心,逐个通知Client端下线 + +在某些场景下,页面的跳转可能造成不太好的用户体验,Sa-Token-SSO 允许你以 `REST API` 的形式构建接口,做到页面无刷新单点注销 + +1. Client端校验ticket的时候将注销回调地址发送到Server端 +2. Server端将注销回调地址存储到Set集合 +3. Client端向Server端发送单点注销请求 +4. Server端遍历Set集合,逐个通知Client端下线 +5. Server端注销下线 +6. 单点注销完成 + +##### 2.1、SSO-Server认证中心增加单点注销接口 +新建 `SsoServerLogoutController` 增加以下代码 +``` java +/** + * Sa-Token-SSO Server端 单点注销 Controller + */ +@RestController +public class SsoServerLogoutController { + + // SSO-Server端:单点注销 + @RequestMapping("ssoLogout") + public String ssoLogout(String loginId, String secretkey) { + + // 遍历通知Client端注销会话 (为了提高响应速度这里可将sync换为async) + SaSsoUtil.singleLogout(secretkey, loginId, url -> OkHttps.sync(url).get()); + + // 完成 + return "ok"; + } + +} +``` + +并在 `application.yml` 下配置API调用秘钥 +``` yml +spring: + sa-token: + sso: + # API调用秘钥(用于SSO模式三的单点注销功能) + secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +##### 2.2、SSO-Client端增加注销接口 +新建 `SsoClientLogoutController` 增加以下代码 +``` java +/** + * Sa-Token-SSO Client端 单点注销 Controller + * @author kong + */ +@RestController +public class SsoClientLogoutController { + + // SSO-Client端:单端注销 (其它Client端会话不受影响) + @RequestMapping("logout") + public AjaxJson logout() { + StpUtil.logout(); + return AjaxJson.getSuccess(); + } + + // SSO-Client端:单点注销 (所有端一起下线) + @RequestMapping("ssoLogout") + public AjaxJson ssoLogout() { + // 如果未登录,则无需注销 + if(StpUtil.isLogin() == false) { + return AjaxJson.getSuccess(); + } + // 调用SSO-Server认证中心API + String url = SaSsoUtil.buildSloUrl(StpUtil.getLoginId()); + String res = OkHttps.sync(url).get().getBody().toString(); + if(res.equals("ok")) { + return AjaxJson.getSuccess("单点注销成功"); + } + return AjaxJson.getError("单点注销失败"); + } + + // 单点注销的回调 + @RequestMapping("sloCallback") + public String sloCallback(String loginId, String secretkey) { + SaSsoUtil.checkSecretkey(secretkey); + StpUtil.logoutByLoginId(loginId); + return "ok"; + } + +} +``` + +并在 `application.yml` 增加配置: API调用秘钥 和 单点注销接口URL +``` yml +spring: + sa-token: + sso: + # SSO-Server端 单点注销地址 + slo-url: http://sa-sso-server.com:9000/ssoLogout + # 接口调用秘钥(用于SSO模式三的单点注销功能) + secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +##### 2.3 更改Client端首页代码 +为了方便测试,我们更改一下Client端中`SsoClientController`类的`index`方法代码 +``` java +// SSO-Client端:首页 +@RequestMapping("/") +public String index() { + String str = "

Sa-Token SSO-Client 应用端

" + + "

当前会话是否登录:" + StpUtil.isLogin() + "

" + + "

登录" + + " 注销

"; + return str; +} +``` +PS:相比于模式二,增加了单点注销的按钮 + + +##### 2.4 启动测试 +启动SSO-Server、SSO-Client,访问测试:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/), +我们主要的测试点在于 `单点注销`,正常登陆即可 + +![sso-type3-client-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-client-index.png 's-w-sh') + +点击 **`[注销]`** 按钮,即可单点注销成功 + +![sso-type3-slo.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo.png 's-w-sh') + +![sso-type3-slo-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo-index.png 's-w-sh') + +PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用Ajax调用接口即可做到无刷单点登录退出 + +例如我们使用 [APIPost接口测试工具](https://www.apipost.cn/) 可以做到同样的效果: + +![sso-slo-apipost.png](https://oss.dev33.cn/sa-token/doc/sso/sso-slo-apipost.png 's-w-sh') + +测试完毕! + + + + +### 3、后记 +当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享 + +当我们理解这一点之后,三种模式的工作原理也浮出水面: + +- 模式一:采用共享Cookie来做到前端Token的共享,从而达到后端的Session会话共享 +- 模式二:采用URL重定向,以ticket码为授权中介,做到多个系统间的会话传播 +- 模式三:采用Http请求主动查询会话,做到Client端与Server端的会话同步 + + +