diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java index c4abc0fa..14722135 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java @@ -49,6 +49,9 @@ public class SaTokenConfig { /** 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) */ private Boolean autoRenew = true; + /** 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 */ + private String cookieDomain; + /** 是否在初始化配置时打印版本字符画 */ private Boolean isV = true; @@ -225,7 +228,21 @@ public class SaTokenConfig { public void setAutoRenew(Boolean autoRenew) { this.autoRenew = autoRenew; } - + + /** + * @return 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 + */ + public String getCookieDomain() { + return cookieDomain; + } + + /** + * @param cookieDomain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 + */ + public void setCookieDomain(String cookieDomain) { + this.cookieDomain = cookieDomain; + } + /** * @return 是否在初始化配置时打印版本字符画 */ @@ -240,7 +257,7 @@ public class SaTokenConfig { this.isV = isV; } - + /** * toString */ @@ -250,9 +267,10 @@ public class SaTokenConfig { + ", allowConcurrentLogin=" + allowConcurrentLogin + ", isShare=" + isShare + ", isReadBody=" + isReadBody + ", isReadHead=" + isReadHead + ", isReadCookie=" + isReadCookie + ", tokenStyle=" + tokenStyle + ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin=" - + tokenSessionCheckLogin + ", autoRenew=" + autoRenew + ", isV=" + isV + "]"; + + tokenSessionCheckLogin + ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain + ", isV=" + + isV + "]"; } - + diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java b/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java index a68fe456..e06774e0 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java @@ -28,9 +28,10 @@ public interface SaTokenCookie { * @param name Cookie名称 * @param value Cookie值 * @param path Cookie路径 + * @param domain Cookie的作用域 * @param timeout 过期时间 (秒) */ - public void addCookie(HttpServletResponse response, String name, String value, String path, int timeout); + public void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout); /** * 删除Cookie diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java index 4240ab6a..fda0176a 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java @@ -24,8 +24,8 @@ public class SaTokenCookieDefaultImpl implements SaTokenCookie { * 添加cookie */ @Override - public void addCookie(HttpServletResponse response, String name, String value, String path, int timeout) { - SaTokenCookieUtil.addCookie(response, name, value, path, timeout); + public void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout) { + SaTokenCookieUtil.addCookie(response, name, value, path, domain, timeout); } /** diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java index 51dbaf5e..6f6e6594 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java @@ -4,6 +4,8 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import cn.dev33.satoken.util.SaTokenInsideUtil; + /** * Cookie操作工具类 * @@ -37,13 +39,17 @@ public class SaTokenCookieUtil { * @param name Cookie名称 * @param value Cookie值 * @param path Cookie写入路径 + * @param domain Cookie的作用域 * @param timeout Cookie有效期 (秒) */ - public static void addCookie(HttpServletResponse response, String name, String value, String path, int timeout) { + public static void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout) { Cookie cookie = new Cookie(name, value); - if (path == null) { + if(SaTokenInsideUtil.isEmpty(path) == false) { path = "/"; } + if(SaTokenInsideUtil.isEmpty(domain) == false) { + cookie.setDomain(domain); + } cookie.setPath(path); cookie.setMaxAge(timeout); response.addCookie(cookie); @@ -61,7 +67,7 @@ public class SaTokenCookieUtil { if (cookies != null) { for (Cookie cookie : cookies) { if (cookie != null && (name).equals(cookie.getName())) { - addCookie(response, name, null, null, 0); + addCookie(response, name, null, null, null, 0); return; } } @@ -82,7 +88,7 @@ public class SaTokenCookieUtil { if (cookies != null) { for (Cookie cookie : cookies) { if (cookie != null && (name).equals(cookie.getName())) { - addCookie(response, name, value, cookie.getPath(), cookie.getMaxAge()); + addCookie(response, name, value, cookie.getPath(), cookie.getDomain(), cookie.getMaxAge()); return; } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java index 43f29868..43f53bc9 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java @@ -210,7 +210,8 @@ public class StpLogic { setLastActivityToNow(tokenValue); // cookie注入 if(config.getIsReadCookie() == true){ - SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)config.getTimeout()); + SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, + "/", config.getCookieDomain(), (int)config.getTimeout()); } } @@ -558,7 +559,8 @@ public class StpLogic { setLastActivityToNow(tokenValue); // cookie注入 if(getConfig().getIsReadCookie() == true){ - SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)getConfig().getTimeout()); + SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, + "/", getConfig().getCookieDomain(), (int)getConfig().getTimeout()); } } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java index 909f84cf..17052ac2 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java @@ -41,6 +41,13 @@ public class SaTokenInsideUtil { return sb.toString(); } + /** + * 指定字符串是否为null或者空字符串 + */ + public static boolean isEmpty(String str) { + return str == null || "".equals(str); + } + /** * 以当前时间戳和随机int数字拼接一个随机字符串 * diff --git a/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java b/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java index adf1eb96..dad5b509 100644 --- a/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java +++ b/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java @@ -130,7 +130,7 @@ public class SaTokenJwtUtil { String tokenValue = createTokenValue(loginId); request.setAttribute(getKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里 if(config.getIsReadCookie() == true){ // cookie注入 - SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)config.getTimeout()); + SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", config.getCookieDomain(), (int)config.getTimeout()); } } diff --git a/sa-token-demo-springboot/src/main/java/com/pj/CorsFilter.java b/sa-token-demo-springboot/src/main/java/com/pj/CorsFilter.java new file mode 100644 index 00000000..4a0cf1f6 --- /dev/null +++ b/sa-token-demo-springboot/src/main/java/com/pj/CorsFilter.java @@ -0,0 +1,67 @@ +package com.pj; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.stereotype.Component; + +/** + * 跨域过滤器 + * @author kong + */ +@Component +public class CorsFilter implements Filter { + + static final String OPTIONS = "OPTIONS"; + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + // 获得客户端domain + String origin = request.getHeader("Origin"); + if (origin == null) { + origin = request.getHeader("Referer"); + } + // 允许指定域访问跨域资源 + response.setHeader("Access-Control-Allow-Origin", origin); + // 允许客户端携带跨域cookie,此时origin值不能为“*”,只能为指定单一域名 + response.setHeader("Access-Control-Allow-Credentials", "true"); + // 允许所有请求方式 + response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); + // 有效时间 + response.setHeader("Access-Control-Max-Age", "3600"); + // 允许的header参数 + response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken"); + // 允许的header参数 +// response.setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,直接返回 + if (OPTIONS.equals(request.getMethod())) { + System.out.println("=======================浏览器发来了OPTIONS预检请求=========="); + response.getWriter().print(""); + return; + } + + // System.out.println("*********************************过滤器被使用**************************2233"); + chain.doFilter(req, res); + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void destroy() { + } + +} diff --git a/sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java b/sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java new file mode 100644 index 00000000..5a9345ce --- /dev/null +++ b/sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java @@ -0,0 +1,35 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.stp.StpUtil; + +/** + * 测试: 同域单点登录 + * @author kong + */ +@RestController +@RequestMapping("/sso/") +public class SSOController { + + // 测试:进行登录 + @RequestMapping("doLogin") + public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) { + System.out.println("---------------- 进行登录 "); + StpUtil.setLoginId(id); + return AjaxJson.getSuccess("登录成功: " + id); + } + + // 测试:是否登录 + @RequestMapping("isLogin") + public AjaxJson isLogin() { + System.out.println("---------------- 是否登录 "); + boolean isLogin = StpUtil.isLogin(); + return AjaxJson.getSuccess("是否登录: " + isLogin); + } + +} diff --git a/sa-token-demo-springboot/src/main/resources/application.yml b/sa-token-demo-springboot/src/main/resources/application.yml index 0ee1952f..b0415b9a 100644 --- a/sa-token-demo-springboot/src/main/resources/application.yml +++ b/sa-token-demo-springboot/src/main/resources/application.yml @@ -17,6 +17,8 @@ spring: is-share: true # token风格 token-style: uuid + # 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie的场景 +# cookie-domain: stp.com # redis配置 diff --git a/sa-token-doc/doc/_sidebar.md b/sa-token-doc/doc/_sidebar.md index 54ac1bb3..39e4553c 100644 --- a/sa-token-doc/doc/_sidebar.md +++ b/sa-token-doc/doc/_sidebar.md @@ -21,6 +21,10 @@ - [框架配置](/use/config) - [会话治理](/use/search-session) +- **进阶** + - [集群、分布式](/senior/dcs) + - [单点登录](/senior/sso) + - **其它** - [常见问题](/more/common-questions) - [友情链接](/more/link) diff --git a/sa-token-doc/doc/senior/dcs.md b/sa-token-doc/doc/senior/dcs.md new file mode 100644 index 00000000..c0000e2e --- /dev/null +++ b/sa-token-doc/doc/senior/dcs.md @@ -0,0 +1,13 @@ +# 集群、分布式 + +集群模式下, + + + + + + + + + + diff --git a/sa-token-doc/doc/senior/sso.md b/sa-token-doc/doc/senior/sso.md new file mode 100644 index 00000000..97cecf84 --- /dev/null +++ b/sa-token-doc/doc/senior/sso.md @@ -0,0 +1,188 @@ +# 单点登录 +--- + +### 什么是单点登录?解决什么问题? + +举个场景:假设你的系统被切割成N个部分:商城、论坛、直播、社交、视频…… 并且这些模块都部署在不同的服务器下, +如果用户每访问其中一个模块都要进行一次登录注册,那么用户将会疯掉 + +为了不让用户疯掉,我们急需一套机制将这个N个模块的授权进行共享,使得用户在其中一个模块登录授权之后,便可以畅通无阻的访问其它模块 + +单点登录——就是为了解决这个问题而生 + +简单来讲就是,单点登录可以做到:在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 + +下面我们将详细介绍“同域Cookie共享"模式下的单点登录(cas机制将会在以后的章节中进行整合) + + +### 解决思路? + +首先我们分析一下多个系统之间为什么无法同步登录状态? +1. 前端的`token`无法在多个系统下共享 +2. 后台的`Session`无法在多个服务间共享 + +所以单点登录第一招,就是对症下药: +1. 使用`共享Cookie`来解决`token共享`问题 +2. 使用`Redis`来解决`Session共享`问题 + +在前面的章节我们已经了解了`sa-token`整合`Redis`的步骤,现在我们来讲一下如何在多个域名下共享Cookie。 + +首先我们需要明确一点:根据`CORS策略`,在A域名下写入的Cookie,在B域名下是无法读取的,浏览器对跨域访问有着非常严格的限制
+ +既然如此,我们如何做到让`Cookie`在多个域名下共享?其实关于跨域Cookie访问,浏览器还有一条规则,那就是同域名下的二级域名是可以共享Cookie的。 +举个例子:只要我们将`Cookie`写入父级域名`stp.com`下,在其任意一个二级域名比如`s1.stp.com`都是可以共享访问的,这就为我们需要的`token共享`提供了必要的前提 + +OK,所有理论就绪,下面开始实战 + + +### 集成步骤 + +sa-token整合同域下的单点登录非常简单,相比于正常的登录,你只需要在配置文件中增加配置 `sa-token.cookie-domain=xxx.com` 来指定一下Cookie写入时指定的父级域名即可,详细步骤示例如下: + +#### 1. 准备工作 +首先修改hosts文件(`C:\WINDOWS\system32\drivers\etc\hosts`),添加以下IP映射,方便我们进行测试: +``` text +127.0.0.1 s1.stp.com +127.0.0.1 s2.stp.com +127.0.0.1 s3.stp.com +``` + +#### 2. 指定Cookie的作用域 +常规情况下,在`s1.stp.com`域名访问服务器,其Cookie也只能写入到`s1.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要在配置文件中新增配置: +``` yml +spring: + sa-token: + # 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie + cookie-domain: stp.com +``` + +#### 3. 新增测试Controller +新建`SSOController.java`控制器,写入代码: +``` java +/** + * 测试: 同域单点登录 + * @author kong + */ +@RestController +@RequestMapping("/sso/") +public class SSOController { + + // 测试:进行登录 + @RequestMapping("doLogin") + public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) { + System.out.println("---------------- 进行登录 "); + StpUtil.setLoginId(id); + return AjaxJson.getSuccess("登录成功: " + id); + } + + // 测试:是否登录 + @RequestMapping("isLogin") + public AjaxJson isLogin() { + System.out.println("---------------- 是否登录 "); + boolean isLogin = StpUtil.isLogin(); + return AjaxJson.getSuccess("是否登录: " + isLogin); + } + +} +``` + +#### 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 +} +``` + +然后再次刷新上面三个测试接口,均可以得到以下结果: +``` js +{ + "code": 200, + "msg": "是否登录: true", + "data": null +} +``` + +测试完毕 + + +### 跨域模式下的解决方案 + +如上,我们使用极其简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制: +- 所有子系统的域名,必须同属一个父级域名 + +如果我们我们的子系统在完全不同的域名下,我们又该怎么完成单点登录功能呢? + +根据前面的总结,单点登录的关键点在于我们如何完成多个系统之间的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); + +``` + + +
+ +总结:此方式仍然限制较大,但巧在提供了一种简便的思路做到了跨域共享token,其实跨域模式下的单点登录标准解法还是cas流程, +参考[单点登录的三种方式](https://www.cnblogs.com/yonghengzh/p/13712729.html) + + + + + + + + + + + + +