You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular前端+.NET 6后端:安全实现登录凭证免重复输入方案问询

安全实现“关闭页面后自动登录”的方案(Angular + .NET 6)

核心原则:绝对不存储明文/加密密码到前端

你最初考虑的localStorage存密码是严重的安全风险——一旦遭遇XSS攻击,密码会直接泄露,所以这个方案绝对不能用。下面是业界通用的安全实现方案,适配多客户端场景,兼顾安全性和用户体验。


这套方案的核心是用短期的Access Token处理接口授权,用长期的Refresh Token在页面重新进入时恢复登录状态,同时将敏感的Refresh Token放在HttpOnly Cookie中(前端无法读取,避免XSS窃取),后端持久化存储Refresh Token以支持多客户端。

一、.NET 6 后端实现步骤

  1. 配置身份验证与JWT
    在Program.cs中配置JWT身份验证,同时启用Cookie存储Refresh Token:

    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = builder.Configuration["Jwt:Issuer"],
                ValidAudience = builder.Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
            };
        })
        .AddCookie(options =>
        {
            options.Cookie.HttpOnly = true;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 生产环境必须启用HTTPS
            options.Cookie.SameSite = SameSiteMode.Strict;
            options.ExpireTimeSpan = TimeSpan.FromDays(7); // Refresh Token有效期
        });
    
  2. 实现登录接口,生成双Token
    登录时验证邮箱密码,生成短期Access Token(比如15分钟)和唯一的Refresh Token,将Refresh Token存储到数据库(关联用户ID、客户端标识如User Agent),并通过HttpOnly Cookie返回给前端:

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginModel model)
    {
        // 验证邮箱密码逻辑,获取用户实体
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
            return Unauthorized();
    
        // 生成Access Token
        var accessToken = GenerateAccessToken(user);
        // 生成唯一Refresh Token
        var refreshToken = GenerateRefreshToken();
        // 存储到数据库(可建RefreshToken表,关联UserId、Token、ExpiresAt、ClientId)
        await _refreshTokenRepository.CreateAsync(new RefreshToken
        {
            UserId = user.Id,
            Token = refreshToken,
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            ClientId = Request.Headers["User-Agent"].ToString()
        });
    
        // 设置HttpOnly Cookie存储Refresh Token
        Response.Cookies.Append("RefreshToken", refreshToken, new CookieOptions
        {
            HttpOnly = true,
            Secure = true,
            SameSite = SameSiteMode.Strict,
            Expires = DateTime.UtcNow.AddDays(7)
        });
    
        return Ok(new { AccessToken = accessToken });
    }
    
  3. 实现Refresh Token刷新接口
    前端页面重新进入时,调用此接口,后端验证Cookie中的Refresh Token合法性(检查有效期、用户关联、客户端标识),合法则返回新的Access Token:

    [HttpPost("refresh-token")]
    public async Task<IActionResult> RefreshToken()
    {
        var refreshToken = Request.Cookies["RefreshToken"];
        if (string.IsNullOrEmpty(refreshToken))
            return Unauthorized();
    
        var tokenRecord = await _refreshTokenRepository.GetByTokenAsync(refreshToken);
        if (tokenRecord == null || tokenRecord.ExpiresAt < DateTime.UtcNow)
            return Unauthorized();
    
        // 验证客户端标识,防止跨设备复用
        if (tokenRecord.ClientId != Request.Headers["User-Agent"].ToString())
            return Unauthorized();
    
        var user = await _userManager.FindByIdAsync(tokenRecord.UserId);
        var newAccessToken = GenerateAccessToken(user);
    
        // 可选:刷新Refresh Token(滚动更新)
        var newRefreshToken = GenerateRefreshToken();
        tokenRecord.Token = newRefreshToken;
        tokenRecord.ExpiresAt = DateTime.UtcNow.AddDays(7);
        await _refreshTokenRepository.UpdateAsync(tokenRecord);
    
        Response.Cookies.Append("RefreshToken", newRefreshToken, new CookieOptions
        {
            HttpOnly = true,
            Secure = true,
            SameSite = SameSiteMode.Strict,
            Expires = DateTime.UtcNow.AddDays(7)
        });
    
        return Ok(new { AccessToken = newAccessToken });
    }
    
  4. 多客户端支持
    数据库的RefreshToken表中每个记录对应一个客户端(通过ClientId区分),同一个用户可以存在多条记录,实现多设备同时登录,还能提供“注销所有设备”的功能(删除该用户的所有Refresh Token记录)。

二、Angular 前端实现步骤

  1. 登录流程处理
    登录时提交邮箱密码到后端,获取Access Token后存储在内存中(比如用一个AuthService的变量,不要存localStorage),同时后端自动设置HttpOnly Cookie:

    // auth.service.ts
    import { HttpClient } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    
    @Injectable({ providedIn: 'root' })
    export class AuthService {
      private accessToken: string | null = null;
    
      constructor(private http: HttpClient) {}
    
      login(email: string, password: string) {
        return this.http.post<{ AccessToken: string }>('/api/auth/login', { email, password }).subscribe(res => {
          this.accessToken = res.AccessToken;
          // 存储邮箱到localStorage,用于自动填充登录框(非敏感信息,安全)
          localStorage.setItem('savedEmail', email);
        });
      }
    
      refreshToken() {
        return this.http.post<{ AccessToken: string }>('/api/auth/refresh-token', {});
      }
    
      getAccessToken() {
        return this.accessToken;
      }
    
      setAccessToken(token: string) {
        this.accessToken = token;
      }
    }
    
  2. 页面初始化时恢复登录状态
    应用启动时(比如在AppComponent的ngOnInit中),自动调用后端的refresh-token接口,获取新的Access Token,恢复登录状态:

    // app.component.ts
    import { Component, OnInit } from '@angular/core';
    import { AuthService } from './auth.service';
    
    @Component({ selector: 'app-root', templateUrl: './app.component.html' })
    export class AppComponent implements OnInit {
      constructor(private authService: AuthService) {}
    
      ngOnInit() {
        // 尝试刷新Token恢复登录
        this.authService.refreshToken().subscribe({
          next: res => {
            this.authService.setAccessToken(res.AccessToken);
            // 从localStorage读取邮箱,自动填充登录框
            const savedEmail = localStorage.getItem('savedEmail');
            if (savedEmail) {
              // 可通过服务或双向绑定传递给登录组件的邮箱输入框
            }
          },
          error: () => {
            // 刷新失败,进入登录页面
          }
        });
      }
    }
    
  3. 接口请求携带Access Token
    用Angular的HttpInterceptor自动给所有请求添加Authorization: Bearer {token}头:

    // auth.interceptor.ts
    import { Injectable } from '@angular/core';
    import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { AuthService } from './auth.service';
    
    @Injectable()
    export class AuthInterceptor implements HttpInterceptor {
      constructor(private authService: AuthService) {}
    
      intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
        const token = this.authService.getAccessToken();
        if (token) {
          request = request.clone({
            setHeaders: {
              Authorization: `Bearer ${token}`
            }
          });
        }
        return next.handle(request);
      }
    }
    

三、关键安全注意事项

  • 绝对不存储敏感凭证到前端存储:密码、Access Token都不能存localStorage/sessionStorage,前者防XSS,后者避免页面刷新后泄露。
  • Refresh Token的安全防护:必须用HttpOnly、Secure、SameSite Cookie存储,后端要验证客户端标识,防止跨设备盗用。
  • Token过期策略:Access Token设短有效期(15-30分钟),Refresh Token设较长有效期(7-30天),同时支持滚动更新。
  • HTTPS强制启用:生产环境必须用HTTPS,否则Cookie的Secure属性无效,存在中间人攻击风险。
  • 用户主动注销:提供注销接口,删除后端的Refresh Token记录并清除Cookie。

内容的提问来源于stack exchange,提问作者user13925921

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 00:45:40