提交 a11ad64d 编写于 作者: shengzhang_'s avatar shengzhang_

完成同域模式下的单点登录

上级 af0b2285
......@@ -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 + "]";
}
......
......@@ -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
......
......@@ -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);
}
/**
......
......@@ -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;
}
}
......
......@@ -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());
}
}
}
......
......@@ -41,6 +41,13 @@ public class SaTokenInsideUtil {
return sb.toString();
}
/**
* 指定字符串是否为null或者空字符串
*/
public static boolean isEmpty(String str) {
return str == null || "".equals(str);
}
/**
* 以当前时间戳和随机int数字拼接一个随机字符串
*
......
......@@ -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());
}
}
......
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() {
}
}
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);
}
}
......@@ -17,6 +17,8 @@ spring:
is-share: true
# token风格
token-style: uuid
# 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie的场景
# cookie-domain: stp.com
# redis配置
......
......@@ -21,6 +21,10 @@
- [框架配置](/use/config)
- [会话治理](/use/search-session)
- **进阶**
- [集群、分布式](/senior/dcs)
- [单点登录](/senior/sso)
- **其它**
- [常见问题](/more/common-questions)
- [友情链接](/more/link)
......
# 集群、分布式
集群模式下,
# 单点登录
---
### 什么是单点登录?解决什么问题?
举个场景:假设你的系统被切割成N个部分:商城、论坛、直播、社交、视频…… 并且这些模块都部署在不同的服务器下,
如果用户每访问其中一个模块都要进行一次登录注册,那么用户将会疯掉
为了不让用户疯掉,我们急需一套机制将这个N个模块的授权进行共享,使得用户在其中一个模块登录授权之后,便可以畅通无阻的访问其它模块
单点登录——就是为了解决这个问题而生
简单来讲就是,单点登录可以做到:在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
下面我们将详细介绍“同域Cookie共享"模式下的单点登录(cas机制将会在以后的章节中进行整合)
### 解决思路?
首先我们分析一下多个系统之间为什么无法同步登录状态?
1. 前端的`token`无法在多个系统下共享
2. 后台的`Session`无法在多个服务间共享
所以单点登录第一招,就是对症下药:
1. 使用`共享Cookie`来解决`token共享`问题
2. 使用`Redis`来解决`Session共享`问题
在前面的章节我们已经了解了`sa-token`整合`Redis`的步骤,现在我们来讲一下如何在多个域名下共享Cookie。
首先我们需要明确一点:根据`CORS策略`,在A域名下写入的Cookie,在B域名下是无法读取的,浏览器对跨域访问有着非常严格的限制 <br>
既然如此,我们如何做到让`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);
```
<br>
总结:此方式仍然限制较大,但巧在提供了一种简便的思路做到了跨域共享token,其实跨域模式下的单点登录标准解法还是cas流程,
参考[单点登录的三种方式](https://www.cnblogs.com/yonghengzh/p/13712729.html)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册