OAuth 2 从入门到精通(一) - 身份认证服务器

前言

翻译类文章,原文来自 Token Based Authentication using ASP.NET Web API 2, Owin, and Identity,没有逐字逐句翻译,大概意思表达了。

创建认证服务

  1. 新建WebAPI项目
    Framework 4.6,新建ASP.NET应用程序,选择“Empty”,“Web API”,“No Authentication”。
  2. 引入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
  1. 添加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();
        }
    }
  1. 删除“Global.ascx”文件。
  2. 添加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 入门系列 - 数据库连接 。

  1. 创建用户身份识别仓储类
    第一步我们实现“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();
    }
}
  1. 添加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”中。

OAuth 2 从入门到精通(一) - 身份认证服务器_第1张图片
注册成功

如果你没有修改JSON字串中的用户名,直接再次按下“Send”按钮,将会出现如下情况:
OAuth 2 从入门到精通(一) - 身份认证服务器_第2张图片
请求无效

  1. 添加一个受保护的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 未授权状态码,因为此时,你发送的请求中并未包含任何验证信息。

  1. 添加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” 进行用户验证。
  1. 实现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。

  1. 在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);
   }
  1. 测试后端API
    假设您在下面的步骤中注册了密码为“SuperPass”的用户名“Taiseer”,我们将使用相同的用户名来生成令牌,因此,为了测试这一点,打开您最喜欢的REST客户端应用程序,发出HTTP请求来生成用户“Taiseer”的令牌。我个人偏好使用PostMan。
    OAuth 2 从入门到精通(一) - 身份认证服务器_第3张图片
    令牌申请

    在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” 和刚刚申请到的令牌字符串(中间加一空格)。
    注意:在这儿,我们无需再传递用户名和密码。如下图:
    OAuth 2 从入门到精通(一) - 身份认证服务器_第4张图片
    资源获取

    一切都正确的话,收到HTTP 状态码200以及所需的受保护数据。如果你修改令牌任意字符,将会收到HTTP 状态码401(未授权)。
    到目前为止,我们建立的后端API已经可以从任何一种前端应用进行访问,不管是Web,Winform或者是原生手机APP。

你可能感兴趣的:(OAuth 2 从入门到精通(一) - 身份认证服务器)