.NET API+React/Flutter跨平台认证令牌安全管理方案咨询
适配React与Flutter的.NET API令牌管理最优方案
针对你同时开发.NET API、React网页端和Flutter移动端的场景,最优方案是双认证模式兼容——既保留网页端Cookie的安全优势,又适配移动端的Bearer令牌需求,完美平衡安全性与跨平台易用性。
核心思路
- 网页端:使用HTTP-only Cookie存储刷新令牌(防XSS攻击),访问令牌存在内存中(避免持久化风险)
- 移动端:使用Bearer令牌模式,访问令牌存内存,刷新令牌加密存储在系统安全容器中
- .NET API:同时支持Cookie认证和JWT Bearer认证,根据请求来源自动适配
React网页端实现
令牌管理逻辑
- 登录:调用API登录接口时带上
credentials: 'include',API通过Set-Cookie设置HTTP-only刷新令牌,响应体返回访问令牌,前端将访问令牌存在React Context/Redux内存中 - 请求:每次API请求将访问令牌放在
Authorization: Bearer {token}头中 - 刷新:访问令牌过期时,调用刷新接口(自动携带HTTP-only Cookie中的刷新令牌),API返回新的访问令牌后更新内存中的值
- 退出:调用退出接口,API清除Cookie中的刷新令牌,前端清空内存中的访问令牌
关键安全点
- 禁止将访问令牌存在
localStorage,避免XSS攻击窃取 - 生产环境强制开启Cookie的
Secure和SameSite=Lax/Strict属性
示例代码(React)
import { createContext, useContext, useState } from 'react'; const AuthContext = createContext(); export function AuthProvider({ children }) { const [accessToken, setAccessToken] = useState(null); // 登录逻辑 const login = async (email, password) => { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), credentials: 'include' // 自动携带/接收Cookie }); const data = await res.json(); setAccessToken(data.accessToken); }; // 刷新令牌逻辑 const refreshToken = async () => { const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' }); const data = await res.json(); setAccessToken(data.accessToken); return data.accessToken; }; return ( <AuthContext.Provider value={{ accessToken, login, refreshToken }}> {children} </AuthContext.Provider> ); } export const useAuth = () => useContext(AuthContext);
Flutter移动端实现
令牌管理逻辑
- 登录:调用API登录接口,直接获取响应体中的访问令牌和刷新令牌;访问令牌存在内存(全局变量或状态管理),刷新令牌用
flutter_secure_storage加密存储(系统密钥链/Keystore) - 请求:每次API请求将访问令牌放在
Authorization: Bearer {token}头中 - 刷新:访问令牌过期时,从安全存储取出刷新令牌,调用刷新接口传递令牌,获取新的访问令牌和刷新令牌后更新内存和安全存储
- 退出:调用退出接口,清除安全存储中的刷新令牌和内存中的访问令牌
关键安全点
- 必须用加密存储插件存储刷新令牌,禁止用
SharedPreferences明文存储 - 访问令牌仅存内存,APP重启后重新登录或刷新获取
示例代码(Flutter)
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; final storage = FlutterSecureStorage(); String? _accessToken; // 登录 Future<void> login(String email, String password) async { final response = await http.post( Uri.parse('https://your-api.com/api/auth/login'), body: {'email': email, 'password': password}, ); final data = jsonDecode(response.body); _accessToken = data['accessToken']; await storage.write(key: 'refreshToken', value: data['refreshToken']); } // 刷新令牌 Future<String?> refreshToken() async { final refreshToken = await storage.read(key: 'refreshToken'); if (refreshToken == null) return null; final response = await http.post( Uri.parse('https://your-api.com/api/auth/refresh'), body: {'refreshToken': refreshToken}, ); final data = jsonDecode(response.body); _accessToken = data['accessToken']; await storage.write(key: 'refreshToken', value: data['refreshToken']); // 令牌旋转 return _accessToken; } // 带令牌的请求 Future<http.Response> authenticatedGet(String url) async { return http.get( Uri.parse(url), headers: {'Authorization': 'Bearer $_accessToken'}, ); }
.NET API适配配置
双认证模式配置
同时启用Cookie认证和JWT Bearer认证,允许两种方式通过授权校验:
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // 添加认证服务 builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.Cookie.Name = "RefreshToken"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 生产环境强制HTTPS options.Cookie.SameSite = SameSiteMode.Lax; options.ExpireTimeSpan = TimeSpan.FromDays(7); options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = async context => { // 校验刷新令牌有效性(示例:从数据库查询) var refreshToken = context.Principal?.FindFirst("RefreshToken")?.Value; if (string.IsNullOrEmpty(refreshToken) || !await ValidateRefreshToken(refreshToken)) { context.RejectPrincipal(); await context.HttpContext.SignOutAsync(); } } }; }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, 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( System.Text.Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])) }; }); // 添加授权服务 builder.Services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); }); // 跨域配置(针对React网页端) builder.Services.AddCors(options => { options.AddPolicy("ReactCors", policy => { policy.WithOrigins("https://your-react-app.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // 允许携带Cookie }); }); var app = builder.Build(); // 启用中间件 app.UseHttpsRedirection(); app.UseCors("ReactCors"); app.UseAuthentication(); app.UseAuthorization(); // 登录接口示例 app.MapPost("/api/auth/login", async (LoginRequest req, HttpContext context, ITokenService tokenService) => { // 校验用户凭证 var user = await _userService.ValidateUser(req.Email, req.Password); if (user == null) return Results.Unauthorized(); // 生成令牌 var accessToken = tokenService.GenerateAccessToken(user); var refreshToken = tokenService.GenerateRefreshToken(user); // 网页端:设置HTTP-only Cookie if (context.Request.Headers["User-Agent"].ToString().Contains("Mozilla")) { context.Response.Cookies.Append("RefreshToken", refreshToken, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, Expires = DateTime.UtcNow.AddDays(7) }); return Results.Ok(new { AccessToken = accessToken }); } // 移动端:返回双令牌 else { return Results.Ok(new { AccessToken = accessToken, RefreshToken = refreshToken }); } }); // 刷新令牌接口示例 app.MapPost("/api/auth/refresh", async (HttpContext context, ITokenService tokenService) => { string? refreshToken; // 从Cookie或请求体获取刷新令牌 if (context.Request.Cookies.ContainsKey("RefreshToken")) { refreshToken = context.Request.Cookies["RefreshToken"]; } else { var body = await context.Request.ReadFromJsonAsync<RefreshRequest>(); refreshToken = body?.RefreshToken; } if (string.IsNullOrEmpty(refreshToken)) return Results.Unauthorized(); // 校验刷新令牌并获取用户信息 var user = await tokenService.ValidateRefreshToken(refreshToken); if (user == null) return Results.Unauthorized(); // 生成新令牌 var newAccessToken = tokenService.GenerateAccessToken(user); var newRefreshToken = tokenService.GenerateRefreshToken(user); // 更新Cookie或返回新令牌 if (context.Request.Cookies.ContainsKey("RefreshToken")) { context.Response.Cookies.Append("RefreshToken", newRefreshToken, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, Expires = DateTime.UtcNow.AddDays(7) }); return Results.Ok(new { AccessToken = newAccessToken }); } else { return Results.Ok(new { AccessToken = newAccessToken, RefreshToken = newRefreshToken }); } }); app.Run();
关键安全增强
- 刷新令牌旋转:每次刷新返回新的刷新令牌,旧令牌失效,降低泄露风险
- 刷新令牌绑定用户ID和设备标识,存储在数据库中,校验时核对
- 访问令牌设置短有效期(15-30分钟),减少泄露影响范围
安全与易用性总结
- 安全性:网页端用HTTP-only Cookie防XSS,移动端用系统加密存储防本地窃取;令牌旋转、短有效期进一步降低风险
- 易用性:前端无需复杂适配,网页端依赖浏览器自动处理Cookie,移动端用标准Bearer模式;API统一接口,仅需根据请求来源调整返回逻辑
内容的提问来源于stack exchange,提问作者Bunny
相关产品推荐
相关产品推荐

