前言
翻译类文章,原文来自 Token Based Authentication using ASP.NET Web API 2, Owin, and Identity,没有逐字逐句翻译,大概意思表达了。
创建认证服务
- 新建WebAPI项目
Framework 4.6,新建ASP.NET应用程序,选择“Empty”,“Web API”,“No Authentication”。 - 引入Nuget包管理
使用Nuget包管理器安装用于Owin服务器的类库,打开Nuget Package Manger控制台,使用如下命令安装:
Install-Package Microsoft.AspNet.WebApi.Owin -Version 5.1.2
Install-Package Microsoft.Owin.Host.SystemWeb -Version 2.1.0
- 添加OWIN启动类
using Microsoft.Owin;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
[assembly: OwinStartup(typeof(AuthenServer.Startup))]
namespace AuthenServer
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
}
}
Startup类将会在Owin服务启动的时候被调用,请注意“assembly”特性标注了将会被调用的类。
方法中的代码,在基本的WebAPI脚手架中,位于Global,ascx文件中,用于注册API路由及相关运行参数。
在步骤一中,通常VS已经生成了一个“WebApiConfig”类,如果还没有,我们可以现在添加一个,默认将此类放在"App_Start"文件夹中。代码如下:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var jsonFormatter = config.Formatters.OfType().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
}
- 删除“Global.ascx”文件。
- 添加ASP.NET Identity身份识别系统
为简(FU)单(ZA)起见,我们直接使用微软的ASP.NET Identity身份识别系统,先引入Identity组件包:
Install-Package Microsoft.AspNet.Identity.Owin -Version 2.0.1
Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.0.1
添加一个用于连接SQL Server数据库的上下文AuthenContext类,以及一个UserViewModel实体类。
public class AuthenContext: IdentityDbContext
{
public AuthenContext(): base("AuthenContext")
{
}
}
-------------------------------------------------------------------------
public class UserViewModel
{
[Required]
[Display(Name = "用户名")]
public string UserName { get; set; }
[Required]
[StringLength(100, ErrorMessage = "{0} 要求最少 {2} 位字符长度。", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "密码")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "确认密码")]
[Compare("Password", ErrorMessage = "两次输入的密码不一致。")]
public string ConfirmPassword { get; set; }
}
在Web.config中添加数据库连接字符串配置项
需要注意的是,name值要和上面AuthenContext类的构造函数中使用的参数值一致,都是“AuthenContext”,数据库我们使用LocalDB,数据库名OAuth,数据库文件放在哪儿没有说明,系统会自动存放在Windows当前登录用户的用户文件夹中。有关连接字符串的详细说明,可以查阅 EF 6.0 入门系列 - 数据库连接 。
- 创建用户身份识别仓储类
第一步我们实现“RegisterUser”及“FindUser”两个方法。ASP.NET Identity System将会帮助我们对密码进行HASH编码等一系列复杂工作,我们只需要简单的将前端传入的用户密码等参数提供给UserManager类。
public class AuthenRepository : IDisposable
{
private AuthenContext _context;
private UserManager _userManager;
public AuthenRepository()
{
_context= new AuthenContext();
_userManager= new UserManager(new UserStore(_context));
}
public async Task RegisterUser(UserViewModel userModel)
{
IdentityUser user = new IdentityUser
{
UserName = userModel.UserName
};
var result = await _userManager.CreateAsync(user, userModel.Password);
return result;
}
public async Task FindUser(string userName, string password)
{
IdentityUser user = await _userManager.FindAsync(userName, password);
return user;
}
public void Dispose()
{
_context.Dispose();
_userManager.Dispose();
}
}
- 添加AccountAPIController
实现一个Register方法,返回成功或者失败。
[RoutePrefix("api/Account")]
public class AccountAPIController : ApiController
{
private AuthenRepository _repository = null;
public AccountAPIController()
{
_repository = new AuthenRepository();
}
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task Register(UserViewModel userModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await _repository.RegisterUser(userModel);
IHttpActionResult errorResult = GetErrorResult(result);
if (errorResult != null)
{
return errorResult;
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_repository.Dispose();
}
base.Dispose(disposing);
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
// No ModelState errors are available to send, so just return an empty BadRequest.
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
}
上面代码中,实现了一个地址为“api/account/register”的Web API方法,可以通过使用HTTP POST方法发送如下的JSON对象来实现新用户注册。
{
"userName": "Taiser",
"password": "SuperPass",
"confirmPassword": "SuperPass"
}
运行此Web应用程序,并打开POSTMAN,发送一个HTTP POST请求到 “http://localhost:port/api/account/register”,运气好的话,你将会收到HTTP 200状态码,同时,在Web.config连接字符串指向的数据库将被创建(第一次运行),同时发送的JSON对象中的用户将会被添加到数据表“AspNetUsers”中。
如果你没有修改JSON字串中的用户名,直接再次按下“Send”按钮,将会出现如下情况:
- 添加一个受保护的API
项目右键添加一个空的API控制器OrdersAPIController,我们将确保此控制权只会向已认证用户返回订单信息,为简单起见,演示代码直接使用静态数据。
[RoutePrefix("api/Orders")]
public class OrdersAPIController : ApiController
{
[Authorize]
[Route("")]
public IHttpActionResult Get()
{
return Ok(Order.CreateOrders());
}
}
#region Helpers
public class Order
{
public int OrderID { get; set; }
public string CustomerName { get; set; }
public string ShipperCity { get; set; }
public Boolean IsShipped { get; set; }
public static List CreateOrders()
{
List OrderList = new List
{
new Order {OrderID = 10248, CustomerName = "Taiseer Joudeh", ShipperCity = "Amman", IsShipped = true },
new Order {OrderID = 10249, CustomerName = "Ahmad Hasan", ShipperCity = "Dubai", IsShipped = false},
new Order {OrderID = 10250,CustomerName = "Tamer Yaser", ShipperCity = "Jeddah", IsShipped = false },
new Order {OrderID = 10251,CustomerName = "Lina Majed", ShipperCity = "Abu Dhabi", IsShipped = false},
new Order {OrderID = 10252,CustomerName = "Yasmeen Rami", ShipperCity = "Kuwait", IsShipped = true}
};
return OrderList;
}
}
#endregion
需要注意的是我们在“Get”方法前面添加了“Authorize”特性,这样一来,当你向“http://localhost:port/api/orders”发送HTTP GET请求时,将会收到401 未授权状态码,因为此时,你发送的请求中并未包含任何验证信息。
- 添加OAuth Berrer Tokens(承载令牌)生成代码
第一步,引入如下OAuth包
Install-Package Microsoft.Owin.Security.OAuth -Version 2.1.0
第二步,在Startup类中添加“ConfigureOAuth”以配置API使用OAuth验证流程。
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
// 此处为原有代码,省略......
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
// 令牌生成
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
“OAuthAuthorizationServerOptions” 配置
- 令牌生成URL为”http://localhost:port/token”。
- 令牌的过期时间为24小时,也就是说,用户在24小时候,还使用之前的令牌,他的请求将会被拒绝,收到401状态码。
- 用户申请令牌的时候,使用 “SimpleAuthorizationServerProvider” 进行用户验证。
- 实现SimpleAuthorizationServerProvider”类
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}
“SimpleAuthorizationServerProvider” 覆盖基类 “OAuthAuthorizationServerProvider“ 的两个方法,分别是 “ValidateClientAuthentication” 和 “GrantResourceOwnerCredentials”。
第一个方法职责是验证连接客户端, The first method is responsible for validating the “Client”, 在我们的例子中,我们只有一个客户端,所以我们总是返回验证成功。
第二个方法 “GrantResourceOwnerCredentials” 职责是验证用户名和密码, 使用前面编写的 “AuthenRepository” 类方法 “FindUser” 来验证用户名和密码的有效性。
如果凭据有效,我们将创建“ClaimsIdentity”类,并将认证类型传递给它(在我们的例子中是“bearer token”),然后我们将在签名的令牌中添加两个声明 “sub” 及 “role”。您可以在这里添加其他不同的声明,但令牌大小会因此增加。
当我们调用“context.Validated(identity)”时,令牌将由系统自动生成。
如果需要允许在令牌中间件提供商上使用CORS (跨源资源共享 Cross-Origin Resource Sharing),我们需要向Owin上下文添加标题“Access-Control-Allow-Origin”,如果您忘记了这一点,那么当您尝试从浏览器调用它时,生成令牌将失败。 这不允许CORS的令牌中间件提供程序不是ASP.NET Web API,我们将在下一步添加。 这句话貌似是作者的笔误,感觉最开始的 Not that应该是Note that。
- 在ASP.NET Web API中配置允许CORS
第一步还是引入Nuget包
Install-Package Microsoft.Owin.Cors -Version 2.1.0
第二步,打开 “Startup” 类,在 “Configuration” 方法中添加一行代码:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
- 测试后端API
假设您在下面的步骤中注册了密码为“SuperPass”的用户名“Taiseer”,我们将使用相同的用户名来生成令牌,因此,为了测试这一点,打开您最喜欢的REST客户端应用程序,发出HTTP请求来生成用户“Taiseer”的令牌。我个人偏好使用PostMan。
在Headers页面,配置“content-type” 为“application/x-www-form-urlencoded”。(配图中看不见,自己试一下,我就不再为此占用无谓的互联网资源了)
在Body页面,配置类型为x-www-form-urlencoded,并添加如同中的三个键值"grant_type"、"username"、"password"。
下方的Raw是按下"Send"按钮后,username和password没有错误的话,令牌服务器返回的令牌,我们需要手工复制此令牌字符串,在下一步获取资源时作为Header提交。
接下来,使用申请的令牌来请求订单数据,URL为"http://localhost:23170/api/orders",HTTP GET请求,并在Header 加入“Authorization”键,内容是“Bearer” 和刚刚申请到的令牌字符串(中间加一空格)。
注意:在这儿,我们无需再传递用户名和密码。如下图:
一切都正确的话,收到HTTP 状态码200以及所需的受保护数据。如果你修改令牌任意字符,将会收到HTTP 状态码401(未授权)。
到目前为止,我们建立的后端API已经可以从任何一种前端应用进行访问,不管是Web,Winform或者是原生手机APP。