SSO 三种模式
@ -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 + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 以数组形式写入允许的授权回调地址
|
||||
|
@ -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_";
|
||||
|
||||
}
|
||||
|
@ -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<String> urlSet = StpUtil.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY, ()-> new HashSet<String>());
|
||||
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<String> urlSet = StpUtil.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY,
|
||||
() -> new HashSet<String>());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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> T getLoginId(String ticket, Class<T> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组的所有元素使用逗号拼接在一起
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
40
sa-token-demo/sa-token-demo-sso1/pom.xml
Normal file
@ -0,0 +1,40 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso1</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.20.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- springboot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
@ -0,0 +1,9 @@
|
||||
# 端口
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
spring:
|
||||
sa-token:
|
||||
# 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie
|
||||
cookie-domain: stp.com
|
||||
|
@ -3,7 +3,7 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso-client</artifactId>
|
||||
<artifactId>sa-token-demo-sso2-client</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
@ -53,7 +53,7 @@
|
||||
<artifactId>sa-token-alone-redis</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -20,38 +20,40 @@ public class SsoClientController {
|
||||
public String index() {
|
||||
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + lencodeURIComponent(location.href);\">登录</a></p>";
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + encodeURIComponent(location.href);\">登录</a></p>";
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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:
|
12
sa-token-demo/sa-token-demo-sso2-server/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
target/
|
||||
|
||||
node_modules/
|
||||
bin/
|
||||
.settings/
|
||||
unpackage/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
.idea/
|
||||
|
||||
.factorypath
|
@ -3,7 +3,7 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso-server</artifactId>
|
||||
<artifactId>sa-token-demo-sso2-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
@ -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 {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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端尚未登录,请先访问<a href='/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>进行登录之后,刷新页面开始授权";
|
||||
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("登录失败!");
|
||||
}
|
||||
|
||||
}
|
@ -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> T getData(Class<T> 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
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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:
|
Before ![]() (image error) Size: 5.8 KiB After ![]() (image error) Size: 5.8 KiB ![]() ![]() |
Before ![]() (image error) Size: 11 KiB After ![]() (image error) Size: 11 KiB ![]() ![]() |
Before ![]() (image error) Size: 5.7 KiB After ![]() (image error) Size: 5.7 KiB ![]() ![]() |
Before ![]() (image error) Size: 701 B After ![]() (image error) Size: 701 B ![]() ![]() |
Before ![]() (image error) Size: 1.7 KiB After ![]() (image error) Size: 1.7 KiB ![]() ![]() |
12
sa-token-demo/sa-token-demo-sso3-client/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
target/
|
||||
|
||||
node_modules/
|
||||
bin/
|
||||
.settings/
|
||||
unpackage/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
.idea/
|
||||
|
||||
.factorypath
|
61
sa-token-demo/sa-token-demo-sso3-client/pom.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso3-client</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.20.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 提供Redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Http请求工具 -->
|
||||
<dependency>
|
||||
<groupId>com.ejlchina</groupId>
|
||||
<artifactId>okhttps</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
@ -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端启动成功");
|
||||
}
|
||||
|
||||
}
|
@ -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 = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + encodeURIComponent(location.href);\">登录</a>" +
|
||||
" <a href='/ssoLogout' target='_blank'>注销</a></p>";
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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> T getData(Class<T> 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
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
12
sa-token-demo/sa-token-demo-sso3-server/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
target/
|
||||
|
||||
node_modules/
|
||||
bin/
|
||||
.settings/
|
||||
unpackage/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
.idea/
|
||||
|
||||
.factorypath
|
66
sa-token-demo/sa-token-demo-sso3-server/pom.xml
Normal file
@ -0,0 +1,66 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso3-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.20.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token整合redis (使用jackson序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 提供redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 视图引擎 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Http请求工具 -->
|
||||
<dependency>
|
||||
<groupId>com.ejlchina</groupId>
|
||||
<artifactId>okhttps</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
@ -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 认证中心启动成功");
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -27,10 +27,6 @@ public class SsoServerController {
|
||||
*/
|
||||
// 情况1:尚未登录
|
||||
if(StpUtil.isLogin() == false) {
|
||||
// String msg = "当前会话在SSO-Server端尚未登录,请先访问"
|
||||
// + "<a href='/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
|
||||
// + "进行登录之后,刷新页面开始授权";
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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> T getData(Class<T> 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
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
2
sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js
vendored
Normal file
@ -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?'<h3 style="'+(e?n.title[1]:"")+'">'+(e?n.title[0]:n.title)+"</h3>":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e='<span yes type="1">'+n.btn[0]+"</span>",2===t&&(e='<span no type="0">'+n.btn[1]+"</span>"+e),'<div class="layui-m-layerbtn">'+e+"</div>"):""}();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='<i></i><i class="layui-m-layerload"></i><i></i><p>'+(n.content||"")+"</p>"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"<div "+("string"==typeof n.shade?'style="'+n.shade+'"':"")+' class="layui-m-layershade"></div>':"")+'<div class="layui-m-layermain" '+(n.fixed?"":'style="position:static;"')+'><div class="layui-m-layersection"><div class="layui-m-layerchild '+(n.skin?"layui-m-layer-"+n.skin+" ":"")+(n.className?n.className:"")+" "+(n.anim?"layui-m-anim-"+n.anim:"")+'" '+(n.style?'style="'+n.style+'"':"")+">"+l+'<div class="layui-m-layercont">'+n.content+"</div>"+c+"</div></div></div>",!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;o<r;o++)l.touch(s[o],a);if(e.shade&&e.shadeClose){var c=t[i]("layui-m-layershade")[0];l.touch(c,function(){layer.close(n.index,e.end)})}e.end&&(l.end[n.index]=e.end)},e.layer={v:"2.0",index:r,open:function(e){var t=new c(e||{});return t.index},close:function(e){var n=a("#"+o[0]+e)[0];n&&(n.innerHTML="",t.body.removeChild(n),clearTimeout(l.timer[e]),delete l.timer[e],"function"==typeof l.end[e]&&l.end[e](),delete l.end[e])},closeAll:function(){for(var e=t[i](o[0]),n=0,a=e.length;n<a;n++)layer.close(0|e[0].getAttribute("index"))}},"function"==typeof define?define(function(){return layer}):function(){var e=document.scripts,n=e[e.length-1],i=n.src,a=i.substring(0,i.lastIndexOf("/")+1);n.getAttribute("merge")||document.head.appendChild(function(){var e=t.createElement("link");return e.href=a+"need/layer.css?2.0",e.type="text/css",e.rel="styleSheet",e.id="layermcss",e}())}()}(window);
|
After ![]() (image error) Size: 5.8 KiB |
After ![]() (image error) Size: 11 KiB |
After ![]() (image error) Size: 5.7 KiB |
After ![]() (image error) Size: 701 B |
After ![]() (image error) Size: 1.7 KiB |
@ -0,0 +1,59 @@
|
||||
*{margin: 0; padding: 0;}
|
||||
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
|
||||
::-webkit-input-placeholder{color: #ccc;}
|
||||
|
||||
/* 视图盒子 */
|
||||
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
|
||||
/* 背景 EAEFF3 */
|
||||
.bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);}
|
||||
.bg-2{height: 50%; background-color: #EAEFF3;}
|
||||
|
||||
/* 渐变背景 */
|
||||
/*.bg-1{
|
||||
background-size: 500%;
|
||||
background-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5);
|
||||
animation: bganimation 30s infinite;
|
||||
}
|
||||
@keyframes bganimation{
|
||||
0%{background-position: 0% 50%;}
|
||||
50%{background-position: 100% 50%;}
|
||||
100%{background-position: 0% 50%;}
|
||||
} */
|
||||
/* 背景 */
|
||||
.bg-1{background: #101C34;}
|
||||
.bg-2{background: #101C34;}
|
||||
/* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */
|
||||
|
||||
|
||||
/* 内容盒子 */
|
||||
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
|
||||
|
||||
/* 登录盒子 */
|
||||
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
|
||||
.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}
|
||||
.login-box{display: flex; align-items: center; text-align: center;}
|
||||
|
||||
/* 表单 */
|
||||
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
|
||||
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
|
||||
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
|
||||
|
||||
/* 输入框 */
|
||||
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
|
||||
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
|
||||
.s-input{font-size: 12px;}
|
||||
.s-input:focus{border-color: #409eff}
|
||||
|
||||
/* 登录按钮 */
|
||||
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
|
||||
.s-btn:hover{background-color: #50aEFF;}
|
||||
|
||||
/* 重置按钮 */
|
||||
.reset-box{text-align: left; font-size: 12px;}
|
||||
.reset-box a{text-decoration: none;}
|
||||
.reset-box a:hover{text-decoration: underline;}
|
||||
|
||||
/* loading框样式 */
|
||||
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
|
@ -0,0 +1,65 @@
|
||||
// sa
|
||||
var sa = {};
|
||||
|
||||
// 打开loading
|
||||
sa.loading = function(msg) {
|
||||
layer.closeAll(); // 开始前先把所有弹窗关了
|
||||
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });
|
||||
};
|
||||
|
||||
// 隐藏loading
|
||||
sa.hideLoading = function() {
|
||||
layer.closeAll();
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------- 登录事件 -----------------------------------
|
||||
|
||||
$('.login-btn').click(function(){
|
||||
sa.loading("正在登录...");
|
||||
// 开始登录
|
||||
setTimeout(function() {
|
||||
$.ajax({
|
||||
url: "doLogin",
|
||||
type: "post",
|
||||
data: {
|
||||
name: $('[name=name]').val(),
|
||||
pwd: $('[name=pwd]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
console.log('返回数据:', res);
|
||||
sa.hideLoading();
|
||||
if(res.code == 200) {
|
||||
layer.msg('登录成功', {anim: 0, icon: 6 });
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 800)
|
||||
} else {
|
||||
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
sa.hideLoading();
|
||||
if(xhr.status == 0){
|
||||
return layer.alert('无法连接到服务器,请检查网络');
|
||||
}
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
});
|
||||
|
||||
// 绑定回车事件
|
||||
$('[name=name],[name=pwd]').bind('keypress', function(event){
|
||||
if(event.keyCode == "13") {
|
||||
$('.login-btn').click();
|
||||
}
|
||||
});
|
||||
|
||||
// 输入框获取焦点
|
||||
$("[name=name]").focus();
|
||||
|
||||
// 打印信息
|
||||
var str = "This page is provided by Sa-Token, Please refer to: " + "http://sa-token.dev33.cn/";
|
||||
console.log(str);
|
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>Sa-SSO-Server 认证中心-登录</title>
|
||||
<meta charset="utf-8">
|
||||
<base th:href="@{/}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./sa-res/login.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="view-box">
|
||||
<div class="bg-1"></div>
|
||||
<div class="bg-2"></div>
|
||||
<div class="content-box">
|
||||
<div class="login-box">
|
||||
<div class="from-box">
|
||||
<h2 class="from-title">Sa-SSO-Server 认证中心</h2>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="name" placeholder="请输入账号" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<button class="s-input s-btn login-btn">登录</button>
|
||||
</div>
|
||||
<div class="from-item reset-box">
|
||||
<a href="javascript: location.reload();" >刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 版权 -->
|
||||
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
|
||||
This page is provided by Sa-Token-SSO
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="./sa-res/jquery.min.js"></script>
|
||||
<script src="./sa-res/layer/layer.js"></script>
|
||||
<script src="./sa-res/login.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -33,7 +33,12 @@
|
||||
- [集群、分布式](/senior/dcs)
|
||||
<!-- - [单点登录](/senior/sso) -->
|
||||
- [多账号验证](/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)
|
||||
|
@ -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 @@
|
||||
<!-- img点击放大 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script>
|
||||
|
||||
<!-- sidebar折叠 -->
|
||||
<!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/sidebar.min.css" />
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/docsify-sidebar-collapse.min.js"></script> -->
|
||||
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
|
||||
<!-- 搜索引擎自动提交 -->
|
||||
|
@ -3,36 +3,37 @@
|
||||
---
|
||||
|
||||
### 什么是单点登录?解决什么问题?
|
||||
举个场景:假设我们的系统被切割成N个部分:商城、论坛、直播、社交…… 如果用户每访问其中一个模块都要进行一次登录注册,那么用户将会疯掉,
|
||||
为了不让用户疯掉,我们急需一套机制将这N个系统的授权进行共享,让用户在其中一个系统登录之后,便可以畅通无阻的访问其它系统
|
||||
|
||||
举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉,
|
||||
为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统
|
||||
|
||||
单点登录——就是为了解决这个问题而生!
|
||||
|
||||
简而言之,单点登录可以做到:**`在多个系统中,用户只需登录一次,就可以访问所有系统。`**
|
||||
简而言之,单点登录可以做到:**`在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。`**
|
||||
|
||||
|
||||
### 架构选型
|
||||
对于单点登录,网上教程很多,但大多数讲述的都是CAS重定向机制,相对来讲,CAS模式较为复杂且并不是实现单点登录的唯一方式。
|
||||
对于单点登录,网上教程大多以CAS模式为主,其实对于不同的系统架构,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式:
|
||||
|
||||
对于不同的系统架构来讲,实现单点登录的步骤也大为不同,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本身的路由拦截特性,你可以非常轻松的定制化开发
|
||||
|
@ -1,12 +1,15 @@
|
||||
# SSO模式一 共享Cookie同步会话
|
||||
|
||||
如果我们的系统可以保证部署在同一个主域名之下,并且后端连接同一个Redis,那么便可以使用 `[共享Cookie同步会话]` 的方式做到单点登录
|
||||
如果我们的系统可以保证部署在同一个主域名之下,并且后端连接同一个Redis,那么便可以使用 **`[共享Cookie同步会话]`** 的方式做到单点登录
|
||||
|
||||
> Sa-Token整合同域单点登录非常简单,相比于正常的登录,你只需增加配置 `sa-token.cookie-domain=xxx.com` 指定一下Cookie写入时的父级域名即可 <br>
|
||||
> 整合示例在官方仓库的 `/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)
|
||||

|
||||
|
||||
``` js
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "登录成功: 10001",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
现在访问任意节点的登录接口:[http://s1.stp.com:8081/sso/doLogin](http://s1.stp.com:8081/sso/doLogin)
|
||||
|
||||

|
||||
|
||||
然后再次刷新上面三个测试接口,均可以得到以下结果:
|
||||
``` js
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "是否登录: true",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
测试完毕
|
||||
|
||||
|
||||
### 跨域模式下的解决方案
|
||||
### 5、跨域模式下的解决方案
|
||||
|
||||
如上,我们使用极其简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制:
|
||||
|
||||
@ -122,50 +118,6 @@ public class SSOController {
|
||||
|
||||
且往下看,[SSO模式二:URL重定向传播会话](/sso/sso-type2)
|
||||
|
||||
<!-- 根据前面的总结,单点登录的关键点在于我们如何完成多个系统之间的token共享,而`Cookie`并非实现此功能的唯一方案,既然浏览器对`Cookie`限制重重,我们何不干脆直接放弃`Cookie`,转投`LocalStorage`的怀抱?
|
||||
|
||||
思路:建立一个登录中心,在中心登录之后将token一次性下发到所有子系统中
|
||||
|
||||
参考以下步骤:
|
||||
``` js
|
||||
// 在主域名登录请求回调函数里执行以下方法
|
||||
|
||||
// 获取token
|
||||
var token = res.data.tokenValue;
|
||||
|
||||
// 创建子域的iframe, 用于传送数据
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.src = "http://s2.stp.com/xxx.html";
|
||||
iframe.style.display = 'none';
|
||||
document.body.append(iframe);
|
||||
|
||||
// 使用postMessage()发送数据到子系统
|
||||
setTimeout(function () {
|
||||
iframe.contentWindow.postMessage(token, "http://s2.stp.com");
|
||||
}, 2000);
|
||||
|
||||
// 销毁iframe
|
||||
setTimeout(function () {
|
||||
iframe.remove();
|
||||
}, 4000);
|
||||
|
||||
|
||||
// 在子系统里接受消息
|
||||
window.addEventListener('message', function (event) {
|
||||
console.log('收到消息', event.data);
|
||||
// 写入本地localStorage缓存中
|
||||
localStorage.setItem('satoken', event.data)
|
||||
}, false);
|
||||
|
||||
```
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
总结:此方式仍然限制较大,但巧在提供了一种简便的思路做到了跨域共享token,其实跨域模式下的单点登录标准解法还是cas流程,
|
||||
参考[单点登录的三种方式](https://www.cnblogs.com/yonghengzh/p/13712729.html) -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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 = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + lencodeURIComponent(location.href);\">登录</a></p>";
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + encodeURIComponent(location.href);\">登录</a></p>";
|
||||
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 {
|
||||
|
||||
| 配置方式 | 举例 | 安全性 | 建议 |
|
||||
| :-------- | :-------- | :-------- | :-------- |
|
||||
| 配置为* | `*` | <font color="#F00" >低</font> | <font color="#F00" >禁止在生产环境下使用</font> |
|
||||
| 配置为* | `*` | <font color="#F00" >低</font> | **<font color="#F00" >禁止在生产环境下使用</font>** |
|
||||
| 配置到域名 | `http://sa-sso-client1.com/*` | <font color="#F70" >中</font> | <font color="#F70" >不建议在生产环境下使用</font> |
|
||||
| 配置到详细地址| `http://sa-sso-client1.com:9001/ssoLogin` | <font color="#080" >高</font> | <font color="#080" >可以在生产环境下使用</font> |
|
||||
|
||||
@ -389,6 +387,12 @@ Token作为长时间有效的会话凭证,在任何时候都不应该直接在
|
||||
|
||||
|
||||
|
||||
### 6、跨Redis的单点登录
|
||||
以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享Redis来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录?
|
||||
|
||||
这就要采用模式三了,且往下看:[Http请求获取会话](/sso/sso-type3)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,2 +1,244 @@
|
||||
# SSO模式三 SSO认证中心开放接口校验Ticket
|
||||
# SSO模式三 Http请求获取会话
|
||||
|
||||
如果既无法做到前端同域,也无法做到后端同Redis,那么可以使用模式三完成单点登录
|
||||
|
||||
> 阅读本篇之前请务必先熟读SSO模式二!因为模式三仅仅属于模式二的一个特殊场景,熟读模式二有助于您快速理解本章内容
|
||||
|
||||
|
||||
### 0、问题分析
|
||||
我们先来分析一下,当后端不使用共享Redis时,会对架构发生哪些影响
|
||||
|
||||
1. Client端 无法直连 Redis 校验 ticket,取出账号id
|
||||
2. Client端 无法与 Server端 共用一套会话,需要自行维护子会话
|
||||
3. 由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销
|
||||
|
||||
所以模式三的主要目标:也就是在 模式二的基础上 解决上述 三个难题
|
||||
|
||||
> 模式三的Demo示例地址:<br/>
|
||||
> 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) <br/>
|
||||
> 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) <br/>
|
||||
> 如遇难点可参考示例
|
||||
|
||||
|
||||
### 1、SSO-Server认证中心开放ticket校验接口
|
||||
既然Client端无法直连Redis校验ticket,那就在Server端开放ticket校验接口,然后Client端通过http请求获取数据
|
||||
|
||||
##### 1.1、添加依赖
|
||||
首先在Server端和Client端均添加以下依赖(如果不需要单点注销功能则Server端可不引入)
|
||||
``` xml
|
||||
<!-- Http请求工具 -->
|
||||
<dependency>
|
||||
<groupId>com.ejlchina</groupId>
|
||||
<artifactId>okhttps</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
```
|
||||
> 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 = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + encodeURIComponent(location.href);\">登录</a>" +
|
||||
" <a href='/ssoLogout' target='_blank'>注销</a></p>";
|
||||
return str;
|
||||
}
|
||||
```
|
||||
PS:相比于模式二,增加了单点注销的按钮
|
||||
|
||||
|
||||
##### 2.4 启动测试
|
||||
启动SSO-Server、SSO-Client,访问测试:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/),
|
||||
我们主要的测试点在于 `单点注销`,正常登陆即可
|
||||
|
||||

|
||||
|
||||
点击 **`[注销]`** 按钮,即可单点注销成功
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用Ajax调用接口即可做到无刷单点登录退出
|
||||
|
||||
例如我们使用 [APIPost接口测试工具](https://www.apipost.cn/) 可以做到同样的效果:
|
||||
|
||||

|
||||
|
||||
测试完毕!
|
||||
|
||||
|
||||
|
||||
|
||||
### 3、后记
|
||||
当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享
|
||||
|
||||
当我们理解这一点之后,三种模式的工作原理也浮出水面:
|
||||
|
||||
- 模式一:采用共享Cookie来做到前端Token的共享,从而达到后端的Session会话共享
|
||||
- 模式二:采用URL重定向,以ticket码为授权中介,做到多个系统间的会话传播
|
||||
- 模式三:采用Http请求主动查询会话,做到Client端与Server端的会话同步
|
||||
|
||||
|
||||
|
||||
|
||||
|