OAuth打造webapi认证服务

使用OAuth打造webapi认证服务供自己的客户端使用

一、什么是OAuth

OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版。注意是Authorization(授权),而不是Authentication(认证)。用来做Authentication(认证)的标准叫做openid connect,我们将在以后的文章中进行介绍。

二、名词定义

理解OAuth中的专业术语能够帮助你理解其流程模式,OAuth中常用的名词术语有4个,为了便于理解这些术语,我们先假设一个很常见的授权场景:

你访问了一个日志网站(third party application),你(client)觉得这个网站很不错,准备以后就要在这个网站上写日志了,所以你准备把QQ空间(Resource owner)里面的日志都导入进来。此日志网站想要导入你在QQ空间中的日志需要知道你的QQ用户名和密码才行,为了安全期间你不会把你的QQ用户名和密码直接输入在日志网站中,所以日志网站帮你导航到了QQ认证界面(Authorization Server),当你输入完用户名和密码后,QQ认证服务器返回给日志网站一个token, 该日志网站凭借此token来访问你在QQ空间中的日志。

  1. third party application 第三方的应用,想要的到Resource owner的授权
  2. client 代表用户
  3. Resource owner 资源拥有者,在这里代表QQ
  4. Authorization server 认证服务,这里代表QQ认证服务,Resource owner和Authorization server可以是不同的服务器,也可以是同一个服务器。

三、OAuth2.0中的四种模式

OAuth定义了四种模式,覆盖了所有的授权应用场景:

  1. 授权码模式(authorization code)
  2. 简化模式(implicit)
  3. 密码模式(resource owner password credentials)
  4. 客户端模式(client credentials)

前面我们假设的场景可以用前两种模式来实现,不同之处在于:

当日志网站(third party application)有服务端使用流程1;

当日志网站(third party application)没有服务端,例如纯的js+html页面需要采用流程2;

本文主描述利用OAuth2.0实现自己的WebApi认证服务,前两种模式使用场景不符合我们的需求。

四、选择合适的OAuth模式打造自己的webApi认证服务

场景:你自己实现了一套webApi,想供自己的客户端调用,又想做认证。

这种场景下你应该选择模式3或者4,特别是当你的的客户端是js+html应该选择3,当你的客户端是移动端(ios应用之类)可以选择3,也可以选择4。

密码模式(resource owner password credentials)的流程:

这种模式的流程非常简单:

  1. 用户向客户端(third party application)提供用户名和密码。
  2. 客户端将用户名和密码发给认证服务器(Authorization server),向后者请求令牌(token)。
  3. 认证服务器确认无误后,向客户端提供访问令牌。
  4. 客户端持令牌(token)访问资源。

此时third party application代表我们自己的客户端,Authorization server和Resource owner代表我们自己的webApi服务。我们在日志网站的场景中提到:用户不能直接为日志网站(third party application)提供QQ(resource owner)的用户名和密码。而此时third party application、authorization server、resource owner都是一家人,Resource owner对third party application足够信任,所以我们才能采取这种模式来实现。

五、使用owin来实现密码模式

owin集成了OAuth2.0的实现,所以在webapi中使用owin来打造authorization无疑是最简单最方便的方案。

  1. 新建webApi项目
  2. 安装Nuget package:

    Microsoft.AspNet.WebApi.Owin

    icrosoft.Owin.Host.SystemWeb

  3. 增加owin的入口类:Startup.cs

在项目中新建一个类,命名为Startup.cs,这个类将作为owin的启动入口,添加下面的代码


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

[assembly: OwinStartup(typeof(OAuthPractice.ProtectedApi.Startup))]

namespace OAuthPractice.ProtectedApi

{

    public class Startup

    {

        public void Configuration(IAppBuilder app)

        {

            var config = new HttpConfiguration();

            WebApiConfig.Register(config);

            app.UseWebApi(config);

        }

    }

}

另外修改WebApiConfig.Register(HttpConfiguration config)方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public static class WebApiConfig

{

    public static void Register(HttpConfiguration config)

    {

        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(

            name: "DefaultApi",

            routeTemplate: "api/{controller}/{id}",

            defaults: new { id = RouteParameter.Optional }

        );

        var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();

        jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

    }

}

最后两句话将会使用CamelCase命名法序列化webApi的返回结果。

3.使用ASP.NET Identity 实现一个简单的用户认证功能,以便我们生成用户名和密码

安装nuget package:

Microsoft.AspNet.Identity.Owin

Microsoft.AspNet.Identity.EntityFramework

4.新建一个Auth的文件夹,并添加AuthContext类:


1

2

3

4

5

6

7

public class AuthContext : IdentityDbContext<IdentityUser>

    {

        public AuthContext():base("AuthContext")

        {

            

        }

    }

同时在web.config中添加connectionString:


1

2

3

<connectionStrings>

  <add name="AuthContext" connectionString="Data Source=.;Initial Catalog=OAuthPractice;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />

</connectionStrings>

5.增加一个Entities文件夹并添加UserModel类:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class UserModel

{

    [Required]

    [Display(Name = "UserModel name")]

    public string UserName { get; set; }

    [Required]

    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]

    [DataType(DataType.Password)]

    [Display(Name = "Password")]

    public string Password { get; set; }

    [DataType(DataType.Password)]

    [Display(Name = "Confirm password")]

    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]

    public string ConfirmPassword { get; set; }

}

6.在Auth文件夹下添加AuthRepository类,增加用户注册和查找功能:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

public class AuthRepository : IDisposable

{

    private AuthContext _ctx;

    private UserManager<IdentityUser> _userManager;

    public AuthRepository()

    {

        _ctx = new AuthContext();

        _userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(_ctx));

    }

    public async Task<IdentityResult> RegisterUser(UserModel userModel)

    {

        IdentityUser user = new IdentityUser

        {

            UserName = userModel.UserName

        };

        var result = await _userManager.CreateAsync(user, userModel.Password);

        return result;

    }

    public async Task<IdentityUser> FindUser(string userName, string password)

    {

        IdentityUser user = await _userManager.FindAsync(userName, password);

        return user;

    }

    public void Dispose()

    {

        _ctx.Dispose();

        _userManager.Dispose();

    }

}

7、增加AccountController


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

[RoutePrefix("api/Account")]

public class AccountController : ApiController

{

    private readonly AuthRepository _authRepository = null;

    public AccountController()

    {

        _authRepository = new AuthRepository();

    }

    // POST api/Account/Register

    [AllowAnonymous]

    [Route("Register")]

    public async Task<IHttpActionResult> Register(UserModel userModel)

    {

        if (!ModelState.IsValid)

        {

            return BadRequest(ModelState);

        }

        IdentityResult result = await _authRepository.RegisterUser(userModel);

        IHttpActionResult errorResult = GetErrorResult(result);

        if (errorResult != null)

        {

            return errorResult;

        }

        return Ok();

    }

    protected override void Dispose(bool disposing)

    {

        if (disposing)

        {

            _authRepository.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;

    }

}

Register方法打上了AllowAnonymous标签,意味着调用这个api无需任何授权。

8.增加一个OrderControll,添加一个受保护的api用来做实验

在Models文件夹下增加Order类:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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<Order> CreateOrders()

    {

        List<Order> OrderList = new List<Order>

        {

            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;

    }

}

增加OrderController类:


1

2

3

4

5

6

7

8

9

10

11

[RoutePrefix("api/Orders")]

public class OrdersController : ApiController

{

    [Authorize]

    [Route("")]

    public List<Order> Get()

    {

        return Order.CreateOrders();

    }

}

我们在Get()方法上加了Authorize标签,所以此api在没有授权的情况下将返回401 Unauthorize。使用postman发个请求试试:

9. 增加OAuth认证


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public class Startup

{

    public void Configuration(IAppBuilder app)

    {

        var config = new HttpConfiguration();

        WebApiConfig.Register(config);

        ConfigureOAuth(app);

        //这一行代码必须放在ConfiureOAuth(app)之后

        app.UseWebApi(config);

    }

    public void ConfigureOAuth(IAppBuilder app)

    {

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()

        {

            AllowInsecureHttp = true,

            TokenEndpointPath = new PathString("/token"),

            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),

            Provider = new SimpleAuthorizationServerProvider()

        };

        // Token Generation

        app.UseOAuthAuthorizationServer(OAuthServerOptions);

        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

    }

ConfigureOAuth(IAppBuilder app)方法开启了OAuth服务。简单说一下OAuthAuthorizationServerOptions中各参数的含义:

AllowInsecureHttp:允许客户端一http协议请求;

TokenEndpointPath:token请求的地址,即http://localhost:端口号/token;

AccessTokenExpireTimeSpan :token过期时间;

Provider :提供具体的认证策略;

SimpleAuthorizationServerProvider的代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider

{

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)

    {

        context.Validated();

        return Task.FromResult<object>(null);

    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)

    {

        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(ClaimTypes.Name, context.UserName));

        identity.AddClaim(new Claim(ClaimTypes.Role, "user"));

        identity.AddClaim(new Claim("sub", context.UserName));

        var props = new AuthenticationProperties(new Dictionary<string, string>

            {

                {

                    "as:client_id", context.ClientId ?? string.Empty

                },

                {

                    "userName", context.UserName

                }

            });

        var ticket = new AuthenticationTicket(identity, props);

        context.Validated(ticket);

    }

    public override Task TokenEndpoint(OAuthTokenEndpointContext context)

    {

        foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)

        {

            context.AdditionalResponseParameters.Add(property.Key, property.Value);

        }

        return Task.FromResult<object>(null);

    }

}

ValidateClientAuthentication方法用来对third party application 认证,具体的做法是为third party application颁发appKey和appSecrect,在本例中我们省略了颁发appKey和appSecrect的环节,我们认为所有的third party application都是合法的,context.Validated(); 表示所有允许此third party application请求。
GrantResourceOwnerCredentials方法则是resource owner password credentials模式的重点,由于客户端发送了用户的用户名和密码,所以我们在这里验证用户名和密码是否正确,后面的代码采用了ClaimsIdentity认证方式,其实我们可以把他当作一个NameValueCollection看待。最后context.Validated(ticket); 表明认证通过。

只有这两个方法同时认证通过才会颁发token。

TokenEndpoint方法将会把Context中的属性加入到token中。
10、注册用户

使用postman发送注册用户的请求(http://{url}/api/account/register)服务器返回200,说明注册成功。

11、向服务器请求token

resource owner password credentials模式需要请求头必须包含3个参数:

grant_type-必须为password

username-用户名

password-用户密码

12、使用token访问受保护的api

在Header中加入:Authorization – bearer {{token}},此token就是上一步得到的token。

此时客户端在30分钟内使用该token即可访问受保护的资源。30分钟这个设置来自AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),你可以自定义token过期时间。

六、刷新token

当token过期后,OAuth2.0提供了token刷新机制:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public void ConfigureOAuth(IAppBuilder app)

{

    OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()

    {

        AllowInsecureHttp = true,

        TokenEndpointPath = new PathString("/token"),

        AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10),

        Provider = new SimpleAuthorizationServerProvider(),

        //refresh token provider

        RefreshTokenProvider = new SimpleRefreshTokenProvider()

    };

    // Token Generation

    app.UseOAuthAuthorizationServer(OAuthServerOptions);

    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

}

1、添加新的RefreshTokenProvider


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider

{

    public async Task CreateAsync(AuthenticationTokenCreateContext context)

    {

        var refreshTokenId = Guid.NewGuid().ToString("n");

        using (AuthRepository _repo = new AuthRepository())

        {

            var token = new RefreshToken()

            {

                Id = refreshTokenId.GetHash(),

                Subject = context.Ticket.Identity.Name,

                IssuedUtc = DateTime.UtcNow,

                ExpiresUtc = DateTime.UtcNow.AddMinutes(30)

            };

            context.Ticket.Properties.IssuedUtc = token.IssuedUtc;

            context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;

            token.ProtectedTicket = context.SerializeTicket();

            var result = await _repo.AddRefreshToken(token);

            if (result)

            {

                context.SetToken(refreshTokenId);

            }

        }

    }

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)

    {

        string hashedTokenId = context.Token.GetHash();

        using (AuthRepository _repo = new AuthRepository())

        {

            var refreshToken = await _repo.FindRefreshToken(hashedTokenId);

            if (refreshToken != null)

            {

                //Get protectedTicket from refreshToken class

                context.DeserializeTicket(refreshToken.ProtectedTicket);

                var result = await _repo.RemoveRefreshToken(hashedTokenId);

            }

        }

    }

    public void Create(AuthenticationTokenCreateContext context)

    {

        throw new NotImplementedException();

    }

    public void Receive(AuthenticationTokenReceiveContext context)

    {

        throw new NotImplementedException();

    }

}

我们实现了其中两个异步方法,对两个同步方法不做实现。其中CreateAsync用来生成RefreshToken值,生成后需要持久化在数据库中,客户端需要拿RefreshToken来请求刷新token,此时ReceiveAsync方法将拿客户的RefreshToken和数据库中RefreshToken做对比,验证成功后删除此refreshToken。

2、重新请求token

可以看到这次请求不但得到了token,还得到了refresh_token

3、当token过期后,凭借上次得到的refresh_token重新获取token

此次请求又得到了新的refresh_token,每次refresh_token只能用一次,因为在方法ReceiveAsync中我们一旦拿到refresh_token就删除了记录。

七、总结

此文重点介绍了OAuth2.0中resource owner password credentials模式的使用,此模式可以实现资源服务为自己的客户端授权。另外文章中也提到模式4-client credentials也可以实现这种场景,但用来给有服务端的客户端使用-区别于纯html+js客户端。原因在于模式4-client credentials使用appKey+appSecrect来验证客户端,如果没有服务端的话appSecrect将暴露在js中。

同样的道理:模式1-授权码模式(authorization code)和模式2-简化模式(implicit)的区别也在于模式2-简化模式(implicit)用在无服务端的场景下,请求头中不用带appSecrect。

在webApi中使用owin来实现OAuth2.0是最简单的解决方案,另外一个方案是使用DotNetOpenOauth,这个方案的实现稍显复杂,可用的文档也较少,源码中带有几个例子我也没有直接跑起来,最后无奈之下几乎读完了整个源码才理解。

八、客户端的实现

我们将采用jquery和angular两种js框架来调用本文实现的服务端。下一篇将实现此功能,另外还要给我们的服务端加上CORS(同源策略)支持。

所有的代码都同步更新在 https://git.oschina.net/richieyangs/OAuthPractice.git

参考:

http://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server

http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api

http://bitoftech.net/2014/06/01/token-based-authentication-asp-net-web-api-2-owin-asp-net-identity/

分类: .NET

时间: 2024-10-06 04:42:24

OAuth打造webapi认证服务的相关文章

使用OAuth打造webapi认证服务供自己的客户端使用(转)

转自:http://www.cnblogs.com/richieyang/p/4918819.html#!comments 一.什么是OAuth OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版.注意是Authorization(授权),而不是Authentication(认证).用来做Authentication(认证)的标准叫做openid connect,我们将在以后的文章中进行介绍. 二.名词定义 理解OAuth中的专业术语能够帮助你理解其流程模

使用OAuth打造webapi认证服务供自己的客户端使用(二)

在上一篇”使用OAuth打造webapi认证服务供自己的客户端使用“的文章中我们实现了一个采用了OAuth流程3-密码模式(resource owner password credentials)的WebApi服务端.今天我们来实现一个js+html版本的客户端. 一.angular客户端 angular版本的客户端代码来自于http://bitoftech.net/2014/06/01/token-based-authentication-asp-net-web-api-2-owin-asp-

使用OAuth打造webapi认证服务供自己的客户端使用

一.什么是OAuth OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版.注意是Authorization(授权),而不是Authentication(认证).用来做Authentication(认证)的标准叫做openid connect,我们将在以后的文章中进行介绍. 二.名词定义 理解OAuth中的专业术语能够帮助你理解其流程模式,OAuth中常用的名词术语有4个,为了便于理解这些术语,我们先假设一个很常见的授权场景: 你访问了一个日志网站(thir

OAuth在WebApi中的使用,前后台分离的调用方式

前段时间由于公司架构服务层向WebApi转换,就研究了OAuth在WebApi中的使用,这中间遇到了很多坑,在此记录一下OAuth的正确使用方式. 1.  OAuth是做什么的? 在网上浏览时,大家都见过这样的功能:网站A提供了第三方登录服务,比如使用新浪微博.QQ账户登录.用户使用第三方账户登陆后,第三方返回Token给网站A,当网站A调用第三方服务请求登录用户信息时需传递该Token给第三方,第三方才允许该服务请求.之后的每次请求无需再次认证,直接使用该Token即可.这就是OAuth的典型

net core3.1打造webapi开发框架的实践

https://www.cnblogs.com/datacool/p/datacool_dotnetcore_demo.html 实践技术看点 1.Swagger管理API说明文档 2.JwtBearer token验证 3.Swagger UI增加Authentication 4.EntityFrameworkCore+MySQL 5.在.net core 3.1下使用Log4net 前言 元旦过后就没什么工作上的任务了,这当然不能让领导看在眼里,动手实践一下新技术吧.于是准备搭一个webap

自己开发实现OAuth做webapi认证

一.作为认证服务器,首先需要提供一个可以通过appid/appsecret来获取token这样的一个接口,于是便有了以下代码. public class AuthController : ApiController { [HttpGet] public HttpResponseMessage Token(string appid = "", string appsecret = "") { ApiResponseEntity rep; var isv = AppMa

在ASP.NET Web API 2中使用Owin OAuth 刷新令牌

在上篇文章介绍了Web Api中使用令牌进行授权的后端实现方法,基于WebApi2和OWIN OAuth实现了获取access token,使用token访问需授权的资源信息.本文将介绍在Web Api中启用刷新令牌的后端实现. 本文要用到上篇文章所使用的代码,代码编写环境为VS 2017..Net Framework 4.7.2,数据库为MS SQL 2008 R2. OAuth 刷新令牌 上文已经搞了一套Token授权访问,这里有多出来一个刷新令牌(Refresh Token),平白添加程序

上周热点回顾(10.26-11.1)

热点随笔: · 手机淘宝UWP(MS-UAP)· 如何阻止SELECT * 语句(Woodytu)· 达内,青鸟!你们使多少花朵误入歧途!(麒麟子(Alex))· 一年后,我又回来啦(dreamfly_cc)· 使用OAuth打造webapi认证服务供自己的客户端使用(richieyang)· 前端优化带来的思考,浅谈前端工程化(叶小钗)· 为Visual Studio更换皮肤和背景图(米笑仙)· 记一次ASP.NET网站的入侵和如何避免被入侵(中国.NET研究协会)· 找工作面试,也打酱油.(

工作五年总结一二三流公司的常见特性

1.程序认证 一流的公司用加密cookie:二流公司用域认证:三流+的公司用session 2.提供页面服务 一流的公司采用路由:二流的公司采用.jsp..aspx..asp..py:然而.html提供服务的纯属垃圾公司. 3.前端框架 一流公司用自己框架.二流公司用angluar,backbone,react等.三流+用jquery,easyui等. 4.数据库 一流公司定制mysql和nosql产品.二流公司Oracle,CRC.三流盗版和正版的MSSQLSERVER 5.提供数据服务 一流