Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
shengzhang_
sa-token
提交
a11ad64d
sa-token
项目概览
shengzhang_
/
sa-token
通知
68
Star
16
Fork
4
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
sa-token
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
a11ad64d
编写于
2月 08, 2021
作者:
shengzhang_
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
完成同域模式下的单点登录
上级
af0b2285
变更
13
隐藏空白更改
内联
并排
Showing
13 changed file
with
357 addition
and
14 deletion
+357
-14
sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java
.../src/main/java/cn/dev33/satoken/config/SaTokenConfig.java
+22
-4
sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java
.../src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java
+2
-1
sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java
...ava/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java
+2
-2
sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java
.../main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java
+10
-4
sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java
...ken-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java
+4
-2
sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java
...rc/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java
+7
-0
sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java
...-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java
+1
-1
sa-token-demo-springboot/src/main/java/com/pj/CorsFilter.java
...oken-demo-springboot/src/main/java/com/pj/CorsFilter.java
+67
-0
sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java
...o-springboot/src/main/java/com/pj/test/SSOController.java
+35
-0
sa-token-demo-springboot/src/main/resources/application.yml
sa-token-demo-springboot/src/main/resources/application.yml
+2
-0
sa-token-doc/doc/_sidebar.md
sa-token-doc/doc/_sidebar.md
+4
-0
sa-token-doc/doc/senior/dcs.md
sa-token-doc/doc/senior/dcs.md
+13
-0
sa-token-doc/doc/senior/sso.md
sa-token-doc/doc/senior/sso.md
+188
-0
未找到文件。
sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java
浏览文件 @
a11ad64d
...
...
@@ -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
+
"]"
;
}
...
...
sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookie.java
浏览文件 @
a11ad64d
...
...
@@ -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
...
...
sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieDefaultImpl.java
浏览文件 @
a11ad64d
...
...
@@ -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
);
}
/**
...
...
sa-token-core/src/main/java/cn/dev33/satoken/cookie/SaTokenCookieUtil.java
浏览文件 @
a11ad64d
...
...
@@ -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
.
get
Domain
(),
cookie
.
get
MaxAge
());
return
;
}
}
...
...
sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java
浏览文件 @
a11ad64d
...
...
@@ -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
());
}
}
}
...
...
sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenInsideUtil.java
浏览文件 @
a11ad64d
...
...
@@ -41,6 +41,13 @@ public class SaTokenInsideUtil {
return
sb
.
toString
();
}
/**
* 指定字符串是否为null或者空字符串
*/
public
static
boolean
isEmpty
(
String
str
)
{
return
str
==
null
||
""
.
equals
(
str
);
}
/**
* 以当前时间戳和随机int数字拼接一个随机字符串
*
...
...
sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java
浏览文件 @
a11ad64d
...
...
@@ -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
());
}
}
...
...
sa-token-demo-springboot/src/main/java/com/pj/CorsFilter.java
0 → 100644
浏览文件 @
a11ad64d
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
()
{
}
}
sa-token-demo-springboot/src/main/java/com/pj/test/SSOController.java
0 → 100644
浏览文件 @
a11ad64d
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
);
}
}
sa-token-demo-springboot/src/main/resources/application.yml
浏览文件 @
a11ad64d
...
...
@@ -17,6 +17,8 @@ spring:
is-share
:
true
# token风格
token-style
:
uuid
# 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie的场景
# cookie-domain: stp.com
# redis配置
...
...
sa-token-doc/doc/_sidebar.md
浏览文件 @
a11ad64d
...
...
@@ -21,6 +21,10 @@
-
[
框架配置
](
/use/config
)
-
[
会话治理
](
/use/search-session
)
-
**进阶**
-
[
集群、分布式
](
/senior/dcs
)
-
[
单点登录
](
/senior/sso
)
-
**其它**
-
[
常见问题
](
/more/common-questions
)
-
[
友情链接
](
/more/link
)
...
...
sa-token-doc/doc/senior/dcs.md
0 → 100644
浏览文件 @
a11ad64d
# 集群、分布式
集群模式下,
sa-token-doc/doc/senior/sso.md
0 → 100644
浏览文件 @
a11ad64d
# 单点登录
---
### 什么是单点登录?解决什么问题?
举个场景:假设你的系统被切割成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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录