angular - 将 JWT token 存储到 HttpOnly cookie 中

标签 angular asp.net-core cookies jwt identityserver4

我读了几篇文章说本地存储不是存储 JWT token 的首选方式,因为它不适合用于 session 存储,因为您可以通过 JavaScript 代码轻松访问它,这可能会导致 XSS 本身,如果有的话易受攻击的第三方库或其他东西。

从这些文章中总结,正确的方法是使用 HttpOnly cookie 而不是本地存储 session /敏感信息。

问题一

我找到了一种 cookie 服务,就像我目前用于本地存储的服务一样。我不清楚的是 expires=Thu, 1 Jan 1990 12:00:00 UTC;路径=/;`。它真的必须在某个时候过期吗?我只需要存储我的 JWT 和刷新 token 。全部信息都在里面。

import { Injectable } from '@angular/core';

/**
 * Handles all business logic relating to setting and getting local storage items.
 */
@Injectable({
  providedIn: 'root'
})
export class LocalStorageService {
  setItem(key: string, value: any): void {
    localStorage.setItem(key, JSON.stringify(value));
  }

  getItem<T>(key: string): T | null {
    const item: string | null = localStorage.getItem(key);
    return item !== null ? (JSON.parse(item) as T) : null;
  }

  removeItem(key: string): void {
    localStorage.removeItem(key);
  }
}
import { Inject, Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root',
  })
export class AppCookieService {
    private cookieStore = {};

    constructor() {
        this.parseCookies(document.cookie);
    }

    public parseCookies(cookies = document.cookie) {
        this.cookieStore = {};
        if (!!cookies === false) { return; }
        const cookiesArr = cookies.split(';');
        for (const cookie of cookiesArr) {
            const cookieArr = cookie.split('=');
            this.cookieStore[cookieArr[0].trim()] = cookieArr[1];
        }
    }

    get(key: string) {
        this.parseCookies();
        return !!this.cookieStore[key] ? this.cookieStore[key] : null;
    }

    remove(key: string) {
      document.cookie = `${key} = ; expires=Thu, 1 jan 1990 12:00:00 UTC; path=/`;
    }

    set(key: string, value: string) {
        document.cookie = key + '=' + (value || '');
    }
}

问题2

查看注销函数 signOut()。在后端撤销JWT token(后端额外订阅)不是更好的做法吗?

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { map, Observable, of } from 'rxjs';

import { JwtHelperService } from '@auth0/angular-jwt';
import { environment } from '@env';
import { LocalStorageService } from '@core/services';
import { AuthResponse, User } from '@core/types';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly ACTION_URL = `${environment.apiUrl}/Accounts/token`;

  private jwtHelperService: JwtHelperService;

  get userInfo(): User | null {
    const accessToken = this.getAccessToken();
    return accessToken ? this.jwtHelperService.decodeToken(accessToken) : null;
  }

  constructor(
    private httpClient: HttpClient,
    private router: Router,
    private localStorageService: LocalStorageService
  ) {
    this.jwtHelperService = new JwtHelperService();
  }

  signIn(credentials: { username: string; password: string }): Observable<AuthResponse> {
    return this.httpClient.post<AuthResponse>(`${this.ACTION_URL}/create`, credentials).pipe(
      map((response: AuthResponse) => {
        this.setUser(response);
        return response;
      })
    );
  }

  refreshToken(): Observable<AuthResponse | null> {
    const refreshToken = this.getRefreshToken();
    if (!refreshToken) {
      this.clearUser();
      return of(null);
    }

    return this.httpClient.post<AuthResponse>(`${this.ACTION_URL}/refresh`, { refreshToken }).pipe(
      map((response) => {
        this.setUser(response);
        return response;
      })
    );
  }

  signOut(): void {
    this.clearUser();
    this.router.navigate(['/auth']);
  }

  getAccessToken(): string | null {
    return this.localStorageService.getItem('accessToken');
  }

  getRefreshToken(): string | null {
    return this.localStorageService.getItem('refreshToken');
  }

  hasAccessTokenExpired(token: string): boolean {
    return this.jwtHelperService.isTokenExpired(token);
  }

  isSignedIn(): boolean {
    return this.getAccessToken() ? true : false;
  }

  private setUser(response: AuthResponse): void {
    this.localStorageService.setItem('accessToken', response.accessToken);
    this.localStorageService.setItem('refreshToken', response.refreshToken);
  }

  private clearUser() {
    this.localStorageService.removeItem('accessToken');
    this.localStorageService.removeItem('refreshToken');
  }
}

问题3

我的后端是 ASP.NET Core 5,我使用的是 IdentityServer4。我不确定是否必须让后端验证 cookie 或者它是如何工作的?

services.AddIdentityServer(options =>
{
    options.Events.RaiseErrorEvents = true;
    options.Events.RaiseInformationEvents = true;
    options.Events.RaiseFailureEvents = true;
    options.Events.RaiseSuccessEvents = true;
    
    options.EmitStaticAudienceClaim = true;
})
    .AddDeveloperSigningCredential()
    .AddInMemoryIdentityResources(Configuration.GetIdentityResources())
    .AddInMemoryApiScopes(Configuration.GetApiScopes(configuration))
    .AddInMemoryApiResources(Configuration.GetApiResources(configuration))
    .AddInMemoryClients(Configuration.GetClients(configuration))
    .AddCustomUserStore();

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = configuration["AuthConfiguration:ClientUrl"];
        options.RequireHttpsMetadata = false;
        options.RoleClaimType = "role";
        
        options.ApiName = configuration["AuthConfiguration:ApiName"];
        options.SupportedTokens = SupportedTokens.Jwt;
        options.JwtValidationClockSkew = TimeSpan.FromTicks(TimeSpan.TicksPerMinute);
    });

最佳答案

  1. 您希望后端使用刷新 token 设置 HttpOnly cookie。因此,您将拥有一个 POST 端点,您可以在其中发布您的用户凭据,并且该端点在 HttpOnly cookie 中返回刷新 token ,并且 accessToken 可以作为常规 JSON 属性在请求正文中返回。 下面是如何设置 cookie 响应的示例:

        var cookieOptions = new CookieOptions
        {
            HttpOnly = true,
            Expires = DateTime.UtcNow.AddDays(7),
            SameSite = SameSiteMode.None,
            Secure = true
        };
        Response.Cookies.Append("refreshToken", token, cookieOptions);
    
  2. 一旦您拥有带刷新 token 的 HttpCookie,您就可以将其传递到专用 API 端点以轮换访问 token 。该端点实际上还可以将刷新 token 旋转为 security best practice . 以下是检查请求中是否包含 HttpCookie 的方法:

        var refreshToken = Request.Cookies["refreshToken"];
        if (string.IsNullOrEmpty(refreshToken))
        {
            return BadRequest(new { Message = "Invalid token" });
        }
    
  3. 您的访问 token 应该是短暂的,例如 15-20 分钟。这意味着您希望在它过期前不久主动轮换它,以确保经过身份验证的用户不会注销。您可以使用 setInterval JavaScript 中的函数来构建此刷新功能。

  4. 您的刷新 token 可以存在更长时间,但它不应该不会过期。此外,如第 2 点所述,在访问 token 刷新时轮换刷新 token 是一个非常好的主意。

  5. 您的访问 token 不需要像本地/ session 存储或 cookie 那样存储在任何地方。您可以简单地将它保存在某个 SPA 服务中,只要不重新加载单个页面,该服务就会存在。如果它由用户重新加载,您只需在初始加载期间轮换 token (记住 HttpOnly cookie 固定在您的域中,并且可以作为资源提供给您的浏览器),一旦您拥有访问 token ,您就可以将其放入每个后端请求的授权 header .

  6. 您需要在某处(关系数据库或键值存储)持久保存已发布的刷新 token ,以便能够验证它们、跟踪过期情况并在需要时撤销。

关于angular - 将 JWT token 存储到 HttpOnly cookie 中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68777033/

相关文章:

javascript - Angular 2/4 字符串比较与忽略大小写

javascript - 使用带有 &lt;input&gt; 标签的 Angular 进行多选下拉菜单

angular - 即使在使用 .NET Core 和 Angular 客户端启用它之后,CORS 错误仍然会发生

javascript - 为什么我的 ASP.NET CORE React Redux 应用程序中的记录没有更新?

php - 在没有 httponly 标志的情况下创建的 Cookie XSRF-TOKEN - Laravel 5.8

ios - 在 iOS5 中让 cookie 与 PhoneGap 一起使用吗?

javascript - 如何将polyfills和外部库与Rollup JS捆绑在一起?

javascript - 如何以 Angular 对html表的每一行进行组件化

c# - 如何将数据从 AuthorizationHandler 传递到 Asp.net Core 中的 Controller

ios - Xamarin.iOS 相当于 Xamarin.Android 的 CookieManager.Instance