65.9K
CodeProject 正在变化。 阅读更多。
Home

Angular 安全性 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (5投票s)

2018 年 7 月 16 日

CPOL

19分钟阅读

viewsIcon

12921

downloadIcon

22

为我们的 Angular 应用程序添加安全性的第 2 部分。

引言

本系列文章的第 1 部分 中,您创建了一组用于用户及用户身份验证/授权的 Angular 类。您使用这些类登录用户,并在类中创建了一组属性来控制菜单和按钮的显示/隐藏。在本篇文章中,您将学习如何针对 Web API 方法进行用户身份验证。该方法将返回一个与您在 Angular 中创建的类具有相同属性的授权对象。您还将学习如何使用 JSON Web Tokens (JWT) 来保护您的 Web API 方法。您将使用 [Authorize] 属性来保护您的方法,并学习如何添加安全策略。

入门应用程序

要跟随本文进行学习,请下载配套的 ZIP 文件。解压 ZIP 文件中的示例后,有一个 VS Code 工作区文件,您可以使用它来加载此应用程序中的两个项目。双击此工作区文件,解决方案将被加载,如图 1 所示。有两个项目;PTC 是 Angular 应用程序。PtcApi 是 ASP.NET Core Web API 项目。

在上一篇文章中,您完成的所有工作都在客户端。对于本文的入门应用程序,我预先构建了一个 ASP.NET Core Web API 项目,并将 ProductCategory 页面连接到了该 Web API 项目中的相应控制器。在本篇文章中,您将构建必要的安全类,以便将用户身份验证和授权信息返回给您的 Angular 应用程序。

Figure 1

图 1:入门应用程序有两个项目;Angular 项目 (PTC) 和 .NET Core Web API 项目 (PtcApi)。

创建应用程序安全类

在本系列文章的第 1 部分中,您创建了 Angular 类来表示用户和用户安全对象,其中包含可以绑定到的属性。您需要在 PtcApi 项目中创建相同的 C# 类。您将使用与 TypeScript 中相同的名称和几乎相同的属性来构建 C# 类。

AppUser 类

AppUser 类是用户类的简化版本,仅包含 UserNamePassword 属性。在本篇文章中,我将保持类的简单性,并且不使用数据库,我将只使用模拟数据,就像我在本系列文章的第 1 部分中所做的那样。这有助于您专注于事物的工作原理,而无需担心大量配置。别担心,您将在文章中使用真实的 SQL 表。右键单击“Model”文件夹,然后添加一个名为 AppUser.cs 的新文件。将以下代码添加到此文件中。

namespace PtcApi.Model
{
  public class AppUser
  {
    public int UserId { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
  }
}

创建 AppUserAuth 类

AppUserAuth 类具有表示用户安全配置文件的属性。同样,为了保持简单,我列出了可以直接绑定到 Angular UI 元素的单个属性。在未来的文章中,我将探讨如何将角色和声明/权限与此处介绍的安全系统结合使用。右键单击“Model”文件夹,然后添加一个名为 AppUserAuth.cs 的新文件。将以下代码添加到此文件中。

namespace PtcApi.Model
{
  public class AppUserAuth
  {
    public AppUserAuth() : base()
    {
      UserName = string.Empty;
      BearerToken = string.Empty;
      IsAuthenticated = false;
      CanAccessProducts = false;
      CanAddProduct = false;
      CanSaveProduct = false;
      CanAccessCategories = false;
      CanAddCategory = false;
    }

    public string UserName { get; set; }
    public string BearerToken { get; set; }
    public bool IsAuthenticated { get; set; }
    public bool CanAccessProducts { get; set; }
    public bool CanAddProduct { get; set; }
    public bool CanSaveProduct { get; set; }
    public bool CanAccessCategories { get; set; }
    public bool CanAddCategory { get; set; }
  }
}

构建 Security Manager 类

与其在 Web API 控制器类中构建模拟数据并创建用户安全对象,不如将该代码放在另一个类中。右键单击 PtcApi 项目,然后创建一个名为 Security 的新文件夹。右键单击新的 Security 文件夹,然后添加一个名为 SecurityManager.cs 的新文件。将以下代码添加到此文件中。

using System;
using System.Collections.Generic;
using System.Linq;
using PtcApi.Model;

namespace PtcApi.Security
{
  public class SecurityManager
  {
    private AppUser _user = null;
    private AppUserAuth _auth = null;
  }
}

添加 CreateMockUsers() 方法

SecurityManager 类添加一个方法来创建一组模拟用户。您将根据此用户集合检查从 Angular 应用程序提交的用户凭据。

private List<appuser> CreateMockUsers()
{
  List<appuser> list = new List<appuser>();

  list.Add(new AppUser()
  {
    UserId = 1,
    UserName = "PSheriff",
    Password = "P@ssw0rd"
  });
  list.Add(new AppUser()
  {
    UserId = 2,
    UserName = "Bjones",
    Password = "P@ssw0rd"
  });

  return list;
}

编写 CreateMockSecurityObjects() 方法

如果您阅读了本系列文章的第 1 部分,以下方法应该类似于 PTC Angular 应用程序中创建的 LOGIN_MOCKS 常量。添加一个名为 CreateMockSecurityObjects() 的方法来构建一个 AppUserAuth 对象列表,其中为每个用户填充了不同的属性。

private List<appuserauth> CreateMockSecurityObjects()
{
  List<appuserauth> list = new List<appuserauth>();

  list.Add(new AppUserAuth()
  {
    UserName = "PSheriff",
    BearerToken = "abi393kdkd9393ikd",
    IsAuthenticated = true,
    CanAccessProducts = true,
    CanAddProduct = true,
    CanSaveProduct = true,
    CanAccessCategories = true,
    CanAddCategory = false
  });
  list.Add(new AppUserAuth()
  {
    UserName = "Bjones",
    BearerToken = "sd9f923k3kdmcjkhd",
    IsAuthenticated = true,
    CanAccessProducts = true,
    CanAddProduct = false,
    CanSaveProduct = false,
    CanAccessCategories = true,
    CanAddCategory = true
  });

  return list;
}

在上面的代码中,我已将一个值硬编码到 BearerToken 属性中。稍后在本篇文章中,您将使用 JWT 生成一个真实的 bearer token。

编写 BuildUserAuthObject 方法

当用户登录 Angular 应用程序时,用户名和密码会以 AppUser 对象的形式发送到一个控制器。该用户对象将被传递到您将在稍后编写的 ValidateUser() 方法中。编写一个名为 BuildUserAuthObject() 的方法,该方法将从 ValidateUser() 方法调用,以构建用户的安全对象。

此方法检查以确保名为 _user 的私有字段变量不为 null。如果不是,它会创建模拟安全对象列表,并在列表中搜索匹配传入的用户名的一个对象。如果找到该对象,则将其分配给 _auth 字段。如果未找到用户安全对象,则创建 AppUserAuth 类的新实例并将其分配给 _auth 字段。返回到 Angular 应用程序的值就是此 _auth 字段中的值。

protected void BuildUserAuthObject()
{
  if (_user != null)
  {
    _auth = CreateMockSecurityObjects()
      .Where(u => u.UserName ==
             _user.UserName).FirstOrDefault();
  }

  if (_auth == null)
  {
    _auth = new AppUserAuth();
  }
}

添加 ValidateUser 方法

ValidateUser() 方法是 SecurityManager 类中唯一暴露的方法。您将要创建的接受来自 Angular 应用程序的 AppUser 对象的安全控制器会调用此方法,并将该用户对象传递进来以验证用户。

ValidateUser() 对象将传入的用户分配给 _user 字段。用户将根据模拟用户列表进行验证,以确保用户名和密码与列表中的某个用户匹配。然后调用 BuildUserAuthObject() 方法来构建一个 AppUserAuth 对象并将其返回给控制器。

public AppUserAuth ValidateUser(AppUser user)
{
  // Assign current user
  _user = user;

  // Attempt to validate user
  _user = CreateMockUsers().Where(
    u => u.UserName.ToLower() == _user.UserName.ToLower()
    && u.Password == _user.Password).FirstOrDefault();

  // Build User Security Object
  BuildUserAuthObject();

  return _auth;
}

添加 Security Controller

现在是时候构建 Angular 应用程序调用的安全控制器了。Angular 以 AppUser 对象的形式传递用户凭据。安全控制器返回一个 AppUserAuth 对象,其中包含适当设置的属性。右键单击“Controllers”文件夹,然后添加一个名为 SecurityController.cs 的新文件。将以下代码添加到此文件中。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PtcApi.Security;
using PtcApi.Model;

namespace PtcApi.Controllers
{
  [Route("api/[controller]")]
  public class SecurityController : Controller
  {

  }
}

在此新的控制器类中,添加一个接受 AppUser 对象的 Login() 方法。创建 SecurityManager 的实例,并将该 AppUser 对象传递给 ValidateUser() 方法。如果用户通过身份验证后返回,则 IsAuthenticated 属性将为 true。将 ret 变量设置为由 StatusCode() 方法生成的 IActionResult。将状态码 200 和用户安全对象的有效负载返回给 Angular 应用程序。如果用户未通过身份验证,则返回状态码 404。

[HttpPost("login")]
public IActionResult Login([FromBody]AppUser user)
{
  IActionResult ret = null;
  AppUserAuth auth = new AppUserAuth();
  SecurityManager mgr = new SecurityManager();

  auth = (AppUserAuth)mgr.ValidateUser(user);
  if (auth.IsAuthenticated)
  {
    ret = StatusCode(StatusCodes.Status200OK, auth);
  }
  else
  {
    ret = StatusCode(StatusCodes.Status404NotFound, 
                     "Invalid User Name/Password.");
  }

  return ret;
}

调用 Web API

要调用 SecurityControllerLogin() 方法,请使用 Angular 中的 HttpClient 服务。返回到 PTC 项目,并打开位于 \src\app\security 文件夹中的 security.service.ts 文件,然后添加以下 import 语句。

import {HttpClient, HttpHeaders} from '@angular/common/http';
import { tap } from 'rxjs/operators';

import 语句下方添加两个常量。第一个常量是 Web API 控制器的位置。第二个常量保存将数据 POST 到 Web API 调用所需的 HttpClient 的标头选项。

const API_URL = "https://:5000/api/security/";

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type': 'application/json'
  })
};

要在安全服务类中使用 HttpClient 服务,请修改构造函数,告知 Angular 注入 HttpClient。依赖注入在 Angular 中被广泛使用,以便将 HttpClient 等服务提供给不同的组件。HttpClientModule 已在 AppModule 类中导入。必须导入此模块才能允许组件使用 HttpClient 服务。

constructor(private http: HttpClient) { }

删除 login() 方法中的硬编码逻辑,并进行 Web API 调用。修改 login() 方法以调用 SecurityController 登录方法。如果从该调用返回有效数据,则使用 pipe() 方法来访问管道。您可以检索返回的数据并将其分配给 securityObject 属性。

在为 securityObject 属性赋值时,切勿使用等号。如果这样做,securityObject 的绑定引用将被擦除,并且绑定到 securityObject 属性的菜单和其他 UI 元素将不再工作。而是使用 Object.assign() 方法将数据从一个对象复制到另一个对象。

此方法还将获取 bearer token 并将其存储在本地存储中。这样做的原因将在本文稍后学习构建 HTTP Interceptor 类时变得显而易见。

login(entity: AppUser): Observable<appuserauth> {
  // Initialize security object
  this.resetSecurityObject();


  return this.http.post<AppUserAuth>(API_URL + "login", entity, httpOptions).pipe(
    tap(resp => {
      // Use object assign to update the current object
      // NOTE: Don't create a new AppUserAuth object
      //       because that destroys all references to object
      Object.assign(this.securityObject, resp);
      // Store into local storage
      localStorage.setItem("bearerToken", this.securityObject.bearerToken);
    }));
}

修改 Login Component

由于 Web API 可能返回 404: Not Found 错误,因此请务必处理此状态码,以便显示“Invalid User Name/Password”错误消息。打开 login.component.ts 文件,并在 subscribe() 方法中添加错误处理块。登录 HTML 包含一个 Bootstrap 警报,其中有一个带有“Invalid User Name/Password”字样的 <p> 标签。此警报仅在 securityObject 不为 nullisAuthenticated 属性设置为 false 时显示。

login() {
  this.securityService.login(this.user)
    .subscribe(resp => {
      this.securityObject = resp;
      if (this.returnUrl) {
        this.router.navigateByUrl(this.returnUrl);
      }
    },
    () => {
      // Initialize security object to display error message
      this.securityObject = new AppUserAuth();
    });
}

试试看

保存所有更改,然后开始调试 Web API 项目。通过在终端窗口中键入 npm start 来启动 Angular 应用程序。在浏览器中,尝试使用“psheriff”或“bjones”以及密码“P@ssw0rd”进行登录。如果一切顺利,Web API 现在将用于验证用户名和密码。尝试使用无效的登录 ID 和密码登录,以确保错误处理正在工作。

授权访问 Web API 调用

现在您已经创建了这些 Web API 调用,您需要确保只有经过授权的人才能调用您的 API。在 .NET 中,您可以使用 [Authorize] 属性来保护控制器类或控制器类中的单个方法。但是,.NET 运行时中必须有一个身份验证/授权组件来为该属性提供数据。[Authorize] 属性必须能够读取该数据以决定用户是否具有调用该方法的权限。

有许多不同的身份验证/授权组件可供选择,例如 Microsoft Identity、OAuth 和 JSON Web Tokens (JWT) 等。在本文中,您将使用 JWT。

保护 Product Get() 方法

为了向您展示应用 Authorize 属性到方法时会发生什么,请打开 ProductController.cs 文件,并将 [Authorize] 属性添加到 Get() 方法。

[HttpGet]
[Authorize]
public IActionResult Get()
{
  // REST OF THE CODE
}

试试看

保存更改并运行 PtcApi 项目。以“psheriff”身份登录,然后单击“Products”菜单。不会显示任何产品数据,因为您尝试调用一个已被 [Authorize] 属性保护的方法。按 F12 键打开开发人员工具,您应该会看到类似图 2 的内容。

请注意,您收到的状态码是 500,而不是 401 (Unauthorized) 或 403 (Forbidden)。原因是您尚未向 .NET Core 注册身份验证服务。必须将 Microsoft Identity、OAuth 或 JWT 注册到 .NET Core,才能返回 401 而不是 500。

Figure 2

图 2:在使用 Authorize 属性但未注册身份验证服务时,您会收到状态码 500。

添加 JWT 配置

要使用 JSON Web Token 系统,您必须执行几个步骤:

  • 添加 JWT 包。
  • 添加 JWT bearer token 检查包。
  • 在配置文件中存储默认 JWT 设置。
  • 将 JWT 注册为身份验证服务。
  • 向 bearer token 添加选项以验证传入的 token。
  • 构建 JWT token 并将其添加到用户安全对象中。

在 Web API 项目中,您首先要做的是添加一些包来使用 JSON Web Token 系统。打开 PtcApi 项目中的终端窗口,然后输入以下命令。

dotnet add package System.IdentityModel.Tokens.Jwt

添加 Bearer Token 检查

除了 Jwt 包之外,还添加一个包以确保 bearer token 从客户端传递。使用终端窗口中的以下命令将此包添加到 PtcApi 项目。

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

将 JWT 设置添加到配置

要签发 JwtToken,需要做几件事:

  • 用于对发送到客户端的数据进行哈希处理的密钥
  • token 发行者的名称
  • token 的目标受众
  • 允许 token 有效的分钟数

将 JWT 信息存储在 appsettings.json 中

您将需要在代码中的两个地方使用以上所有项:一次在 .NET Core Startup 类中配置 JWT,另一次在为特定用户生成新 token 时。您不想将数据硬编码到两个地方,因此请使用 .NET Core 配置系统从 PtcApi 项目根文件夹中的 appsettings.json 文件中检索此数据。

打开 appsettings.json 文件,并添加一个名为 JwtSettings 的新条目。将以下属性和值添加到此 JwtSettings 对象中。

{
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "JwtSettings": {
    "key": "This*Is&A!Long)Key(For%Creating@A$SymmetricKey",
    "issuer": "https://:5000",
    "audience": "PTCUsers",
    "minutesToExpiration": "10"
  }
}

添加 JwtSettings 类

您可以使用 .NET core 中的配置系统来检索此数据,但我更喜欢使用一个类。右键单击 Security 文件夹,然后添加一个名为 JwtSettings.cs 的新文件,并添加以下代码。

public class JwtSettings {
  public string Key { get; set; }
  public string Issuer { get; set; }
  public string Audience { get; set; }
  public int MinutesToExpiration { get; set; }
}

读取设置

打开 Startup.cs 文件,并添加一个新方法,使用 .NET Core 配置对象读取这些设置,并创建一个新的 JwtSettings 类实例。

public JwtSettings GetJwtSettings() {
  JwtSettings settings = new JwtSettings();

  settings.Key = Configuration["JwtSettings:key"];
  settings.Audience = Configuration["JwtSettings:audience"];
  settings.Issuer = Configuration["JwtSettings:issuer"];
  settings.MinutesToExpiration = 
   Convert.ToInt32(
      Configuration["JwtSettings:minutesToExpiration"]);

  return settings;
}

创建 JWT 设置的单例

修改 ConfigureServices() 方法以创建 JwtSettings 类实例并调用 GetJwtSettings() 方法。创建此对象时,将其作为单例添加到 .NET core 服务中,以便您可以将其注入到任何控制器中。

public void ConfigureServices(IServiceCollection services)
{
  // Get JWT Token Settings from JwtSettings.json file
  JwtSettings settings;
  settings = GetJwtSettings();
  services.AddSingleton<JwtSettings>(settings);

  // REST OF THE CODE HERE
}

将 JWT 注册为身份验证提供程序

在此代码下方,您将把 JWT 注册为身份验证提供程序。使用安全密钥、发行者、受众和过期分钟数来配置 JWT 的设置。此对象确保从 Angular 传递的 bearer token 有效。

public void ConfigureServices(IServiceCollection services)
{
  // Get JWT Token Settings from JwtSettings.json file
  JwtSettings settings;
  settings = GetJwtSettings();
  services.AddSingleton<JwtSettings>(settings);

  // Register Jwt as the Authentication service
  services.AddAuthentication(options =>
  {
    options.DefaultAuthenticateScheme = "JwtBearer";
    options.DefaultChallengeScheme = "JwtBearer";
  })
  .AddJwtBearer("JwtBearer", jwtBearerOptions =>
  {
    jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
    {
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.Key)),
      ValidateIssuer = true,
      ValidIssuer = settings.Issuer,

      ValidateAudience = true,
      ValidAudience = settings.Audience,

      ValidateLifetime = true,
      ClockSkew = TimeSpan.FromMinutes(settings.MinutesToExpiration)
    };
  });

  // REST OF THE CODE HERE
}

Startup 类中要做的最后一件事是修改 Configure() 方法,并告诉它使用身份验证。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   // REST OF THE CODE HERE

   app.UseAuthentication();

   app.UseMvc();
}

将设置注入 SecurityController

您希望 .NET Core 将 JwtSettings 类注入到 SecurityController 中,以便您可以访问 JWT Token 设置。打开 SecurityController.cs 文件,并添加一个接受 JwtSettings 类实例的构造函数。将此 settings 对象存储在名为 _settings 的字段中。将此 _settings 字段传递给 SecurityManager 构造函数。

public class SecurityController : Controller
{
  private JwtSettings _settings;
  public SecurityController(JwtSettings settings)
  {
    _settings = settings;
  }

  [HttpPost("login")]
  public IActionResult Login([FromBody]AppUser user)
  {
    IActionResult ret = null;
    AppUserAuth auth = new AppUserAuth();
    SecurityManager mgr = new SecurityManager(_settings);

    auth = (AppUserAuth)mgr.ValidateUser(user);

  // REST OF THE CODE HERE
}

在 SecurityManager 中接受设置

在上面的代码中,您将 JwtSettings 类的一个实例传递给了 SecurityManager 类。打开 SecurityManager.cs 文件,并添加代码以接受此 JwtSettings 实例。代码看起来将与 SecurityController 中添加的构造函数和 private 字段非常相似。

private JwtSettings _settings = new JwtSettings();
public SecurityManager(JwtSettings settings)
{
  _settings = settings;
}

构建 JSON Web Token

您终于可以构建一个 bearer token 了,该 token 可以添加到用户安全对象中的 BearerToken 属性。向 SecurityManager 类添加一个名为 BuildJwtToken() 的新方法。从您在 Startup 类中使用的相同 Key 属性生成一个对称安全密钥。任何 JWT token 都需要的两个声明,SubJti,都应该被创建。在 Sub 声明中放入用户名,在 Jti 声明中创建一个该用户的唯一标识符。生成 Guid 是该字段的良好标识符。

接下来,您将添加一系列自定义声明。在这个简单的例子中,为 AppUserAuth 类中的每个属性创建一个声明。我相信您可以看到在哪里可以通过循环遍历来自表中的角色和/或声明/权限集合来添加声明。但是,我希望在本篇文章中保持简单。请注意,我使用 ToLower() 将“True”和“False”值转换为小写。这与 JavaScript/TypeScript 处理 truefalse 的方式保持一致。

创建一个新的 JwtSecurityToken 对象,并使用与 Startup 类中相同的属性和 JwtSettings 类中的相同值进行设置。如果您不使用相同的值,那么当 token 从 Angular 传入时,它们将无法被验证。使用 JwtSecurityTokenHandler 类的 WriteToken() 方法将生成的 string 进行 base64 编码。正是这个 base64 编码的 string 被放入到返回给 Angular 的用户安全对象中的 BearerToken 属性。

protected string BuildJwtToken()
{
  SymmetricSecurityKey key = 
           new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.Key));

  // Create standard JWT claims
  List<Claim> jwtClaims = new List<Claim>();
  jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Sub, _user.UserName));
  jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
  
  // Add custom claims
  jwtClaims.Add(new Claim("isAuthenticated", 
                           _auth.IsAuthenticated.ToString().ToLower()));
  jwtClaims.Add(new Claim("canAccessProducts", 
                           _auth.CanAccessProducts.ToString().ToLower()));
  jwtClaims.Add(new Claim("canAddProduct", _auth.CanAddProduct.ToString().ToLower()));
  jwtClaims.Add(new Claim("canSaveProduct", _auth.CanSaveProduct.ToString().ToLower()));
  jwtClaims.Add(new Claim("canAccessCategories", 
                           _auth.CanAccessCategories.ToString().ToLower()));
  jwtClaims.Add(new Claim("canAddCategory", _auth.CanAddCategory.ToString().ToLower()));

  // Create the JwtSecurityToken object
  var token = new JwtSecurityToken(
    issuer: _settings.Issuer,
    audience: _settings.Audience,
    claims: jwtClaims,
    notBefore: DateTime.UtcNow,
    expires: DateTime.UtcNow.AddMinutes(_settings.MinutesToExpiration),
    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
  );

  // Create a string representation of the Jwt token
  return new JwtSecurityTokenHandler().WriteToken(token); ;
}

调用 BuildJwtToken() 方法

找到 BuildUserAuthObject() 方法,并添加代码来调用此 BuildJwtToken() 方法,并将返回的 string 结果分配给 BearerToken 属性。

protected void BuildUserAuthObject()
{
  if (_user != null)
  {
    _auth = CreateMockSecurityObjects()
      .Where(u => u.UserName ==
             _user.UserName).FirstOrDefault();
  }

  if (_auth == null)
  {
    _auth = new AppUserAuth();
  }
  else {
    // Create JWT Bearer Token
    _auth.BearerToken = BuildJwtToken();  
  }
}

试试看

保存所有更改,然后开始调试 PtcApi 项目。如果您已登录 Angular 应用程序,请单击“Logout”菜单。输入“psheriff”或“bjones”以及密码“P@ssw0rd”,您应该会看到一个与图 3 类似的屏幕。

Figure 3

图 3:在 jwt.io 网站上解码 bearerToken。

如果您想查看 bearerToken 属性的构成,可以在 www.jwt.io 网站上解码。将登录页面上的 bearerToken 复制到剪贴板。打开浏览器窗口并转到 www.jwt.io。向下滚动页面,直到看到一个标有“Encoded”的框。删除其中的内容,然后粘贴您的 bearer token。您应该会立即看到包含您所有数据的 payload 数据,如图 4 所示。

Figure 4

图 4:在 www.jwt.io 上解码您的 bearer token。

试试看

单击“Products”菜单,您现在应该会在开发人员工具控制台窗口中看到一个通用的 401 Unauthorized 消息,如图 5 所示。出现此错误的原因是服务器不知道您就是刚刚登录的同一个人。您必须在每次 Web API 调用中都传递 bearer token,以向服务器证明您有权调用该 API。

Figure 5

图 5:除非将 bearer token 传回服务器,否则您将从任何受保护的 Web API 方法中收到 401 错误。

将标头添加到 Product Service

为避免出现状态码 401,您必须从 Angular 代码的每次 Web API 调用中都传递 bearer token。您将学习如何自动将 token 添加到每次调用中,但首先,只需修改产品服务类中的 Get() 方法。打开 product.service.ts 文件,并修改构造函数以注入 SecurityService

constructor(private http: HttpClient,
 private securityService: SecurityService
) { }

getProducts() 方法中添加代码以创建新的 HttpHeaders 对象。实例化此对象后,调用 set() 方法,并将“Authorization”作为标头名称传递。在此标头中传递的数据是单词 'Bearer ' 加上一个空格,然后是 bearer token 本身。向 HttpClientget() 方法添加第二个参数,并传递一个具有 headers 属性的对象,以及您创建的 HttpHeaders 对象作为该属性的值。

getProducts(): Observable<Product[]> {
  let httpOptions = new HttpHeaders()
  .set('Authorization', 'Bearer ' + this.securityService.securityObject.bearerToken);

  return this.http.get<Product[]>(API_URL<code>, { headers: httpOptions }</code>);
}

试试看

保存更改,转到浏览器并以“psheriff”身份登录,然后单击“Products”菜单,您应该会看到产品数据已显示。这是因为服务器已验证了您的 token,并且现在知道您是谁。因此,您被授予了对 ProductControllerGet() 方法的访问权限。

HTTP Interceptor

与其在每个 Web API 调用前都添加相同的标头,不如创建一个 HTTP Interceptor 类,将自定义标头添加到 Angular 的每个 Web API 调用中。右键单击 \security 文件夹,然后添加一个名为 http-interceptor.module.ts 的文件。我不会解释这段代码,因为它在 http://bit.ly/2GYt1H3 上有详细的文档,您应该能在互联网上找到许多关于它的博客文章。

我只想指出一点,您是从本地存储中检索 bearer token 的。您可能想知道为什么不将 SecurityService 对象注入到这个类中,并像在所有其他组件中一样从 SecurityService 对象中检索它。由于安全服务类也使用 HttpClient,因此无法将 HTTP interceptor 注入 SecurityService。这会导致递归错误。

如果您还记得,您在 SecurityService 类的 login() 方法中添加了将 bearer token 存储到本地存储的代码。现在您知道该代码的原因了。

import { Injectable, NgModule } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpEvent, HttpInterceptor, HttpHandler,
         HttpRequest } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    var token = localStorage.getItem("bearerToken");

    if(token) {
      const newReq = req.clone(
        { 
           headers: req.headers.set('Authorization', 'Bearer ' + token)
        });

        return next.handle(newReq);
    }
    else {
      return next.handle(req);
    }
  }
};

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, 
      useClass: HttpRequestInterceptor, 
      multi: true }
  ]
})
export class HttpInterceptorModule { }

为了使用这个 HTTP Interceptor 类,请在您的 AppModule 类中注册它。打开 app.module.ts 文件,并将此新模块添加到 imports 属性中。

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  AppRoutingModule<code>,
  HttpInterceptorModule</code>
],

打开 product.service.ts 文件,并将 getProducts() 方法改回您最初的样子。删除创建 HttpHeaders 的代码,以及将其作为第二个参数传递给 get() 方法的代码。现在 getProducts() 方法应该如下所示。

getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(API_URL);
}

试试看

保存更改,转到浏览器,以“psheriff”身份登录,然后尝试访问“Products”菜单。您应该仍然能从产品控制器获取数据。这验证了 HTTP Interceptor 类正在工作。

添加安全策略

[Authorize] 属性仅确保用户已通过身份验证。但是,对于某些 Web API 方法,您可能希望根据角色或声明/权限限制访问。这通过向 .NET Core Web API 项目的服务添加授权来实现。

打开 Startup.cs 文件,并在 ConfigureServices 方法中,紧挨着您之前添加的用于添加身份验证的代码下方,添加以下代码。对于 AppUserAuth 对象中的每个属性,您可以添加 policy 对象并指定用户必须拥有的值。

services.AddAuthorization(cfg =>
{
   // NOTE: The claim key and value are case-sensitive
   cfg.AddPolicy("CanAccessProducts", p =>
      p.RequireClaim("CanAccessProducts", "true"));
});

创建声明后,您可以将 Policy 属性添加到 Authorize 属性,以检查任何声明名称。例如,在 ProductController.Get() 方法上,您可以将以下代码添加到 Authorize 属性中,以将该方法的用法限制为仅那些 CanAccessProducts 属性设置为 true 的用户。

[Authorize(Policy = "CanAccessProducts")]

试试看

打开 PtcApi 项目中的 SecurityManager.cs 文件,并找到 CreateMockSecurityObjects() 方法。将“CanAccessProducts”声明修改为“PSheriff”用户的“false”值。

打开 app-routing.module.ts 文件,并删除“products”路径的路由守卫。

{
    path: 'products',
    component: ProductListComponent
},

保存更改,然后开始调试 PtcApi 项目。返回浏览器,以“psheriff”身份登录,然后在地址栏中键入 https://:4200/products,您现在应该会收到一个 403-Forbidden 错误。将路由守卫放回,并将 SecurityManager 类中的 CanAccessProducts 属性重置为“true”值。

摘要

在本篇文章中,您构建了 Web API 调用来对用户进行身份验证,并将授权对象返回给 Angular。此外,您配置了 .NET Core Web API 项目以使用 JSON Web Token 系统来保护您的 Web API 调用。您学习了如何将 bearer token 发送到 Angular,以及如何通过 HTTP Interceptor 类让 Angular 将这些 token 发送回来。

历史

  • 2018 年 7 月 16 日:初始版本
© . All rights reserved.