SSO 单点登录安全
概述
单点登录(Single Sign-On,SSO) 是一种身份认证机制,允许用户使用一组凭据(用户名/密码)登录后,即可访问多个相互独立的应用程序或系统,无需重复输入凭据。SSO 的核心价值在于提升用户体验、降低密码疲劳、集中化身份管理,并减少因重复登录导致的安全风险。
然而,SSO 也是一把双刃剑——它集中了认证的入口,一旦 SSO 系统被攻破,攻击者便可获得访问所有关联应用的权限。因此,SSO 的安全设计至关重要。
SSO 核心概念与架构模式
核心角色
| 角色 | 说明 |
|---|---|
| 用户(User) | 需要认证的最终使用者 |
| 身份提供者(IdP,Identity Provider) | 负责认证用户身份并颁发令牌的中心化服务 |
| 服务提供者(SP,Service Provider) | 也叫 Relying Party(RP),依赖 IdP 验证用户身份的应用程序 |
| 认证代理(Authentication Proxy) | 部署在 SP 前端的代理层,拦截未认证请求并重定向到 IdP |
常见架构模式
1. 中心化 Broker 模式
最经典的 SSO 架构,所有应用通过一个统一的认证中心(如 CAS Server)完成登录。用户访问应用 A → 重定向到 CAS → 登录 → 获取票据 → 访问应用 B 时自动携带票据验证。
User → App A → 重定向到 CAS Server → 登录成功 → 获取 ST(Service Ticket)
→ 携带 ST 访问 App A → App A 向 CAS 校验 ST → 允许访问
→ 访问 App B → 携带已存在的 TGT → 获取新的 ST → 无感登录2. 代理/网关模式
在微服务架构中,API 网关承担认证代理角色,所有请求先经过网关进行令牌验证,再将身份信息透传给后端服务。这种模式可有效隔离认证逻辑,但网关本身成为关键安全节点。
3. 联邦模式(Federation)
跨组织的 SSO 场景,通过信任联邦协议(如 SAML、OIDC)建立 IdP 之间的互信关系。典型场景包括企业间的合作平台、学术联盟等。
CAS(Central Authentication Service)协议与安全考量
协议概述
CAS 是较早的 SSO 协议,核心组件包括:
- CAS Server:独立的认证服务
- CAS Client:集成在各应用中的组件
- TGT(Ticket Granting Ticket):存储于 CAS Server 会话中,代表用户已登录的凭证
- ST(Service Ticket):一次性服务票据,用于访问具体应用
- PGT(Proxy Granting Ticket):代理授权的票据,支持级联认证
认证流程
- 用户访问受保护的 SP 资源
- SP 检测到未认证,重定向到 CAS Server 登录页面,携带
service参数 - 用户登录成功后,CAS Server 创建 TGT 并在 URL 中携带 ST 重定向回 SP
- SP 通过后台通道(back-channel)向 CAS Server 验证 ST 的有效性
- 验证通过后,SP 建立本地会话
安全考量
| 风险 | 说明 | 防护措施 |
|---|---|---|
| ST 重用攻击 | ST 被拦截后重复使用 | ST 设计为一次性,使用后立即失效;使用 HTTPS 加密传输 |
| TGT 劫持 | TGT Cookie 被窃取导致会话劫持 | 设置 Secure、HttpOnly、SameSite 标志;绑定客户端 IP |
| Service 参数篡改 | 攻击者修改 service 参数指向恶意站点 | 对 service 参数进行白名单校验;使用数字签名 |
| 重放攻击 | 截获认证响应并重放 | 引入时间戳和 nonce;限制票据有效期 |
| 开放重定向 | CAS Server 被用作开放重定向器 | 对重定向目标进行严格的 URL 白名单验证 |
CAS 协议安全配置示例
// CAS 客户端安全配置示例(Spring Security + CAS)
@Configuration
@EnableWebSecurity
public class CasSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/**", "/logout/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/login/cas/**")
)
.sessionManagement(session -> session
.sessionFixation().migrateSession()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
// CAS 认证过滤器配置
http.addFilterBefore(
casAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties());
filter.setAuthenticationManager(authenticationManager());
// 验证 service 参数白名单
filter.setAuthenticationSuccessHandler((request, response, auth) -> {
String service = request.getParameter("service");
if (!isAllowedServiceUrl(service)) {
throw new BadCredentialsException("非法 service 参数");
}
response.sendRedirect(service);
});
return filter;
}
private boolean isAllowedServiceUrl(String url) {
// 严格的白名单验证
List<String> allowedPatterns = Arrays.asList(
"https://app1.example.com/*",
"https://app2.example.com/*"
);
return allowedPatterns.stream()
.anyMatch(pattern -> url != null && url.matches(pattern.replace("*", ".*")));
}
}SAML 2.0 断言与安全配置
SAML 2.0 核心概念
SAML 2.0(Security Assertion Markup Language)是基于 XML 的联邦身份协议,在企业和政府 SSO 场景中广泛使用。
| 组件 | 说明 |
|---|---|
| 断言(Assertion) | IdP 签发的 XML 格式身份声明,包含认证、属性、授权等信息 |
| IdP(Identity Provider) | 负责认证用户并签发 SAML 断言 |
| SP(Service Provider) | 依赖 SAML 断言进行授权的应用 |
| 元数据(Metadata) | XML 格式的 SP/IdP 配置信息,包含证书、端点地址等 |
SAML 绑定模式
- Redirect Binding:通过 HTTP 重定向传输 SAML 请求,受 URL 长度限制
- POST Binding:通过 HTTP POST 表单传输 SAML 断言,支持更长的数据
- Artifact Binding:通过引用(artifact)间接传输断言,减少 URL 数据量
常见安全漏洞
1. SAML 断言伪造(XML Signature Wrapping)
这是 SAML 最经典也最危险的攻击方式。攻击者利用 XML 签名验证的缺陷,在合法签名元素外部包裹伪造的断言数据。
攻击原理:
<!-- 原始合法断言(已签名) -->
<Assertion ID="valid-assertion-123">
<Issuer>https://trusted-idp.example.com</Issuer>
<Subject>
<NameID>admin@example.com</NameID>
</Subject>
</Assertion>
<!-- 攻击者构造的伪造断言 -->
<soap:Envelope>
<soap:Body>
<!-- 将原始签名断言放入此处,SP 验证时找到签名但读取外部断言 -->
<Assertion ID="forged-assertion-999">
<Issuer>https://trusted-idp.example.com</Issuer>
<Subject>
<NameID>attacker@evil.com</NameID>
</Subject>
<!-- 内部包裹原始断言以满足签名验证 -->
<Assertion ID="valid-assertion-123">
...
<Signature>
<!-- 合法签名 -->
</Signature>
</Assertion>
</Assertion>
</soap:Body>
</soap:Envelope>防御方案:
- 使用安全断言解析库,禁用 DTD 和外部实体
- 验证断言 ID 的唯一性并检查是否已被消费
- 对 XML 签名采用 严格模式 校验,拒绝包含嵌套断言的消息
- 在 SP 端验证
InResponseTo属性匹配原始请求 ID
<!-- 安全的 SAML 元数据配置示例 -->
<EntityDescriptor entityID="https://sp.example.com/saml/metadata">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"
AuthnRequestsSigned="true"
WantAssertionsSigned="true">
<KeyDescriptor use="signing">
<KeyInfo>
<X509Data>
<X509Certificate>MIID...(Base64 编码的公钥证书)</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://sp.example.com/saml/acs"
index="0"/>
</SPSSODescriptor>
</EntityDescriptor>2. XML 外部实体注入(XXE)
如果 SAML 解析器未安全配置,攻击者可通过恶意的 XML 断言读取服务器本地文件或发起 SSRF 攻击。
防御: 在 XML 解析器中禁用 DTD、外部实体和外部参数实体。
// 安全的 SAML 断言解析
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);3. 未校验断言接收端点
SP 的 AssertionConsumerService(ACS)URL 如果接受来自任意来源的断言,攻击者可构造恶意绑定端点进行劫持。
防御: 严格校验 ACS URL 必须与预先注册的元数据一致;验证 Destination 属性。
OIDC 作为 SSO 协议的实现
OpenID Connect 概述
OIDC(OpenID Connect)在 OAuth 2.0 协议基础上增加了身份认证层,是目前现代 Web 和移动应用中 SSO 的主流选择。它使用 JWT(JSON Web Token)作为 ID Token 的载体,具有轻量、易解析、跨语言的优势。
OIDC 核心流程
用户 → 浏览器 → RP (SP)
→ 发起认证请求到 OP (IdP)
→ 用户同意授权
→ OP 返回 ID Token + Access Token
→ RP 验证 ID Token 签名
→ 建立本地会话OIDC 的 Token 类型
| Token | 用途 | 格式 | 安全要求 |
|---|---|---|---|
| ID Token | 身份认证信息 | JWT(签名或加密) | 必须验证签名、iss、aud、exp |
| Access Token | API 访问授权 | 不透明或 JWT | 仅 OP 和资源服务器可验证 |
| Refresh Token | 无感续期令牌 | 不透明 | 长期有效,需安全存储 |
OIDC 安全最佳实践
- ID Token 验证不能省略:必须验证签名、
iss(签发者)、aud(受众)、exp(过期时间)和nonce - 强制使用 PKCE:公共客户端(Native App / SPA)必须使用 PKCE(Proof Key for Code Exchange)防止授权码拦截
- State 参数防 CSRF:认证请求中的
state参数必须包含不可预测的值并在回调时校验 - Nonce 参数防重放:
nonce值应在 ID Token 中验证,确保一次性使用
// OIDC 客户端安全验证示例(Node.js + openid-client)
const { generators, Client } = require('openid-client');
// 生成 PKCE 参数
const codeVerifier = generators.codeVerifier(); // 随机字符串
const codeChallenge = generators.codeChallenge(codeVerifier);
const nonce = generators.nonce();
const state = generators.state();
// 构造认证请求
const authUrl = client.authorizationUrl({
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
nonce: nonce,
state: state,
});
// 回调处理——严格验证
async function handleCallback(req, res) {
const params = client.callbackParams(req);
// 验证 state 参数防止 CSRF
if (params.state !== req.session.oauthState) {
throw new Error('State 参数不匹配,可能遭受 CSRF 攻击');
}
// 验证 ID Token
const tokenSet = await client.callback(
redirectUri,
params,
{
code_verifier: req.session.codeVerifier, // 验证 PKCE
nonce: req.session.nonce, // 验证 nonce
state: req.session.oauthState, // 验证 state
}
);
const idToken = tokenSet.claims();
console.log(`用户认证成功: ${idToken.sub}`);
}SSO 常见安全漏洞
1. 跨域认证绕过
SSO 系统中,不同子域或跨域应用之间的信任关系常被攻击者利用。
典型场景:
- IdP 设置的 Cookie 作用域过大(如
Domain=.example.com),子域 XSS 漏洞可窃取全部会话 - 通配符回调 URI(如
https://*.example.com/callback)允许攻击者在可控子域获取授权码 - CORS 配置过于宽松,允许任意跨域读取用户信息
防护建议:
- Cookie 作用域遵循最小权限原则,避免宽泛的 Domain 设置
- 回调 URI 注册时使用精确匹配,禁止通配符
- 使用
SameSite=Strict或Lax属性限制 Cookie 跨站发送 - 不同安全级别的应用使用不同的客户端 ID 和 Secret
2. 重定向 URI 验证不严
重定向 URI 参数是 SSO 协议中最常被攻击的向量之一。
攻击方式:
// 合法回调 URI
https://app.example.com/callback
// 攻击者利用路径遍历
https://app.example.com/callback/../../evil.com/
// 利用开放重定向
https://app.example.com/callback?redirect=https://evil.com
// 利用解析差异
https://app.example.com/callback.evil.com/安全配置示例:
# Python Flask OIDC 回调 URI 严格验证
import re
from urllib.parse import urlparse
ALLOWED_REDIRECT_URIS = [
"https://app.example.com/callback",
"https://app.example.com/login",
]
def validate_redirect_uri(uri):
"""严格验证回调 URI"""
parsed = urlparse(uri)
# 方案必须一致
if parsed.scheme != 'https':
return False
# 路径必须精确匹配(不允许模糊匹配)
if uri not in ALLOWED_REDIRECT_URIS:
return False
# 不允许带有 fragment
if parsed.fragment:
return False
# 不允许带有非标准端口
if parsed.port and parsed.port not in [443]:
return False
return True3. 会话固定与会话劫持
会话固定(Session Fixation): 攻击者在用户登录前设置一个已知的会话 ID,用户登录后该会话 ID 未被重置,攻击者即可利用同一会话 ID 接管用户会话。
会话劫持(Session Hijacking): 攻击者通过窃取会话 Cookie 或 Token 冒充合法用户。
防护措施:
| 措施 | 说明 |
|---|---|
| 登录后重置会话 ID | 用户认证成功后调用 session_regenerate_id() 或等效方法 |
| 绑定会话与上下文 | 将会话绑定到客户端 IP、User-Agent 等信息 |
| 短 TTL 令牌 | 缩短会话和 Token 有效期,结合 Refresh Token 机制 |
| 令牌轮换(Token Rotation) | Refresh Token 每次使用后自动轮换,旧令牌立即失效 |
| 令牌吊销机制 | 提供后端吊销接口,支持主动终止可疑会话 |
4. SSO 会话固定攻击示例
// 不安全的 SSO 会话处理
session_start();
// 用户登录后未重置会话 ID
$_SESSION['user'] = $userData;
// 攻击者可事先设置 session_id 并在用户登录后使用同一 ID 访问
// 安全的 SSO 会话处理
session_start();
// 用户认证成功后必须重置会话 ID
if ($loginSuccess) {
session_regenerate_id(true); // 删除旧会话
$_SESSION['user'] = $userData;
$_SESSION['login_time'] = time();
$_SESSION['client_ip'] = $_SERVER['REMOTE_ADDR']; // 绑定客户端 IP
}SSO 单点登出(SLO)安全
SLO 的挑战
单点登出(Single Logout,SLO)是 SSO 中最容易被忽略的安全环节。用户在一个应用上登出后,IdP 和其他关联应用的会话应全部清除。
SLO 的协议实现
SAML SLO 流程:
- SP 发起登出请求到 IdP
- IdP 清除本地会话,同时向其他所有已登录的 SP 发起登出请求
- 各 SP 清除本地会话后返回确认
- IdP 返回登出完成页面给用户
OIDC RP-Initiated Logout:
GET /logout?
id_token_hint=<用户的 ID Token>&
post_logout_redirect_uri=https://app.example.com/logout-complete&
state=<防 CSRF 的随机值>SLO 安全风险
| 风险 | 说明 | 防护 |
|---|---|---|
| SLO 拒绝服务 | 恶意构造大量登出请求造成资源耗尽 | 限制登出请求频率;验证请求来源 |
| SLO 重定向攻击 | post_logout_redirect_uri 被篡改为恶意网站 | 使用白名单验证重定向目标 |
| 不完整的 SLO | 部分 SP 未收到登出通知,仍然保持有效会话 | 使用可靠的异步通知机制;设置全局会话超时 |
| 登出确认伪造 | 攻击者伪造 SP 的登出确认响应 | 验证登出请求中的签名和 Issuer |
SLO 安全配置建议
- 必须验证
id_token_hint:确保登出请求确实由持有该 Token 的用户发起 - 严格限制
post_logout_redirect_uri:使用白名单而非正则匹配 - 前端通道与后端通道结合:同时使用前端重定向和后端直接通信两种方式通知 SP
- 设置全局会话最大存活时间:即使 SLO 失败,过期的全局会话也应自动失效
IdP 发现与 Federation 安全
IdP 发现机制
在联邦 SSO 中,用户需要知道使用哪个 IdP 进行认证。常见的发现机制包括:
| 机制 | 描述 | 安全考量 |
|---|---|---|
| Home Realm Discovery(HRD) | 用户输入邮箱/域名,系统自动匹配 IdP | 需防止域名枚举攻击 |
| IdP 列表选择 | 展示所有可用的 IdP 供用户选择 | 需防范钓鱼 IdP 混入列表 |
| 域名提示(domain_hint) | SP 通过 domain_hint 参数指定 IdP | 需验证 hint 在允许范围内 |
| OrgID 方式 | 使用组织标识符直接确定 IdP | 标识符需防篡改 |
Federation 安全模型
显式信任 vs 隐式信任
- 显式信任:SP 和 IdP 在元数据中明确定义信任关系,所有端点地址和证书精确指定。推荐使用。
- 隐式信任:通过 Common Domain Cookie 或共享域建立信任,安全性较低。
安全 Federation 配置原则
- 元数据签名验证:IdP 和 SP 必须对元数据进行数字签名
- 证书轮换机制:建立证书过期预警和自动轮换流程
- 仅接受 HTTPS:所有 SAML/OIDC 端点必须使用 TLS 1.2+
- 实体 ID 唯一性:确保 Federation 中所有实体 ID 全局唯一,防止混淆攻击
<!-- Federation 元数据签名配置 -->
<EntitiesDescriptor Name="Example-Federation"
validUntil="2026-12-31T23:59:59Z"
xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<Signature>
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#example-federation">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>...</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>...</SignatureValue>
</Signature>
<!-- 各个 IdP 和 SP 的 EntityDescriptor -->
</EntitiesDescriptor>实际配置示例与最佳实践
Spring Security + OIDC SSO 完整配置
@Configuration
@EnableWebSecurity
public class OidcSsoConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/oauth2/authorization/messaging-client-oidc")
.successHandler((request, response, auth) -> {
OidcUser oidcUser = (OidcUser) auth.getPrincipal();
// 验证 nonce(Spring Security 自动处理)
// 验证 iss 和 aud
String issuer = oidcUser.getIssuer().toString();
if (!issuer.equals("https://accounts.example.com")) {
throw new RuntimeException("非法 Issuer: " + issuer);
}
// 验证 aud 包含当前客户端 ID
if (!oidcUser.getAudience().contains("messaging-client")) {
throw new RuntimeException("Audience 不匹配");
}
response.sendRedirect("/dashboard");
})
.failureHandler((request, response, exception) -> {
// 记录认证失败详情
log.warn("OIDC 认证失败: {}", exception.getMessage());
response.sendRedirect("/login?error=true");
})
)
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
)
.sessionManagement(session -> session
.sessionFixation().migrateSession()
.maximumSessions(1)
.expiredUrl("/login?expired=true")
)
// 强制 HTTPS
.requiresChannel(channel -> channel
.anyRequest().requiresSecure()
);
return http.build();
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
return (request, response, auth) -> {
// 构造 RP-Initiated Logout 请求
String idTokenHint = ""; // 从当前认证信息获取 ID Token
String postLogoutUri = "https://app.example.com/logout-complete";
String state = UUID.randomUUID().toString();
String logoutUrl = String.format(
"https://accounts.example.com/logout?id_token_hint=%s&" +
"post_logout_redirect_uri=%s&state=%s",
URLEncoder.encode(idTokenHint, StandardCharsets.UTF_8),
URLEncoder.encode(postLogoutUri, StandardCharsets.UTF_8),
state
);
response.sendRedirect(logoutUrl);
};
}
}SSO 最佳实践清单
1. 传输层安全
- 所有 IdP、SP 和认证端点强制使用 HTTPS(TLS 1.2+)
- 禁用不安全的 TLS 版本和弱密码套件
- 考虑使用 HSTS(HTTP Strict-Transport-Security)头部
2. 令牌与凭证管理
- 令牌有效期遵循最小化原则:ID Token 5-10 分钟,Access Token 15-60 分钟
- Refresh Token 必须支持轮换(Rotation)和吊销(Revocation)
- 客户端 Secret 不得存储在客户端代码中(公共客户端使用 PKCE)
- 令牌签名使用 RS256 或 ES256,避免使用
none算法
3. 协议级别防护
| 防护措施 | SAML | OIDC |
|---|---|---|
| 验证签名 | 必须验证 XML Signature | 必须验证 JWT Signature |
| 防止重放 | 使用 NotBefore/NotOnOrAfter | 使用 jti、iat、exp |
| 防 CSRF | 使用 RelayState 参数 | 使用 state 参数 |
| 请求绑定 | 优先使用 POST Binding | 使用授权码模式 + PKCE |
| 断言加密 | 敏感属性使用 XML Encryption | ID Token 使用 JWE |
4. 监控与审计
- 记录所有认证事件(成功/失败),包含时间戳、用户标识、来源 IP
- 监控异常模式:短时间内大量认证失败、异地登录、异常 User-Agent
- 配置实时告警机制,针对高敏感应用设置额外的 MFA 强制要求
- 定期审计注册的回调 URI、公钥证书和元数据配置
5. 实施检查清单
□ 所有认证端点使用 HTTPS(TLS 1.2+)
□ IdP 元数据已签名且定期轮换证书
□ 重定向 URI 使用精确匹配白名单
□ SAML 断言已签名,敏感属性已加密
□ OIDC 使用 PKCE(公共客户端)
□ state 和 nonce 参数得到正确验证
□ 会话在登录后已重置(regenerate)
□ SLO 实现了完整登出所有关联应用
□ post_logout_redirect_uri 白名单验证
□ 令牌有效期遵循最小化原则
□ Refresh Token 支持轮换和吊销
□ 所有 XML 解析器禁用了 DTD 和外部实体
□ 已实施认证事件监控和异常告警总结
SSO 单点登录在提升用户体验和降低密码管理成本的同时,也引入了集中的攻击面。在实际设计和实施中,需要平衡便利性与安全性:
- 协议选择:新系统优先选择 OIDC,旧系统兼容 SAML 2.0
- 传输加密:所有通道强制使用 HTTPS,端点严格绑定域名
- 输入验证:对所有回调参数进行白名单验证,防止开放重定向
- 签名与加密:令牌必须签名,敏感数据应加密
- 会话管理:登录后重置会话 ID,绑定上下文信息
- 完整登出:SLO 确保所有关联应用会话同时清除
- 持续监控:记录认证日志,设置异常检测和告警
SSO 安全不是一次性的配置工作,而是需要持续维护和审计的长期过程。随着身份认证技术的演进,开发者应当持续关注最新的安全实践和协议更新,确保 SSO 系统的安全健壮性。