增加Cookie模式的secure、httpOnly、sameSite等配置

This commit is contained in:
click33 2021-10-05 23:51:04 +08:00
parent cf6632df79
commit 643118177a
12 changed files with 541 additions and 172 deletions

View File

@ -0,0 +1,122 @@
package cn.dev33.satoken.config;
/**
* Sa-Token Cookie写入 相关配置
* @author kong
*
*/
public class SaCookieConfig {
/**
* 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
*/
private String domain;
/**
* 路径
*/
private String path;
/**
* 是否只在 https 协议下有效
*/
private Boolean secure = false;
/**
* 是否禁止 js 操作 Cookie
*/
private Boolean httpOnly = false;
/**
* 第三方限制级别Strict=完全禁止Lax=部分允许None=不限制
*/
private String sameSite;
/**
* @return 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
*/
public String getDomain() {
return domain;
}
/**
* @param domain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
* @return 对象自身
*/
public SaCookieConfig setDomain(String domain) {
this.domain = domain;
return this;
}
/**
* @return 路径
*/
public String getPath() {
return path;
}
/**
* @param path 路径
* @return 对象自身
*/
public SaCookieConfig setPath(String path) {
this.path = path;
return this;
}
/**
* @return 是否只在 https 协议下有效
*/
public Boolean getSecure() {
return secure;
}
/**
* @param secure 是否只在 https 协议下有效
* @return 对象自身
*/
public SaCookieConfig setSecure(Boolean secure) {
this.secure = secure;
return this;
}
/**
* @return 是否禁止 js 操作 Cookie
*/
public Boolean getHttpOnly() {
return httpOnly;
}
/**
* @param httpOnly 是否禁止 js 操作 Cookie
* @return 对象自身
*/
public SaCookieConfig setHttpOnly(Boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
/**
* @return 第三方限制级别Strict=完全禁止Lax=部分允许None=不限制
*/
public String getSameSite() {
return sameSite;
}
/**
* @param sameSite 第三方限制级别Strict=完全禁止Lax=部分允许None=不限制
* @return 对象自身
*/
public SaCookieConfig setSameSite(String sameSite) {
this.sameSite = sameSite;
return this;
}
// toString
@Override
public String toString() {
return "SaCookieConfig [domain=" + domain + ", path=" + path + ", secure=" + secure + ", httpOnly=" + httpOnly
+ ", sameSite=" + sameSite + "]";
}
}

View File

@ -41,12 +41,6 @@ public class SaTokenConfig implements Serializable {
/** 是否尝试从cookie里读取token */
private Boolean isReadCookie = true;
/** 使用Cookie时,是否为HttpOnly */
private Boolean isCookieHttpOnly = false;
/** 使用Cookie时,是否为Secure */
private Boolean isCookieSecure = false;
/** token风格(默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik) */
private String tokenStyle = "uuid";
@ -59,9 +53,6 @@ public class SaTokenConfig implements Serializable {
/** 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) */
private Boolean autoRenew = true;
/** 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 */
private String cookieDomain;
/** token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */
private String tokenPrefix;
@ -90,6 +81,11 @@ public class SaTokenConfig implements Serializable {
private String currDomain;
/**
* Cookie配置对象
*/
public SaCookieConfig cookie = new SaCookieConfig();
/**
* SSO单点登录配置对象
*/
@ -226,38 +222,6 @@ public class SaTokenConfig implements Serializable {
return this;
}
/**
* @return 使用Cookie时,是否为HttpOnly
*/
public Boolean getIsCookieHttpOnly() {
return isCookieHttpOnly;
}
/**
* @param isCookieHttpOnly 使用Cookie时,是否为HttpOnly
* @return 对象自身
*/
public SaTokenConfig setIsCookieHttpOnly(Boolean isCookieHttpOnly) {
this.isCookieHttpOnly = isCookieHttpOnly;
return this;
}
/**
* @return 使用Cookie时,是否为Secure
*/
public Boolean getIsCookieSecure() {
return isCookieSecure;
}
/**
* @param isCookieSecure 使用Cookie时,是否为Secure
* @return 对象自身
*/
public SaTokenConfig setIsCookieSecure(Boolean isCookieSecure) {
this.isCookieSecure = isCookieSecure;
return this;
}
/**
* @return token风格(默认可取值uuidsimple-uuidrandom-32random-64random-128tik)
*/
@ -324,22 +288,6 @@ public class SaTokenConfig implements Serializable {
return this;
}
/**
* @return 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
*/
public String getCookieDomain() {
return cookieDomain;
}
/**
* @param cookieDomain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
* @return 对象自身
*/
public SaTokenConfig setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
return this;
}
/**
* @return token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
*/
@ -461,23 +409,54 @@ public class SaTokenConfig implements Serializable {
/**
* @param sso SSO单点登录配置对象
* @return 对象自身
*/
public void setSso(SaSsoConfig sso) {
public SaTokenConfig setSso(SaSsoConfig sso) {
this.sso = sso;
return this;
}
/**
* @return Cookie 全局配置对象
*/
public SaCookieConfig getCookie() {
return cookie;
}
/**
* @param cookie Cookie 全局配置对象
* @return 对象自身
*/
public SaTokenConfig setCookie(SaCookieConfig cookie) {
this.cookie = cookie;
return this;
}
@Override
public String toString() {
return "SaTokenConfig [tokenName=" + tokenName + ", timeout=" + timeout + ", activityTimeout=" + activityTimeout
+ ", isConcurrent=" + isConcurrent + ", isShare=" + isShare + ", isReadBody=" + isReadBody
+ ", isReadHead=" + isReadHead + ", isReadCookie=" + isReadCookie
+ ", isCookieHttpOnly=" + isCookieHttpOnly + ", isCookieSecure=" + isCookieSecure
return "SaTokenConfig ["
+ "tokenName=" + tokenName
+ ", timeout=" + timeout
+ ", activityTimeout=" + activityTimeout
+ ", isConcurrent=" + isConcurrent
+ ", isShare=" + isShare
+ ", isReadBody=" + isReadBody
+ ", isReadHead=" + isReadHead
+ ", isReadCookie=" + isReadCookie
+ ", tokenStyle=" + tokenStyle
+ ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
+ ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain + ", tokenPrefix=" + tokenPrefix
+ ", isPrint=" + isPrint + ", isLog=" + isLog + ", jwtSecretKey=" + jwtSecretKey + ", idTokenTimeout="
+ idTokenTimeout + ", basic=" + basic + ", currDomain=" + currDomain + ", sso=" + sso + "]";
+ ", dataRefreshPeriod=" + dataRefreshPeriod
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
+ ", autoRenew=" + autoRenew
+ ", tokenPrefix=" + tokenPrefix
+ ", isPrint=" + isPrint
+ ", isLog=" + isLog
+ ", jwtSecretKey=" + jwtSecretKey
+ ", idTokenTimeout=" + idTokenTimeout
+ ", basic=" + basic
+ ", currDomain=" + currDomain
+ ", sso=" + sso
+ ", cookie=" + cookie
+ "]";
}
@ -503,4 +482,25 @@ public class SaTokenConfig implements Serializable {
return this;
}
/**
* <h1> 本函数设计已过时未来版本可能移除此函数请及时更换为 getCookie().getDomain() 使用方式保持不变 </h1>
* @return 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
*/
@Deprecated
public String getCookieDomain() {
return getCookie().getDomain();
}
/**
* <h1> 本函数设计已过时未来版本可能移除此函数请及时更换为 getCookie().setDomain() 使用方式保持不变 </h1>
* @param cookieDomain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
* @return 对象自身
*/
@Deprecated
public SaTokenConfig setCookieDomain(String cookieDomain) {
this.getCookie().setDomain(cookieDomain);
return this;
}
}

View File

@ -0,0 +1,275 @@
package cn.dev33.satoken.context.model;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Cookie Model
* @author kong
*
*/
public class SaCookie {
/**
* 写入响应头时使用的key
*/
public static final String HEADER_NAME = "Set-Cookie";
/**
* 名称
*/
private String name;
/**
*
*/
private String value;
/**
* 有效时长 单位-1代表为临时Cookie 浏览器关闭后自动删除
*/
private int maxAge = -1;
/**
*
*/
private String domain;
/**
* 路径
*/
private String path;
/**
* 是否只在 https 协议下有效
*/
private Boolean secure = false;
/**
* 是否禁止 js 操作 Cookie
*/
private Boolean httpOnly = false;
/**
* 第三方限制级别Strict=完全禁止Lax=部分允许None=不限制
*/
private String sameSite;
/**
* 构造一个
*/
public SaCookie() {
}
/**
* 构造一个
* @param name 名字
* @param value
*/
public SaCookie(String name, String value) {
this.name = name;
this.value = value;
}
/**
* @return 名称
*/
public String getName() {
return name;
}
/**
* @param name 名称
*/
public SaCookie setName(String name) {
this.name = name;
return this;
}
/**
* @return
*/
public String getValue() {
return value;
}
/**
* @param value
* @return 对象自身
*/
public SaCookie setValue(String value) {
this.value = value;
return this;
}
/**
* @return 有效时长 单位-1代表为临时Cookie 浏览器关闭后自动删除
*/
public int getMaxAge() {
return maxAge;
}
/**
* @param maxAge 有效时长 单位-1代表为临时Cookie 浏览器关闭后自动删除
* @return 对象自身
*/
public SaCookie setMaxAge(int maxAge) {
this.maxAge = maxAge;
return this;
}
/**
* @return
*/
public String getDomain() {
return domain;
}
/**
* @param domain
* @return 对象自身
*/
public SaCookie setDomain(String domain) {
this.domain = domain;
return this;
}
/**
* @return 路径
*/
public String getPath() {
return path;
}
/**
* @param path 路径
* @return 对象自身
*/
public SaCookie setPath(String path) {
this.path = path;
return this;
}
/**
* @return 是否只在 https 协议下有效
*/
public Boolean getSecure() {
return secure;
}
/**
* @param secure 是否只在 https 协议下有效
* @return 对象自身
*/
public SaCookie setSecure(Boolean secure) {
this.secure = secure;
return this;
}
/**
* @return 是否禁止 js 操作 Cookie
*/
public Boolean getHttpOnly() {
return httpOnly;
}
/**
* @param httpOnly 是否禁止 js 操作 Cookie
* @return 对象自身
*/
public SaCookie setHttpOnly(Boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
/**
* @return 第三方限制级别Strict=完全禁止Lax=部分允许None=不限制
*/
public String getSameSite() {
return sameSite;
}
/**
* @param sameSite 第三方限制级别Strict=完全禁止Lax=部分允许None=不限制
* @return 对象自身
*/
public SaCookie setSameSite(String sameSite) {
this.sameSite = sameSite;
return this;
}
// toString
@Override
public String toString() {
return "SaCookie [name=" + name + ", value=" + value + ", maxAge=" + maxAge + ", domain=" + domain + ", path=" + path
+ ", secure=" + secure + ", httpOnly=" + httpOnly + ", sameSite="
+ sameSite + "]";
}
/**
* 构建一下
*/
public void builde() {
if(path == null) {
path = "/";
}
}
/**
* 转换为响应头 Set-Cookie 参数需要的值
* @return /
*/
public String toHeaderValue() {
this.builde();
if(SaFoxUtil.isEmpty(name)) {
throw new SaTokenException("name不能为空");
}
if(value != null && value.indexOf(";") > -1) {
throw new SaTokenException("无效Value" + value);
}
// Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax
StringBuffer sb = new StringBuffer();
sb.append(name + "=" + value);
if(maxAge >= 0) {
sb.append("; Max-Age=" + maxAge);
String expires;
if(maxAge == 0) {
expires = Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME);
} else {
expires = OffsetDateTime.now().plusSeconds(maxAge).format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
sb.append("; Expires=" + expires);
}
if(SaFoxUtil.isEmpty(domain) == false) {
sb.append("; Domain=" + domain);
}
if(SaFoxUtil.isEmpty(path) == false) {
sb.append("; Path=" + path);
}
if(secure) {
sb.append("; Secure");
}
if(httpOnly) {
sb.append("; HttpOnly");
}
if(SaFoxUtil.isEmpty(sameSite) == false) {
sb.append("; sameSite=" + sameSite);
}
return sb.toString();
}
}

View File

@ -17,7 +17,9 @@ public interface SaResponse {
* 删除指定Cookie
* @param name Cookie名称
*/
public void deleteCookie(String name);
public default void deleteCookie(String name) {
addCookie(name, null, null, null, 0);
}
/**
* 写入指定Cookie
@ -28,21 +30,17 @@ public interface SaResponse {
* @param timeout 过期时间
*/
public default void addCookie(String name, String value, String path, String domain, int timeout) {
this.addCookie(name, value, path, domain, timeout, false, false);
this.addCookie(new SaCookie(name, value).setPath(path).setDomain(domain).setMaxAge(timeout));
}
/**
* 写入指定Cookie
* @param name Cookie名称
* @param value Cookie值
* @param path Cookie路径
* @param domain Cookie的作用域
* @param timeout 过期时间
* @param isHttpOnly 是否为HttpOnly
* @param isSecure 是否为Secure
* @param cookie Cookie-Model
*/
public void addCookie(String name, String value, String path, String domain, int timeout, boolean isHttpOnly, boolean isSecure);
public default void addCookie(SaCookie cookie) {
this.addHeader(SaCookie.HEADER_NAME, cookie.toHeaderValue());
}
/**
* 设置响应状态码
* @param sc 响应状态码
@ -57,6 +55,14 @@ public interface SaResponse {
* @return 对象自身
*/
public SaResponse setHeader(String name, String value);
/**
* 在响应头里添加一个值
* @param name 名字
* @param value
* @return 对象自身
*/
public SaResponse addHeader(String name, String value);
/**
* 在响应头写入 [Server] 服务器名称

View File

@ -12,10 +12,11 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaCheckSafe;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.config.SaCookieConfig;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaCookie;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.context.model.SaStorage;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.exception.DisableLoginException;
@ -110,13 +111,31 @@ public class StpLogic {
storage.set(splicingKeyJustCreatedSave(), tokenValue);
}
// 2. token保存到[Cookie]
// 2. Token 保存到 [Cookie]
if (config.getIsReadCookie()) {
SaResponse response = SaHolder.getResponse();
response.addCookie(getTokenName(), tokenValue, "/",
config.getCookieDomain(), cookieTimeout, config.getIsCookieHttpOnly(), config.getIsCookieSecure());
setTokenValueToCookie(tokenValue, cookieTimeout);
}
}
/**
* Token 保存到 [Cookie]
* @param tokenValue token值
* @param cookieTimeout Cookie存活时间()
*/
public void setTokenValueToCookie(String tokenValue, int cookieTimeout){
SaCookieConfig cfg = getConfig().getCookie();
SaCookie cookie = new SaCookie()
.setName(getTokenName())
.setValue(tokenValue)
.setMaxAge(cookieTimeout)
.setDomain(cfg.getDomain())
.setPath(cfg.getPath())
.setSecure(cfg.getSecure())
.setHttpOnly(cfg.getHttpOnly())
.setSameSite(cfg.getSameSite())
;
SaHolder.getResponse().addCookie(cookie);
}
/**
* 获取当前TokenValue

View File

@ -133,7 +133,7 @@ public class SaTokenJwtUtil {
String tokenValue = createTokenValue(loginId);
storage.set(splicingKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里
if(config.getIsReadCookie() == true){ // cookie注入
SaManager.getSaTokenContext().getResponse().addCookie(getTokenName(), tokenValue, "/", config.getCookieDomain(), (int)config.getTimeout());
SaManager.getSaTokenContext().getResponse().addCookie(getTokenName(), tokenValue, "/", config.getCookie().getDomain(), (int)config.getTimeout());
}
}

View File

@ -37,7 +37,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册注解拦截器
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**").excludePathPatterns("");
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
/**

View File

@ -239,7 +239,7 @@ public class TestController {
// 测试 浏览器访问 http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
System.out.println("进来了");
System.out.println("------------进来了");
return AjaxJson.getSuccess();
}
@ -249,6 +249,4 @@ public class TestController {
return AjaxJson.getSuccess();
}
}

View File

@ -18,7 +18,7 @@ sa-token:
token-style: uuid
# 是否输出操作日志
is-log: false
spring:
# redis配置
redis:

View File

@ -3,12 +3,9 @@ package cn.dev33.satoken.reactor.model;
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseCookie.ResponseCookieBuilder;
import org.springframework.http.server.reactive.ServerHttpResponse;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Response for Reactor
@ -38,43 +35,6 @@ public class SaResponseForReactor implements SaResponse {
return response;
}
/**
* 删除指定Cookie
*/
@Override
public void deleteCookie(String name) {
addCookie(name, null, null, null, 0, false, false);
}
/**
* 写入指定Cookie
*/
@Override
public void addCookie(String name, String value, String path, String domain, int timeout, boolean isHttpOnly, boolean isSecure) {
// 构建CookieBuilder
ResponseCookieBuilder builder = ResponseCookie.from(name, value)
.domain(domain)
.path(path)
.maxAge(timeout)
.httpOnly(isHttpOnly)
.secure(isSecure)
;
// set path
if(SaFoxUtil.isEmpty(path) == true) {
path = "/";
}
builder.path(path);
// set domain
if(SaFoxUtil.isEmpty(domain) == false) {
builder.domain(domain);
}
// 写入Cookie
response.addCookie(builder.build());
}
/**
* 设置响应状态码
*/
@ -93,6 +53,17 @@ public class SaResponseForReactor implements SaResponse {
return this;
}
/**
* 在响应头里添加一个值
* @param name 名字
* @param value
* @return 对象自身
*/
public SaResponse addHeader(String name, String value) {
response.getHeaders().add(name, value);
return this;
}
/**
* 重定向
*/

View File

@ -2,12 +2,10 @@ package cn.dev33.satoken.servlet.model;
import java.io.IOException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Response for Servlet
@ -37,33 +35,6 @@ public class SaResponseForServlet implements SaResponse {
return response;
}
/**
* 删除指定Cookie
*/
@Override
public void deleteCookie(String name) {
addCookie(name, null, null, null, 0, false, false);
}
/**
* 写入指定Cookie
*/
@Override
public void addCookie(String name, String value, String path, String domain, int timeout, boolean isHttpOnly, boolean isSecure) {
Cookie cookie = new Cookie(name, value);
if(SaFoxUtil.isEmpty(path) == true) {
path = "/";
}
if(SaFoxUtil.isEmpty(domain) == false) {
cookie.setDomain(domain);
}
cookie.setPath(path);
cookie.setMaxAge(timeout);
cookie.setHttpOnly(isHttpOnly);
cookie.setSecure(isSecure);
response.addCookie(cookie);
}
/**
* 设置响应状态码
*/
@ -82,6 +53,17 @@ public class SaResponseForServlet implements SaResponse {
return this;
}
/**
* 在响应头里添加一个值
* @param name 名字
* @param value
* @return 对象自身
*/
public SaResponse addHeader(String name, String value) {
response.addHeader(name, value);
return this;
}
/**
* 重定向
*/

View File

@ -1,6 +1,5 @@
package cn.dev33.satoken.solon.model;
import org.noear.solon.Utils;
import org.noear.solon.core.handle.Context;
import cn.dev33.satoken.context.model.SaResponse;
@ -22,20 +21,6 @@ public class SaResponseForSolon implements SaResponse {
return ctx;
}
@Override
public void deleteCookie(String s) {
ctx.cookieRemove(s);
}
@Override
public void addCookie(String name, String value, String path, String domain, int timeout, boolean isHttpOnly, boolean isSecure) {
if (Utils.isNotEmpty(path)) {
path = "/";
}
ctx.cookieSet(name, value, domain, path, timeout);
}
@Override
public SaResponse setStatus(int sc) {
ctx.status(sc);
@ -47,6 +32,17 @@ public class SaResponseForSolon implements SaResponse {
ctx.headerSet(name, value);
return this;
}
/**
* 在响应头里添加一个值
* @param name 名字
* @param value
* @return 对象自身
*/
public SaResponse addHeader(String name, String value) {
ctx.headerAdd(name, value);
return this;
}
@Override
public Object redirect(String url) {