- 对于认证的理解
- JWT结构
- OAuth2.0
- 微信OAuth2.0登录
- OAuth2.0 授权模式
- Authorization Code Flow
- Authorization Code Flow With PKCE
- Client Credentials Flow
- Resource Owner Password Flow
- Device Authorization Flow
- 即将到来的OAuth2.1
- OIDC(OpenID Connect) 1.0
- OAuth2.0 & OIDC 1.0 授权模式选型
- SSO
- SLO
- Spring Security OAuth2 及 Auhorization Server
- Security架构
- Authorization Server核心FIlter
- Spring Security OAuth2支持情况
- 扩展后的Authorization Code + PKCE时序图
- 认证架构演进
- 前后端不分离 + RPC
- 前后端分离
- 前后端分离 + 应用网关
- Istio对Resource Server的支持
- APISIX对Resource Server的支持
- 附录
- 附录A - 微信OAuth端点
- 附录B - OAuth & OIDC术语
- 附录C - OAuth & OIDC常用端点
- 传统session认证(认证通过信息写进session中、前后端不分离、模版引擎)
- 分布式session管理(如借助redis统一管理session)
- 单点登录、单点登出(需要
统一的认证中心
来维护用户认证信息,如CAS)- 在CAS中就已经使用了浏览器端重定向、颁发ticket、验证ticket并返回用户信息等,
- 2类Session:CAS Server本身借助session来管理用户登录状态,而其他接入的Cas Client也借助各自的Session来管理各自的登录态。
- 现阶段单点登录:多个SPA前端
- OAuth三方登录集成(如对接微信登录)
- OAuth也是利用浏览器重定向,code换accessToken,accessToken换用户信息的套路(类似CAS)
- JWT的流行及无状态(使用JWT来代替session存储,契合OAuth2 Password)
- 账号密码暴露给客户端
- 客户端接入认证的逻辑总在变(如今天支持账号密码,明天支持手机号,后天支持微信登录,大后天…)
- 吊销token的有状态
- 身份云IDasS
- https://www.authing.co/
- https://auth0.com/
- Azure ADFS
JWT结构
https://jwt.io/
OAuth2.0 微信OAuth2.0登录
https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
几个角度:
1)微信:微信作为Authorization Server和Resource Server
2)身份云:身份云作为Authorization Server,我们的应用作为Client和Resource Server。
3)自建用户认证中心:我们自建Authorization Server,我们的应用作为Client和Resource Server。
起初对OAuth的理解就局限于借助第三方登录,并获取第三方用户信息,且转化为自己的系统用户,
当时看到比较多的就是OAuth2的4种授权类型,
但是随着学习的深入,发现OAuth2能做的远不止第三方登录。
之后再从OAuth2到OIDC(支持身份认证、单点登出等)
,貌似一套完整的统一认证模型
就清晰了。
可以对比之前说的对接微信登录,那么:
- 微信 - 是认证中心(提供了OAuth协议)
- 微信 - 是资源(用户身份信息)的提供者
- 我们的接入系统 - 是接入微信登录的客户端,且是资源(用户身份)的索取者
注:
通过Oauth 用户在微信平台的域名下完成认证,在微信平台的网页上输入用户认证信息,
接入微信登录的系统是无法获取到微信平台的用户账号、密码等信息的。
换个角度,假如把微信也变成我们自己内部的一个系统,即统一认证中心(管理用户及认证,实现Oauth协议),那么:
- 我们的统一认证中心 - 提供了OAuth协议(用户管理、权限管理、认证UI管理等)
- 我们的资源系统 - 是资源(如后端API接口)的提供者,需要对accessToken进行认证和鉴权
- 我们的接入系统 - 是接入统一认证中心的客户端,且是资源(调用后端API)的索取者
从这个角度来看,我们自己也可以实现OAuth/OIDC协议,来作为我们自己的统一认证中心,
我们自己的内部应用都可以通过标准协议OAuth/OIDC进行接入,
将繁琐的用户认证工作都放到统一认证中心,减轻接入应用的负担,
而我们的资源服务仅需要对accessToken进行验证,
通过OAuth/OIDC还可以实现单点登录、单点登出,
而其他第三方应用也可以通过标准的OAuth/OIDC协议接入到我们的统一认证中心,
即通过我们的用户登录他们的系统。
OAuth2.0 授权模式
授权模式 | 说明 |
---|---|
授权码模式(Authorization Code) 通过code换取oken | 较为常用,安全等级高,适用于存在Web后端的应用。 注: 通过code获取Token需要同时携带ClientId和ClientSecret, 所以该模式仅适用于可以安全存储ClientKey且不会泄露的应用(如后端服务), 而前端服务(SPA)、原生应用(桌面、移动端)都会暴露给具体用户,ClientKey有被用户拦截的风险, 下面提到的PKCE可适用于前端服务(SPA)、原生应用(桌面、移动端)。 |
授权码+PKCE模式(Authorization Code With PKCE) 通过code+code_verifier换取oken | 可适用于前端服务(SPA)、原生应用(桌面、移动端)。 |
客户端凭证模式(Client Credentials) 通过Cient凭证(ClientId和Secret)换取token | 适用于服务器间(M2M)通信。 |
刷新令牌模式(Refresh Token) 通过refresh_token换取新token | 根据之前授权通过时颁发的refresh_token获取新的access_token,避免用户多次重复登录(无感登录),提升用户体验。 |
设备码模式(Device Code) deviceCode+UserCode+VerficationUri 引导用户通过其他设备(如手机)进行认证授权 | 适用于可联网的无浏览器或输入受限的设备授权,例如智能电视、电子相册、打印机等。 |
直接返回token | 不推荐, 作为授权码模式的备用选项。 |
直接通过用户名密码换取token | 不推荐, 最不安全,客户端需要知道用户账号, 且客户端与认证方式强绑定,后续认证方式修改客户端也需要一起修改。 |
Authorization Code Flow
注: 把上图中的第5、6、7步删除,即对应Implicit授权模式。
Authorization Code Flow With PKCE
PKCE(Proof Key for Code Exchange)
通过一种密码学手段阻止CSRF和Authorization Code注入攻击,
确保恶意第三方即使截获Authorization Code,也无法向认证服务器交换Access Token,
同时不需要在客户端存储ClientKey,避免ClientKey被泄露。
code_verifier 验证码,随机字符串
code_challenge 挑战码,code_challenge = Base64(Sha256(code_verifier))
Client Credentials Flow
Resource Owner Password Flow
不推荐
1)账号密码暴露给Client端,不安全
2)客户端与认证方式绑定,认证逻辑修改则所有Client都需要修改
Device Authorization Flow
即将到来的OAuth2.1
https://oauth.net/2.1/
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05
- 仅保留
Authorization Code
,Client Credentials
,Refresh Token
,Device Code
授权模式 - 移除
Implicit和Password授权模式 - Authorization Code模式必须使用PKCE
- Bearer Token不可使用query参数形式
- Refresh Token for Public Client更严格(sender-contrained or one-time use)
OIDC(OpenID Connect) 1.0
- OAuth2.0的超集
- 身份层 - ID Token
- UserInfo Endpoint
- 单点登出
OIDC补充协议 | 功能 |
---|---|
OpenID Connect Core 1.0 - UserInfo Endpoint | RP端向OP发出查询用户详细信息的请求 |
OpenID Connect RP-Initiated Logout 1.0 | RP端向OP发出的初始登出请求(适用于单点登出SLO,end_session_endpoint ),OP首先清除OP端的session, 然后向当前session下的其他已登录的RP触发Front-Channel或者Back-Channel登出请求, 最终可通过 post_logout_redirect_uri 重定向会当前RP |
OpenID Connect Front-Channel Logout 1.0 | 返回OP前端登出页面, 页面通过嵌入iframe(src= frontchannel_logout_uri ),同时触发当前OP Session对应的多个Client的登出接口 |
OpenID Connect Back-Channel Logout 1.0 | OP直接向当前OP Session对应的多个Client后端服务(不依赖User Agent,如不依赖浏览器)发送登出请求backchannel_logout_uri ,且适用于User Agent被关闭的时候也可以退出登录。 |
OpenID Connect Session Management 1.0 | RP端通过check_session_iframe 监听当前UserAgent中用户的登录状态,收到登录状态改变通知后RP应该清除自己的Session信息、页面跳转等。 |
OAuth2.0 & OIDC 1.0 授权模式选型
总结上图,关于OIDC的最优建议如下:
应用类型(即Client) | 选型建议 |
---|---|
SPA 如Vue、React等单页应用(前后端分离), 有专门的后端资源服务层(Resource Server)提供API服务 | PKCE + 授权码模式 即由前端接入认证,而后端仅接入鉴权 注: 如采用Nodej.js运行时环境并提供安全的后端存储,则可直接使用授权码模式。 |
原生应用Native(移动端应用、桌面应用) 如Android、Ios(前后端分离), 有专门的后端资源服务层(Resource Server)提供API服务 | PKCE + 授权码模式 即由前端接入认证,而后端仅接入鉴权 |
WEB应用(即存在Web后端服务) 适用于获取第三方资源、集成第三方登录(如微信登录)、集成SSO | 授权码模式 接入Oauth后携带code回调到Web后端, 由后端获取AccessToken, 在后端携带AccessToken请求资源, 可使用session存储Token和User信息, 即使用session作为当前Web应用的身份验证机制 |
服务器间通信 无终端用户 | Client Credentials 模式 |
SSO
SLO
Spring Security OAuth2 及 Auhorization Server
Spring Seurity在2019年11月发布了OAuth 2.0 Migration Guide,
- 宣布禁用原spring-security-oauth,并将其合并到Spring Security 5.2+,
- 并且不再提供Authorization Server的支持。
后续在广大开发者的呼声下,在2020年4月Spring官方发布声明Announcing the Spring Authorization Server,
- 宣布启动由社区主导的spring-authorization-server项目,专注于OAuth2 Authorization Server实现,
- 目前最新版本为spring-authorization-server:0.2.2(发布于2022-01-27)。
截止到今天(2022-04-21):
- Spring Security 5.6.3- OAuth2
- OAuth2 Log In
- OAuth2 Client
- OAuth2 Resource Server
- Spring社区版 - OAuth2 Authorization Server 0.2.3
如上几大模块组成了Spring Security对OAuth2生态的最新支持。
Security架构
比如认证相关的Filter,其总体结构如下:
- AuthenticationProcessingFilter - 认证过滤器
- AuthenticationManager -> ProviderManager - 认证管理器
- AuthenticationProvider - 具体的认证逻辑实现
- AuthenticationSuccessHandler - 认证成功处理器
- AuthenticationFailureHandler - 认证失败处理器
- AuthenticationManager -> ProviderManager - 认证管理器
Authorization Server整体调用层次:
- EndpointFilter
- AuthenticationConverter - 转换request为Authentication
- AuthenticationManager -> ProviderManager - 认证管理器
- AuthenticationProvider.authenticate(authentication)
- AuthenticaionSuceessHandler.onAuthenticationSuccess
- AuthenticaionFailureHandler.onAuthenticationFailure
Authorization Server核心FIlter
核心FIlter | endpoint | 说明 |
---|---|---|
OAuth2AuthorizationEndpointFilter | GET|POST /oauth2/authorize | 授权端点,即RP跳转到OP的认证入口, 且EU认证通过后,OP重定向回RP,且附加code参数 |
OAuth2ClientAuthenticationFilter | POST /oauth2/token|introspect|revoke | 即RP向OP发送获取token请求、检查token、吊销token时,OP端提供的认证逻辑 |
OAuth2TokenEndpointFilter | POST /oauth2/token | Token端点,RP向OP请求Token(通过code换token、执行refresh_token流程) |
OAuth2TokenIntrospectionEndpointFilter | POST /oauth2/introspect | 校验Token端点,RP请求OP检测token有效性 |
OAuth2TokenRevocationEndpointFilter | POST /oauth2/revoke | 吊销Token端点,RP请求OP吊销token |
OidcProviderConfigurationEndpointFilter | GET /.well-known/openid-configuration | OIDC协议发现端点 |
OidcUserInfoEndpointFilter | GET /userinfo | 用户信息端点,提供用户信息查询 |
OidcClientRegistrationEndpointFilter | POST /connect/register | 客户端信息注册端点(暂未使用) |
Spring Security OAuth2支持情况
查看Spring Security相关源码,可以发现如上提到的核心功能支持情况如下表:
功能 | Authorization Server | OAuth2 Client |
---|---|---|
Authorization Code Flow with PKCE 授权码Code流程 + PKCE | ✔️ | ✔️ |
Refresh Token without client_secret PKCE后续无client_secret刷新token | ❌ | ❌ |
Refresh Token Retotation(Reuse) 令牌轮换 | ✔️ | ✔️ |
OAuth2 SSO 单点登录 | ✔️ | ✔️ |
OIDC end_session_endpoint 单点登出 | ❌ | ✔️ |
所以以上表格中未实现的,也就是需要扩展的功能。
扩展后的Authorization Code + PKCE时序图
认证架构演进 前后端不分离 + RPC 前后端分离 前后端分离 + 应用网关 Istio对Resource Server的支持
RequestAuthentication 整体配置结构如下:
namespace
策略生效的命名空间selector -> matchLables
指定策略作用的目标workloadjwtRules
jwt验证规则的配置issuer
jwt的发布者audiences
受众,即jwt的接受者jwksUri | jwks
jwks(公钥),即用来验证jwt签名的公钥,jwksUri和jwks二选一fromHeaders
jwt在请求头的位置name
header名称prefix
token前缀,如"Bearer "
fromParams
jwt在query参数中的参数名称outputPayloadToHeader
指定jwt验证通过后的payload传递给哪个请求headerforwardOriginalToken
是否保留原始token,默认false
jwtRules
概括起来分为如下几类配置:
- jwt token的位置(jwtRules.fromHeaders, jwtRules.fromParams)
- jwt.payload等相关属性(jwtRules.issuer, jwtRules.audiences)、request
- jwt公钥,即JWKS(JSON Web Key Set),用来验证jwt的签名(jwtRules.jwksUri, jwtRules.jwks)
- 是否保留jwt(jwtRules.outputPayloadToHeader, jwtRules.forwardOriginalToken)
RequestAuthentication 配置示例
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: httpbin
namespace: foo
spec:
# worload的label选择器
selector:
matchLabels:
app: httpbin
# jwt验证规则
jwtRules:
# jwt的发布者
- issuer: "issuer-foo"
# 受众,即jwt的接受者
audiences:
- bookstore_android.apps.example.com
bookstore_web.apps.example.com
# jwks(公钥)uri发现地址,即用来验证jwt签名的公钥
jwksUri: https://example.com/.well-known/jwks.json
# jwks(此属性与jwksUri需二选一)
jwks: "jwks json"
# jwt在请求头的位置
fromHeaders:
- name: x-jwt-assertion
prefix: "Bearer "
# jwt在query参数中的参数名称
fromParams:
- "my_token"
# 指定jwt验证通过后的payload传递给哪个请求header
# 传递格式:base64_encoded(jwt_payload_in_JSON)
outputPayloadToHeader: payload_header
# 是否保留原始token(若保留则继续传递token到upstream请求),默认false
forwardOriginalToken: false
注:
- jwt验证通过,则正常接受请求
- jwt验证失败,则拒绝请求
jwt为空,默认接受请求(需通过设置Authoriation policies来拒绝不带token的请求)
- 支持多个不同位置的jwt token,
- 但仅支持一个有效的token(多个有效的token会导致输出的principal不确定)
AuthorizationPolicy 其整体结构如下:
namespace
策略生效的命名空间(即目标workload所属的namespace)selector -> matchLables
指定策略作用的目标workloadactions: CUSTOM | DENY | ALLOW
是否允许请求rules
指定触发action的条件(即满足rules定义的请求,则执行action)from -> source[]
指定请求来源(source满足的条件),为空则表示全部来源
pincipals(workload身份)、requestPrincipals(用户身份)、namespaces、ipBlocks、remoteIpBlocks及相应not属性to -> operation[]
指定请求的 *** 作(request满足的条件),为空则表示所有 *** 作
hosts、ports、methods、paths及相应not属性when
指定额外的附件条件(key对应Istio属性)key -> values[] | notValues[]
request.headers[header_name]、
request.auth.[principal | audiences | presenter | claims[claimName] ]、
source.[ip | namespace | principal ]、remote.ip、
destination.[ip | port]、 connection.sni
AuthorizationPolicy配置概括起来就是:
允许(或拒绝)
来源workload 或 用户
对目标workload
进行什么 *** 作
,并且需要满足什么条件
。
具体配置示例:
# 作用目标: workload(namespace=foo, label(app=httpbin, version=v1))
# 动作action: 允许访问ALLOW
# 来源rules.from.source: principals=="cluster.local/ns/default/sa/sleep" or namepsace=="dev"
# *** 作rules.to.operation:method=="GET"
# 条件rules.when: request.auth.claims[iss]=="https://accounts.google.com"
# 即[仅允许][cluster.local/ns/default/sa/sleep身份(PeerAuthentication mTls验证通过)、或者dev命名空间]的workload访问[httpbin:v1服务的GET请求]
# 且[用户RequestAuthentication JWT验证通过、且jwt.claims[iss]=="https://accounts.google.com"]
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: foo
spec:
selector:
matchLabels:
app: httpbin
version: v1
action: ALLOW
rules:
- from:
- source:
# 指定来源workload身份
# principals: ["*"]则表示需要PeerAuthentication mtls验证通过
principals: ["cluster.local/ns/default/sa/sleep"]
- source:
# 指定来源namespace
namespaces: ["dev"]
to:
- operation:
# 执行GET请求
methods: ["GET"]
when:
# 指定用户jwt需验证通过,且iss为指定值
- key: request.auth.claims[iss]
values: ["https://accounts.google.com"]
# [拒绝][不属于foo命名空间的服务]访问[httpbin:v1服务]
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin-deny
namespace: foo
spec:
selector:
matchLabels:
app: httpbin
version: v1
action: DENY
rules:
- from:
- source:
# 不是来自于foo命名空间
notNamespaces: ["foo"]
# 访问非/healthz的请求,均需要JWT验证通过,
# 而访问/healthz的请求,可无需JWT验证(即请求未携带JWT令牌、或者携带的JWT令牌为空)
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: disable-jwt-for-healthz
namespace: default
spec:
selector:
matchLabels:
app: products
action: ALLOW
rules:
- to:
- operation:
# 访问除了/healthz以外的path
notPaths: ["/healthz"]
from:
- source:
# 需要RequestAuthentication JWT验证通过
requestPrincipals: ["*"]
APISIX对Resource Server的支持
附录A - 微信OAuth端点
并非标准OAuth2 Authorization Code模式,例如:
- appid, secret
- token_endpoint不唯一
- https://open.weixin.qq.com/connect/qrconnect
- ?appid=APPID
- &redirect_uri=REDIRECT_URI
- &response_type=code
- &scope=snsapi_login|snsapi_userinfo
- &state=STATE
- #wechat_redirect
- https://api.weixin.qq.com/sns/oauth2/access_token
- ?appid=APPID
- &secret=SECRET
- &code=CODE
- &grant_type=authorization_code
- https://api.weixin.qq.com/sns/oauth2/refresh_token
- ?appid=APPID
- &grant_type=refresh_token
- &refresh_token=REFRESH_TOKEN
- https://api.weixin.qq.com/sns/userinfo
- ?access_token=ACCESS_TOKEN
- &openid=OPENID
附录B - OAuth & OIDC术语
OIDC及OAuth核心术语:
OIDC | OAuth | 说明 |
---|---|---|
EU End User | Resource Owner | 拥有资源的用户 |
RP Relying Party | Client Third-party application | 用来代指OAuth2中的受信任的客户端(Web应用、SPA、移动端应用…), 即接入认证中心(OP、Authorization Server)登录跳转且并且获取Token(IdToken、AccessToken), 可通过IdToken获取用户身份信息, 然后携带AccessToken访问资源(Resource Server、Userinfo Endpoint) |
OP OpenID Provider | Auhtorization Server | 为用户(EU、Resource Owner)提供认证的服务(如微信授权平台、Github授权平台、自建用户中心等), 用来为应用客户端(RP、Client)提供用户(EU、Resource Owner)的身份认证信息 |
Resource Server | Resource Server | 资源服务器, 即提供资源API且需要对(受保护的)资源访问进行鉴权的服务, Cilent端携带AccessToken访问ResourceServer, 然后由Resource Server对此AccessToken进行鉴权 |
Endpoint | Endpoint | 端点,即提供的API接口 |
ID Token | 无 | 身份令牌(JWT), 用于认证,标识用户身份已经认证,可用于获取用户身份信息(如用户名、头像等)。 |
Access Token | Access Token | 访问令牌(JWT), 用于鉴权,适用于API的访问鉴权。 |
User Agent | User Agent | 应用执行端, 如浏览器,手机端,即RP的执行端 |
附录C - OAuth & OIDC常用端点
OIDC和OAuth的常用端点(Endpoint,即提供的API接口)
endpoint | 提供方 | 调用方 | 认证 | 格式 | desc |
---|---|---|---|---|---|
authoriztion_endpoint | OP | RP | 无 | Code Flow模式: GET authorization_endpoint ?response_type=code &redirect_uri={redirect_uri} &client_id={client_id} &state={random_str} &nonce={another_random_str} &scope=openid profile email PKCE + Code Flow模式 (额外添加code_challenge参数) GET authorization_endpoint ?response_type=code &redirect_uri={redirect_uri} &client_id={client_id} &state={random_str} &nonce={another_random_str} &scope=openid profile email &code_challenge_method=S256 &code_challenge={code_challenge} | 进入OP登录授权界面 |
redirect_uri | RP | OP | 无 | GET redirect_uri ?code={code} &state={random_str} | 在OP登录成功后,将code回调给RP |
token_endpoint | OP | RP | client_secret_post client_secret_basic | Code Flow模式: POST token_endpoint ?grant_type=authorization_code &code={code} &redirect_uri={redirect_uri} &client_id={client_id} &client_secret={client_secret} PKCE + Code Flow模式 (删除client_secret参数后 额外添加code_verfier参数) POST token_endpoint ?grant_type=authorization_code &code={code} &redirect_uri={redirect_uri} &client_id={client_id} &code_verifier={code_verifier} Refresh Token模式: POST token_endpoint ?grant_type=refresh_token &client_id={client_id} &client_secret={client_secret} &refresh_token={refresh_token} 注:在PKCE模式下支持无client_secret执行刷新token流程 | 通过code获取access_token、id_token, 根据refresh_token获取新的access_token |
userinfo_endpoint | OP | RP | Bearer {access_token} | GET userinfo_endpoint | 根据acces_token获取用户信息 |
introspection_endpoint | OP | Resource Server | client_secret_post client_secret_basic | POST introspection_endpoint ?token={your_token} | 用于验证access_token是否有效, 通过返回结果中的active进行标识 |
revocation_endpoint | OP | RP | client_secret_post client_secret_basic | POST revocation_endpoint ?token={your_token} &token_type_hint={} &token_type_hint=access_token | 撤销token、access_token, 若撤销refresh_token,则此refresh_token关联的access_token也会被撤销 |
end_session_endpoint | OP | RP | 无 | GET end_session_point ?id_token_hint={id_token_issued_to_client} &post_logout_redirect_uri={post_logout_redirect_uri} &state={random_str} | 结束浏览器中OP的session会话, 并触发后续的多RP的Front-Channel或Back-Channel登出, 最后可配合post_logout_redirect_uri 重定向回RP页面 |
post_logout_redirect_uri | RP | OP | 无 | GET post_logout_redirect_uri | OP登出成功后会重定向回RP页面, 或对应于RP登录界面、登出状态展示页面等 |
check_session_iframe | OP | RP | 无 | RP iframe.src=check_session_iframe | Session Management,检查当前RP的登录状态 |
frontchannel_logout_uri | RP | OP iframe | 无 | GET fontchannel_logout_uri ?iss={issureId} &sid={idToken.sid} | RP前端实现的登出处理页面, 由OP Front-Channel模式下返回的Html iframe调用 |
backchannel_logout_uri | RP | OP | 无 | POST backchannel_logout_uri ?logout_token={logout_token like idToken} | RP后端实现的后端登出接口, RP后端需验证logout_token并根据logout_token找出并清除后端的对应的session信息 |
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)