Angular前端+.NET 6后端:安全实现登录凭证免重复输入方案问询
核心原则:绝对不存储明文/加密密码到前端
你最初考虑的localStorage存密码是严重的安全风险——一旦遭遇XSS攻击,密码会直接泄露,所以这个方案绝对不能用。下面是业界通用的安全实现方案,适配多客户端场景,兼顾安全性和用户体验。
方案架构:HttpOnly Cookie + Refresh Token 模式
这套方案的核心是用短期的Access Token处理接口授权,用长期的Refresh Token在页面重新进入时恢复登录状态,同时将敏感的Refresh Token放在HttpOnly Cookie中(前端无法读取,避免XSS窃取),后端持久化存储Refresh Token以支持多客户端。
一、.NET 6 后端实现步骤
配置身份验证与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有效期 });实现登录接口,生成双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 }); }实现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 }); }多客户端支持
数据库的RefreshToken表中每个记录对应一个客户端(通过ClientId区分),同一个用户可以存在多条记录,实现多设备同时登录,还能提供“注销所有设备”的功能(删除该用户的所有Refresh Token记录)。
二、Angular 前端实现步骤
登录流程处理
登录时提交邮箱密码到后端,获取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; } }页面初始化时恢复登录状态
应用启动时(比如在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: () => { // 刷新失败,进入登录页面 } }); } }接口请求携带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

