mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-05-08 22:58:00 +08:00
新增登录后将 Token 写入响应头的方法
This commit is contained in:
parent
fa14d98462
commit
e21928183d
@ -46,6 +46,9 @@ public class SaTokenConfig implements Serializable {
|
|||||||
/** 是否尝试从cookie里读取token */
|
/** 是否尝试从cookie里读取token */
|
||||||
private Boolean isReadCookie = true;
|
private Boolean isReadCookie = true;
|
||||||
|
|
||||||
|
/** 是否在登录后将 Token 写入到响应头 */
|
||||||
|
private Boolean isWriteHeader = false;
|
||||||
|
|
||||||
/** token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */
|
/** token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */
|
||||||
private String tokenStyle = "uuid";
|
private String tokenStyle = "uuid";
|
||||||
|
|
||||||
@ -240,6 +243,22 @@ public class SaTokenConfig implements Serializable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 是否在登录后将 Token 写入到响应头
|
||||||
|
*/
|
||||||
|
public Boolean getIsWriteHeader() {
|
||||||
|
return isWriteHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isWriteHeader 是否在登录后将 Token 写入到响应头
|
||||||
|
* @return 对象自身
|
||||||
|
*/
|
||||||
|
public SaTokenConfig setIsWriteHeader(Boolean isWriteHeader) {
|
||||||
|
this.isWriteHeader = isWriteHeader;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
* @return token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||||
*/
|
*/
|
||||||
@ -462,6 +481,7 @@ public class SaTokenConfig implements Serializable {
|
|||||||
+ ", isReadBody=" + isReadBody
|
+ ", isReadBody=" + isReadBody
|
||||||
+ ", isReadHeader=" + isReadHeader
|
+ ", isReadHeader=" + isReadHeader
|
||||||
+ ", isReadCookie=" + isReadCookie
|
+ ", isReadCookie=" + isReadCookie
|
||||||
|
+ ", isWriteHeader=" + isWriteHeader
|
||||||
+ ", tokenStyle=" + tokenStyle
|
+ ", tokenStyle=" + tokenStyle
|
||||||
+ ", dataRefreshPeriod=" + dataRefreshPeriod
|
+ ", dataRefreshPeriod=" + dataRefreshPeriod
|
||||||
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
|
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
|
||||||
|
@ -7,6 +7,11 @@ package cn.dev33.satoken.context.model;
|
|||||||
*/
|
*/
|
||||||
public interface SaResponse {
|
public interface SaResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定前端可以获取到哪些响应头时使用的参数名
|
||||||
|
*/
|
||||||
|
public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取底层源对象
|
* 获取底层源对象
|
||||||
* @return see note
|
* @return see note
|
||||||
|
@ -16,7 +16,7 @@ public class SaLoginConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param device 此次登录的客户端设备类型
|
* @param device 此次登录的客户端设备类型
|
||||||
* @return SaLoginModel配置对象
|
* @return 登录参数 Model
|
||||||
*/
|
*/
|
||||||
public static SaLoginModel setDevice(String device) {
|
public static SaLoginModel setDevice(String device) {
|
||||||
return create().setDevice(device);
|
return create().setDevice(device);
|
||||||
@ -24,7 +24,7 @@ public class SaLoginConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
* @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
||||||
* @return 对象自身
|
* @return 登录参数 Model
|
||||||
*/
|
*/
|
||||||
public static SaLoginModel setIsLastingCookie(Boolean isLastingCookie) {
|
public static SaLoginModel setIsLastingCookie(Boolean isLastingCookie) {
|
||||||
return create().setIsLastingCookie(isLastingCookie);
|
return create().setIsLastingCookie(isLastingCookie);
|
||||||
@ -32,7 +32,7 @@ public class SaLoginConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param timeout 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
|
* @param timeout 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
|
||||||
* @return 对象自身
|
* @return 登录参数 Model
|
||||||
*/
|
*/
|
||||||
public static SaLoginModel setTimeout(long timeout) {
|
public static SaLoginModel setTimeout(long timeout) {
|
||||||
return create().setTimeout(timeout);
|
return create().setTimeout(timeout);
|
||||||
@ -40,7 +40,7 @@ public class SaLoginConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param extraData 扩展信息(只在jwt模式下生效)
|
* @param extraData 扩展信息(只在jwt模式下生效)
|
||||||
* @return 对象自身
|
* @return 登录参数 Model
|
||||||
*/
|
*/
|
||||||
public static SaLoginModel setExtraData(Map<String, Object> extraData) {
|
public static SaLoginModel setExtraData(Map<String, Object> extraData) {
|
||||||
return create().setExtraData(extraData);
|
return create().setExtraData(extraData);
|
||||||
@ -48,7 +48,7 @@ public class SaLoginConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param token 预定Token(预定本次登录生成的Token值)
|
* @param token 预定Token(预定本次登录生成的Token值)
|
||||||
* @return 对象自身
|
* @return 登录参数 Model
|
||||||
*/
|
*/
|
||||||
public static SaLoginModel setToken(String token) {
|
public static SaLoginModel setToken(String token) {
|
||||||
return create().setToken(token);
|
return create().setToken(token);
|
||||||
@ -58,12 +58,20 @@ public class SaLoginConfig {
|
|||||||
* 写入扩展数据(只在jwt模式下生效)
|
* 写入扩展数据(只在jwt模式下生效)
|
||||||
* @param key 键
|
* @param key 键
|
||||||
* @param value 值
|
* @param value 值
|
||||||
* @return 对象自身
|
* @return 登录参数 Model
|
||||||
*/
|
*/
|
||||||
public static SaLoginModel setExtra(String key, Object value) {
|
public static SaLoginModel setExtra(String key, Object value) {
|
||||||
return create().setExtra(key, value);
|
return create().setExtra(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isWriteHeader 是否在登录后将 Token 写入到响应头
|
||||||
|
* @return 登录参数 Model
|
||||||
|
*/
|
||||||
|
public static SaLoginModel setIsWriteHeader(Boolean isWriteHeader) {
|
||||||
|
return create().setIsWriteHeader(isWriteHeader);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 静态方法获取一个 SaLoginModel 对象
|
* 静态方法获取一个 SaLoginModel 对象
|
||||||
* @return SaLoginModel 对象
|
* @return SaLoginModel 对象
|
||||||
|
@ -23,7 +23,7 @@ public class SaLoginModel {
|
|||||||
/**
|
/**
|
||||||
* 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
* 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
||||||
*/
|
*/
|
||||||
public Boolean isLastingCookie;
|
public Boolean isLastingCookie = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
|
* 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
|
||||||
@ -40,6 +40,9 @@ public class SaLoginModel {
|
|||||||
*/
|
*/
|
||||||
public String token;
|
public String token;
|
||||||
|
|
||||||
|
/** 是否在登录后将 Token 写入到响应头 */
|
||||||
|
private Boolean isWriteHeader;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 此次登录的客户端设备类型
|
* @return 此次登录的客户端设备类型
|
||||||
@ -58,12 +61,22 @@ public class SaLoginModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 参考 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
* @return 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
||||||
*/
|
*/
|
||||||
public Boolean getIsLastingCookie() {
|
public Boolean getIsLastingCookie() {
|
||||||
return isLastingCookie;
|
return isLastingCookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
||||||
|
*/
|
||||||
|
public Boolean getIsLastingCookieOrFalse() {
|
||||||
|
if(isLastingCookie == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isLastingCookie;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
* @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
|
||||||
* @return 对象自身
|
* @return 对象自身
|
||||||
@ -121,13 +134,45 @@ public class SaLoginModel {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 是否在登录后将 Token 写入到响应头
|
||||||
|
*/
|
||||||
|
public Boolean getIsWriteHeader() {
|
||||||
|
return isWriteHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 是否在登录后将 Token 写入到响应头
|
||||||
|
*/
|
||||||
|
public Boolean getIsWriteHeaderOrFalse() {
|
||||||
|
if(isWriteHeader == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isWriteHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isWriteHeader 是否在登录后将 Token 写入到响应头
|
||||||
|
* @return 对象自身
|
||||||
|
*/
|
||||||
|
public SaLoginModel setIsWriteHeader(Boolean isWriteHeader) {
|
||||||
|
this.isWriteHeader = isWriteHeader;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* toString
|
* toString
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "SaLoginModel [device=" + device + ", isLastingCookie=" + isLastingCookie + ", timeout=" + timeout
|
return "SaLoginModel ["
|
||||||
+ ", extraData=" + extraData + ", token=" + token + "]";
|
+ "device=" + device
|
||||||
|
+ ", isLastingCookie=" + isLastingCookie
|
||||||
|
+ ", timeout=" + timeout
|
||||||
|
+ ", extraData=" + extraData
|
||||||
|
+ ", token=" + token
|
||||||
|
+ ", isWriteHeader=" + isWriteHeader
|
||||||
|
+ "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------ 附加方法
|
// ------ 附加方法
|
||||||
@ -174,9 +219,10 @@ public class SaLoginModel {
|
|||||||
* @return Cookie时长
|
* @return Cookie时长
|
||||||
*/
|
*/
|
||||||
public int getCookieTimeout() {
|
public int getCookieTimeout() {
|
||||||
if(isLastingCookie == false) {
|
if(getIsLastingCookieOrFalse() == false) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
initTimeout();
|
||||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||||
return Integer.MAX_VALUE;
|
return Integer.MAX_VALUE;
|
||||||
}
|
}
|
||||||
@ -193,6 +239,15 @@ public class SaLoginModel {
|
|||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 timeout 值 (如果尚未配置timeout,则取全局配置的值)
|
||||||
|
*/
|
||||||
|
public void initTimeout() {
|
||||||
|
if(timeout == null) {
|
||||||
|
timeout = SaManager.getConfig().getTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建对象,初始化默认值
|
* 构建对象,初始化默认值
|
||||||
* @return 对象自身
|
* @return 对象自身
|
||||||
@ -210,12 +265,15 @@ public class SaLoginModel {
|
|||||||
// if(device == null) {
|
// if(device == null) {
|
||||||
// device = SaTokenConsts.DEFAULT_LOGIN_DEVICE;
|
// device = SaTokenConsts.DEFAULT_LOGIN_DEVICE;
|
||||||
// }
|
// }
|
||||||
if(isLastingCookie == null) {
|
// if(isLastingCookie == null) {
|
||||||
isLastingCookie = true;
|
// isLastingCookie = true;
|
||||||
}
|
// }
|
||||||
if(timeout == null) {
|
if(timeout == null) {
|
||||||
timeout = config.getTimeout();
|
timeout = config.getTimeout();
|
||||||
}
|
}
|
||||||
|
if(isWriteHeader == null) {
|
||||||
|
isWriteHeader = config.getIsWriteHeader();
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import cn.dev33.satoken.config.SaTokenConfig;
|
|||||||
import cn.dev33.satoken.context.SaHolder;
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
import cn.dev33.satoken.context.model.SaCookie;
|
import cn.dev33.satoken.context.model.SaCookie;
|
||||||
import cn.dev33.satoken.context.model.SaRequest;
|
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.context.model.SaStorage;
|
||||||
import cn.dev33.satoken.dao.SaTokenDao;
|
import cn.dev33.satoken.dao.SaTokenDao;
|
||||||
import cn.dev33.satoken.exception.ApiDisabledException;
|
import cn.dev33.satoken.exception.ApiDisabledException;
|
||||||
@ -102,7 +103,7 @@ public class StpLogic {
|
|||||||
* @param tokenValue token值
|
* @param tokenValue token值
|
||||||
*/
|
*/
|
||||||
public void setTokenValue(String tokenValue){
|
public void setTokenValue(String tokenValue){
|
||||||
setTokenValue(tokenValue, getConfigOfCookieTimeout());
|
setTokenValue(tokenValue, new SaLoginModel().setTimeout(getConfig().getTimeout()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,17 +112,31 @@ public class StpLogic {
|
|||||||
* @param cookieTimeout Cookie存活时间(秒)
|
* @param cookieTimeout Cookie存活时间(秒)
|
||||||
*/
|
*/
|
||||||
public void setTokenValue(String tokenValue, int cookieTimeout){
|
public void setTokenValue(String tokenValue, int cookieTimeout){
|
||||||
|
setTokenValue(tokenValue, new SaLoginModel().setTimeout(cookieTimeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在当前会话写入当前TokenValue
|
||||||
|
* @param tokenValue token值
|
||||||
|
* @param loginModel 登录参数
|
||||||
|
*/
|
||||||
|
public void setTokenValue(String tokenValue, SaLoginModel loginModel){
|
||||||
|
|
||||||
if(SaFoxUtil.isEmpty(tokenValue)) {
|
if(SaFoxUtil.isEmpty(tokenValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 将token保存到[存储器]里
|
// 1. 将 Token 保存到 [存储器] 里
|
||||||
setTokenValueToStorage(tokenValue);
|
setTokenValueToStorage(tokenValue);
|
||||||
|
|
||||||
// 2. 将 Token 保存到 [Cookie] 里
|
// 2. 将 Token 保存到 [Cookie] 里
|
||||||
if (getConfig().getIsReadCookie()) {
|
if (getConfig().getIsReadCookie()) {
|
||||||
setTokenValueToCookie(tokenValue, cookieTimeout);
|
setTokenValueToCookie(tokenValue, loginModel.getCookieTimeout());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 将 Token 写入到响应头里
|
||||||
|
if(loginModel.getIsWriteHeaderOrFalse()) {
|
||||||
|
setTokenValueToResponseHeader(tokenValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +180,17 @@ public class StpLogic {
|
|||||||
SaHolder.getResponse().addCookie(cookie);
|
SaHolder.getResponse().addCookie(cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Token 写入到 [响应头] 里
|
||||||
|
* @param tokenValue token值
|
||||||
|
*/
|
||||||
|
public void setTokenValueToResponseHeader(String tokenValue){
|
||||||
|
String tokenName = getTokenName();
|
||||||
|
SaResponse response = SaHolder.getResponse();
|
||||||
|
response.setHeader(tokenName, tokenValue);
|
||||||
|
response.addHeader(SaResponse.ACCESS_CONTROL_EXPOSE_HEADERS, tokenName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前TokenValue
|
* 获取当前TokenValue
|
||||||
* @return 当前tokenValue
|
* @return 当前tokenValue
|
||||||
@ -292,7 +318,7 @@ public class StpLogic {
|
|||||||
String token = createLoginSession(id, loginModel);
|
String token = createLoginSession(id, loginModel);
|
||||||
|
|
||||||
// 2、在当前客户端注入Token
|
// 2、在当前客户端注入Token
|
||||||
setTokenValue(token, loginModel.getCookieTimeout());
|
setTokenValue(token, loginModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,6 +84,15 @@ public class StpUtil {
|
|||||||
stpLogic.setTokenValue(tokenValue, cookieTimeout);
|
stpLogic.setTokenValue(tokenValue, cookieTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在当前会话写入当前TokenValue
|
||||||
|
* @param tokenValue token值
|
||||||
|
* @param loginModel 登录参数
|
||||||
|
*/
|
||||||
|
public static void setTokenValue(String tokenValue, SaLoginModel loginModel){
|
||||||
|
stpLogic.setTokenValue(tokenValue, loginModel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前TokenValue
|
* 获取当前TokenValue
|
||||||
* @return 当前tokenValue
|
* @return 当前tokenValue
|
||||||
|
@ -8,6 +8,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
import cn.dev33.satoken.context.SaHolder;
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
import cn.dev33.satoken.filter.SaServletFilter;
|
import cn.dev33.satoken.filter.SaServletFilter;
|
||||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||||
|
import cn.dev33.satoken.router.SaHttpMethod;
|
||||||
|
import cn.dev33.satoken.router.SaRouter;
|
||||||
import cn.dev33.satoken.util.SaResult;
|
import cn.dev33.satoken.util.SaResult;
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 前置函数:在每次认证函数之前执行
|
// 前置函数:在每次认证函数之前执行
|
||||||
.setBeforeAuth(r -> {
|
.setBeforeAuth(obj -> {
|
||||||
// ---------- 设置一些安全响应头 ----------
|
// ---------- 设置一些安全响应头 ----------
|
||||||
SaHolder.getResponse()
|
SaHolder.getResponse()
|
||||||
// 服务器名称
|
// 服务器名称
|
||||||
@ -62,7 +64,21 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
.setHeader("X-XSS-Protection", "1; mode=block")
|
.setHeader("X-XSS-Protection", "1; mode=block")
|
||||||
// 禁用浏览器内容嗅探
|
// 禁用浏览器内容嗅探
|
||||||
.setHeader("X-Content-Type-Options", "nosniff")
|
.setHeader("X-Content-Type-Options", "nosniff")
|
||||||
;
|
|
||||||
|
// ---------- 设置跨域响应头 ----------
|
||||||
|
// 允许指定域访问跨域资源
|
||||||
|
.setHeader("Access-Control-Allow-Origin", "*")
|
||||||
|
// 允许所有请求方式
|
||||||
|
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
|
||||||
|
// 有效时间
|
||||||
|
.setHeader("Access-Control-Max-Age", "3600")
|
||||||
|
// 允许的header参数
|
||||||
|
.setHeader("Access-Control-Allow-Headers", "*");
|
||||||
|
|
||||||
|
// 如果是预检请求,则立即返回到前端
|
||||||
|
SaRouter.match(SaHttpMethod.OPTIONS)
|
||||||
|
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
|
||||||
|
.back();
|
||||||
})
|
})
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user