PAYATHON 2026

.NET 9 + Angular 20 如何安全存储 accessToken 和 refreshToken?

数据小许

建议由服务端管理认证状态,浏览器持有受保护的认证 Cookie。如果后端需要使用 accessToken 和 refreshToken 调用其他 API,可以由后端保存和使用这些 token,Angular 只访问自己的后端。

把两个 token 合并进一个 Cookie,没有明确的安全收益;额外增加一个 session ID Cookie,也不会自动提高安全性。需要考虑凭据的用途、服务端验证方式、Cookie 属性,以及刷新和撤销流程。

两个 token 合并到一个 Cookie,能解决什么问题?

合并会改变数据的组织方式,但 accessToken 和 refreshToken 的职责仍然不同:前者用于访问资源,后者用于换取新的访问凭据。放在一起后,两者会共用 Cookie 的发送范围和浏览器过期时间,后端仍须分别检查各自的有效期。

加密可以隐藏 token 内容,但如果服务端接受整个 Cookie 作为认证凭据,攻击者窃取 Cookie 后仍可能直接重放它,无须先解密。因此,看不到 token 明文,不代表 Cookie 被盗后无法使用。

如果已有 session ID,而且后端可以据此取回 token,通常不必再把 token 副本发给浏览器。也不要自行设计 token 拼接、加密和签名协议。

应该选择服务端会话,还是直接存 token?

对于由自己控制前后端的应用,可以根据后端是否需要访问其他资源服务来选择:

  • Angular 只访问自己的 .NET 后端:可以使用 ASP.NET Core Cookie Authentication,未必需要为浏览器额外引入 accessToken 和 refreshToken。
  • 后端需要代表用户访问其他 API:可以采用 Backend for Frontend(BFF)模式,由后端保存、刷新 token,并转发受控的 API 请求。
  • 浏览器必须直接调用独立资源服务:需要另行设计浏览器 OAuth 流程,不能直接套用“所有 token 留在后端”的方案。

服务端会话便于集中撤销登录状态,也能减少浏览器接触上游 token 的机会,但需要维护会话存储、过期清理和多实例一致性。session ID 本身仍是敏感凭据,泄露后同样可能被冒用。

ASP.NET Core 默认的认证 Cookie 保存的是受 Data Protection 保护的认证票据,并非只保存 session ID。要让 Cookie 引用服务端票据,可以通过 CookieAuthenticationOptions.SessionStore 接入 ITicketStore;仅调用 AddSession() 不会完成这项认证改造。SessionStore 官方说明

以下是现有 ASP.NET Core 项目中的基础配置片段,假设 Angular 与后端通过同一 HTTPS origin 对外提供服务:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "__Host-session";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.Path = "/";

        options.Events.OnRedirectToLogin = context =>
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            return Task.CompletedTask;
        };
        options.Events.OnRedirectToAccessDenied = context =>
        {
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
            return Task.CompletedTask;
        };
    });

builder.Services.AddAuthorization();
builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-XSRF-TOKEN";
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
builder.Services.AddControllers(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

应用管线还需在映射控制器前调用 app.UseAuthentication() 和 app.UseAuthorization(),需要登录的接口使用 [Authorize]。身份校验成功后,通过 SignInAsync 签发认证 Cookie。示例将认证失败配置为 401、权限不足配置为 403,适用于 API 请求,可以避免前端收到登录页面重定向。Cookie Authentication 官方说明

这段配置尚未接入 ITicketStore,也没有实现登录、token 存储或刷新逻辑。若采用服务端会话,需要实现并配置票据存储;多实例部署还要共享适当的会话存储和 Data Protection 密钥。

__Host-session 使用浏览器识别的 __Host- 前缀,要求 Secure、Path=/,且不得设置 Domain。原先的 __session 只是普通名称,没有这种前缀约束。HttpOnly 限制 JavaScript 读取 Cookie,但无法阻止 XSS 代码借当前用户身份发送请求。OWASP 会话安全说明

Angular 如何配合 CSRF 防护?

浏览器会自动将认证 Cookie 附加到符合条件的请求中,因此需要保护修改状态的接口。SameSite 可以减少部分风险,仍应配合服务端 antiforgery 验证。

Angular HttpClient 默认使用 XSRF-TOKEN Cookie 和 X-XSRF-TOKEN 请求头。同源部署并使用相对 API URL,最容易正确接入这套机制。后端必须签发并验证 token,单靠 Angular 添加请求头无法完成防护。Angular 安全文档

在前述服务配置基础上,可以添加一个签发请求 token 的端点:

app.MapGet("/auth/csrf", (
    HttpContext context,
    Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery) =>
{
    var tokens = antiforgery.GetAndStoreTokens(context);
    context.Response.Headers["Cache-Control"] = "no-store";
    context.Response.Cookies.Append(
        "XSRF-TOKEN",
        tokens.RequestToken!,
        new CookieOptions
        {
            HttpOnly = false,
            Secure = true,
            SameSite = SameSiteMode.Lax,
            Path = "/"
        });
    return Results.NoContent();
});

这里允许 JavaScript 读取的 XSRF-TOKEN 是 CSRF 请求 token,不是认证 Cookie,也不是 refreshToken。GetAndStoreTokens 管理的 antiforgery Cookie 与这个请求 token 配合使用。

前端需要在提交受保护请求前获取 CSRF token,并在登录成功或身份发生变化后重新获取。上面的全局过滤器用于 MVC 控制器;如果业务接口使用 Minimal API,需要另外接入适用的验证,例如显式调用 IAntiforgery.ValidateRequestAsync。ASP.NET Core antiforgery 文档

如果前后端跨 origin,不能假定 Angular 会自动给跨 origin 请求添加 XSRF 请求头。还需处理凭据请求、明确的 CORS origin 白名单和 CSRF token 传递。跨 origin 与跨 site 也不同;只有确实需要跨 site Cookie 时,才考虑 SameSite=None; Secure,并评估浏览器的第三方 Cookie 限制。

token 刷新和退出如何处理?

采用服务端保存 token 的方案时,登录后建立会话,并将 token 与该会话关联。普通请求由后端按需使用 accessToken,过期时由后端刷新,Angular 无须读取 refreshToken。

刷新逻辑应遵循身份服务的实际规则。如果支持 refresh token rotation,要保存新值并处理旧值失效,同时协调并发刷新,避免多个请求争用同一个 refreshToken。刷新失败或凭据被撤销时,应结束相应会话。

退出时,需要删除浏览器认证 Cookie,并使服务端会话失效;身份服务支持撤销时,再撤销相应 refreshToken。仅清除 Cookie 不能让已被复制的凭据立即失效。会话还应有服务端执行的空闲超时和绝对有效期,不能只依赖浏览器 Cookie 的过期时间。

如果暂时无法迁移到新方案,可以先确认浏览器是否确实需要持有 token,再完善 Cookie 属性、CSRF 验证和退出失效机制。单纯合并 Cookie,或增加一个尚未参与认证验证的 session ID,不会解决这些问题。

备注:内容仅供参考。