上篇文章介绍了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),平白添加程序的复杂度,意义何在呢

  • 刷新令牌在设置时,有几个选项,其中有一个AccessTokenExpireTimeSpan,即过期时间,针对不同的客户端过期时间该设置为多久呢
  • 而且令牌一旦生成即可在过期时间内一直使用,如果修改了用户的权限信息,如何通知到客户端其权限的变更
  • 还有就是访问令牌过期后,客户端调用需要重新验证用户名密码进行交互,这样是不是有点麻烦了?

使用刷新令牌

刷新令牌(Refresh Token) 是用来从身份认证服务器交换获得新的访问令牌,允许在没有用户交互的情况下请求新的访问令牌。刷新令牌有几个好处:

  • 可以无需用户交互情况下更新访问令牌:可以设置一个较长时间的刷新令牌有效期,客户端一次身份验证后除非管理员撤销刷新令牌,否则在刷新令牌有效期内均不用再次身份验证
  • 可以撤销已通过身份验证的用户的授权: 只要用户获取到访问令牌,除非自己编写单独的处理逻辑,否则是没法撤销掉访问令牌,但是刷新令牌相对简单的多

这个在没有特定的业务场景比较难理解,下面还是一步一步操作一遍,动动手后会有更多收获。本文需要使用进行客户端和刷新令牌的持久化,需要用到EF和数据库客户端。

步骤一:设计客户端表、刷新令牌表,启用持久化操作

  1. 启用EntityFramework,安装 Nuget包

    install-package Entityframework

  2. 添加数据实体,项目右键,新建文件夹命名为Entities,然后文件夹右键,新增类命名为OAuthContext,代码如下:

    using System.Data.Entity;
    
    namespace OAuthExample.Entities
    {
    public class OAuthContext : DbContext
    {
    public OAuthContext() : base("OAuthConnection") { } public DbSet<Client> Clients { get; set; }
    public DbSet<RefreshToken> RefreshTokens { get; set; }
    }
    }
  3. 添加客户端、刷新令牌实体 ,在文件夹右键,分别新增Client类、RefreshToken类,代码如下:

    Client 实体:

    using System.ComponentModel.DataAnnotations;
    
    namespace OAuthExample.Entities
    {
    public class Client
    {
    [Key]
    public string Id { get; set; } [Required]
    public string Secret { get; set; }
    [Required]
    [MaxLength()]
    public string Name { get; set; } public ApplicationTypes ApplicationType { get; set; } public bool Active { get; set; } public int RefreshTokenLifeTime { get; set; } [MaxLength()]
    public string AllowedOrigin { get; set; }
    } public enum ApplicationTypes
    {
    JavaScript = ,
    NativeConfidential =
    };
    }

    RefreshToken 实体:

    using System;
    using System.ComponentModel.DataAnnotations; namespace OAuthExample.Entities
    {
    public class RefreshToken
    {
    [Key]
    public string Id { get; set; } [Required]
    [MaxLength()]
    public string Subject { get; set; } [Required]
    [MaxLength()]
    public string ClientId { get; set; } public DateTime IssuedUtc { get; set; } public DateTime ExpiresUtc { get; set; } [Required]
    public string ProtectedTicket { get; set; }
    }
    }
  4. 进行数据库迁移 ,在程序包管理器控制台分别运行如下命令:

    PM> enable-migrations
    
    PM> add-migration initDatabase
    
    PM> update-database
  5. 实现仓储类,在项目中添加文件夹,命名为Infrastructure,然后添加类,命名为 AuthRepository ,代码如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using OAuthExample.Entities; namespace OAuthExample.Infrastructure
    {
    public class AuthRepository : IDisposable
    {
    private OAuthContext context; public AuthRepository()
    {
    context = new OAuthContext();
    } public Client FindClient(string clientId)
    {
    var client = context.Clients.Find(clientId); return client;
    } public async Task<bool> AddRefreshToken(RefreshToken token)
    {
    var existingToken = context.RefreshTokens.Where(r => r.Subject == token.Subject && r.ClientId == token.ClientId).SingleOrDefault(); if (existingToken != null)
    {
    var result = await RemoveRefreshToken(existingToken);
    } context.RefreshTokens.Add(token); return await context.SaveChangesAsync() > ;
    } public async Task<bool> RemoveRefreshToken(string refreshTokenId)
    {
    var refreshToken = await context.RefreshTokens.FindAsync(refreshTokenId); if (refreshToken != null)
    {
    context.RefreshTokens.Remove(refreshToken);
    return await context.SaveChangesAsync() > ;
    } return false;
    } public async Task<bool> RemoveRefreshToken(RefreshToken refreshToken)
    {
    context.RefreshTokens.Remove(refreshToken);
    return await context.SaveChangesAsync() > ;
    } public async Task<RefreshToken> FindRefreshToken(string refreshTokenId)
    {
    var refreshToken = await context.RefreshTokens.FindAsync(refreshTokenId); return refreshToken;
    } public List<RefreshToken> GetAllRefreshTokens()
    {
    return context.RefreshTokens.ToList();
    } public void Dispose()
    {
    context.Dispose();
    }
    }
    }

到这里终于完成了Client与RefreshToken两个实体表的管理,这里主要是实现一下Client和RefreshToken这两个实体的一些增删改查操作,在后面会用到。具体实现方式不限于以上代码。

这里有个小插曲,执行数据迁移的时候出现错误 “无法将参数绑定到参数“Path”,因为该参数是空值。”,卡了半个小时也没解决,最后卸载掉当前EntityFramework,换了个低版本的,一切正常了。

步骤二:实现Client验证

现在,我们需要实现负责验证发送给应用程序请求访问令牌或使用刷新令牌的客户端信息的客户端信息的逻辑,因此打开文件“ CustomAuthorizationServerProvider”修改方法ValidateClientAuthentication,代码如下:

public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{ string clientId = string.Empty;
string clientSecret = string.Empty;
Client client = null; if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
} if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return Task.FromResult<object>(null);
} using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
} if (client == null)
{
context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
return Task.FromResult<object>(null);
} if (client.ApplicationType == ApplicationTypes.NativeConfidential)
{
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret should be sent.");
return Task.FromResult<object>(null);
}
else
{
if (client.Secret != Helper.GetHash(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return Task.FromResult<object>(null);
}
}
} if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return Task.FromResult<object>(null);
} context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); context.Validated();
return Task.FromResult<object>(null);
}

上述操作,我们主要执行了一下验证步骤

  • 在请求标头中获取clientId 和Client Secret
  • 检查Client Id是否为空
  • 检查Client是否注册
  • 检查Client是否需要进行Client Secret验证的客户端类型,在此上下文中,NativeConfidential时需要验证Javascript不需要验证
  • 检查Client是否处于活动状态
  • 以上所有均为有效时,将上下文标记为有效,验证通过

步骤三:验证资源所有者凭证

现在,我们需要修改方法“ GrantResourceOwnerCredentials”以验证资源所有者的用户名/密码是否正确,并将客户端ID绑定到生成的访问令牌,因此打开文件“ CustomAuthorizationServerProvider”并修改GrantResourceOwnerCredentials方法和添加TokenEndpoint实现代码:

/// <summary>
/// Called when a request to the Token endpoint arrives with a "grant_type" of "password". This occurs when the user has provided name and password
/// credentials directly into the client application's user interface, and the client application is using those to acquire an "access_token" and
/// optional "refresh_token". If the web application supports the
/// resource owner credentials grant type it must validate the context.Username and context.Password as appropriate. To issue an
/// access token the context.Validated must be called with a new ticket containing the claims about the resource owner which should be associated
/// with the access token. The application should take appropriate measures to ensure that the endpoint isn’t abused by malicious callers.
/// The default behavior is to reject this grant type.
/// See also http://tools.ietf.org/html/rfc6749#section-4.3.2
/// </summary>
/// <param name="context">The context of the event carries information in and results out.</param>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
if (allowedOrigin == null)
{
allowedOrigin = "*";
}
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
//这里是验证用户名和密码,可以根据项目情况自己实现
if (!(context.UserName == "zhangsan" && context.Password == ""))
{
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("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},
{
"userName", context.UserName
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket); } /// <summary>
/// Called at the final stage of a successful Token endpoint request. An application may implement this call in order to do any final
/// modification of the claims being used to issue access or refresh tokens. This call may also be used in order to add additional
/// response parameters to the Token endpoint's json response body.
/// </summary>
/// <param name="context">The context of the event carries information in and results out.</param>
/// <returns>
/// Task to enable asynchronous execution
/// </returns>
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (var item in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(item.Key, item.Value);
}
return Task.FromResult<object>(null); }

通过上面代码,我们完成了如下操作:

  • 从OWIN中获取到“Access-Control-Allow-Origin”并添加到OWIN上下文响应中
  • 验证资源所有者的用户名/密码,这里是写死的,实际应用中可以自己扩展一下,验证完成后调用context.Validated(ticket),将生成token

步骤四:生成Refresh Token,并持久化

现在我们需要生成Refresh Token并实现持久化到数据库中,我们需要添加一个新的实现类。项目中找到Providers文件夹,右键添加类,命名为”CustomRefreshTokenProvider”,该类继承于”IAuthenticationTokenProvider”,实现代码如下:

using System;
using System.Threading.Tasks;
using log4net;
using Microsoft.Owin.Security.Infrastructure;
using OAuthExample.Entities;
using OAuthExample.Infrastructure; namespace OAuthExample.Providers
{
public class CustomRefreshTokenProvider : IAuthenticationTokenProvider
{
private ILog logger = LogManager.GetLogger(typeof(CustomRefreshTokenProvider)); public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
/// <summary>
/// Creates the asynchronous.
/// </summary>
/// <param name="context">The context.</param>
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clientid = context.Ticket.Properties.Dictionary["as:client_id"]; if (string.IsNullOrEmpty(clientid))
{
return;
}
//为刷新令牌生成一个唯一的标识符,这里我们使用Guid,也可以自己单独写一个字符串生成的算法
var refreshTokenId = Guid.NewGuid().ToString("n"); using (AuthRepository _repo = new AuthRepository())
{
//从Owin上下文中读取令牌生存时间,并将生存时间设置到刷新令牌
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime"); var token = new RefreshToken()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
//为刷新令牌设置有效期
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;
//负责对票证内容进行序列化,稍后我们将次序列化字符串持久化到数据
token.ProtectedTicket = context.SerializeTicket();
var result = await _repo.AddRefreshToken(token); //在响应中文中发送刷新令牌Id
if (result)
{
context.SetToken(refreshTokenId);
}
}
} public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
/// <summary>
/// Receives the asynchronous.
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
/// <exception cref="System.NotImplementedException"></exception>
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
//设置跨域访问
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin }); //获取到刷新令牌,hash后在数据库查找是否已经存在
string hashedTokenId = Helper.GetHash(context.Token);
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);
}
}
}
}
}

通过上面代码,我们完成了如下操作:

  • 在CreateAsync 方法中

    我们在此方法实现了刷新令牌的设置和生成,并持久化到数据。添加此方法后,在获取access token的过程中,需要将client Id添加到Authorization中,验证通过后,在响应报文中生成了refresh_token

  • 在ReceiveAsync 方法中
  • 我们在此方法实现了通过刷新令牌获取访问令牌的一部分,详见代码注释

步骤五:使用刷新令牌生成访问令牌

打开CustomRefreshTokenProvider类,添加实现接口方法ReceiveAsync 。代码见上

打开CustomAuthorizationServerProvider类,添加GrantRefreshToken方法的实现,代码如下:

/// <summary>
/// Called when a request to the Token endpoint arrives with a "grant_type" of "refresh_token". This occurs if your application has issued a "refresh_token"
/// along with the "access_token", and the client is attempting to use the "refresh_token" to acquire a new "access_token", and possibly a new "refresh_token".
/// To issue a refresh token the an Options.RefreshTokenProvider must be assigned to create the value which is returned. The claims and properties
/// associated with the refresh token are present in the context.Ticket. The application must call context.Validated to instruct the
/// Authorization Server middleware to issue an access token based on those claims and properties. The call to context.Validated may
/// be given a different AuthenticationTicket or ClaimsIdentity in order to control which information flows from the refresh token to
/// the access token. The default behavior when using the OAuthAuthorizationServerProvider is to flow information from the refresh token to
/// the access token unmodified.
/// See also http://tools.ietf.org/html/rfc6749#section-6
/// </summary>
/// <param name="context">The context of the event carries information in and results out.</param>
/// <returns>
/// Task to enable asynchronous execution
/// </returns>
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
//从原始票证中读取Client Id与当前的Client Id进行比较,如果不同,将拒绝次操作请求,以保证刷新令牌生成后绑定到同一个Client
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId; if (originalClient != currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
} // Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity); var newClaim = newIdentity.Claims.Where(c => c.Type == "newClaim").FirstOrDefault();
if (newClaim != null)
{
newIdentity.RemoveClaim(newClaim);
}
newIdentity.AddClaim(new Claim("newClaim", "newValue")); var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
//方法执行后,代码将回到CustomRefreshTokenProvider中的"CreateAsync"方法,生成新的刷新令牌,并将其和新的访问令牌一起返回到响应中
context.Validated(newTicket); return Task.FromResult<object>(null);
}

代码测试

使用PostMan进行模拟测试

在未授权时,访问 http://localhost:56638/api/Orders,提示“已拒绝为此请求授权”

获取授权,访问 http://localhost:56638/oauth/token,获得的报文中包含有refresh_token

使用Refresh token获取新的Access token

使用新的Access Token 附加到Order请求,再次尝试访问:

监视请求上下文中的信息如下,注意newClaim是刷新令牌访问时才设置的:

总结

dddd,终于把这两个总结搞完了,回头把之前webapi参数加密的合到一起,代码整理一下放到文末。

本文参照了很多文章以及代码,文章主要架构与下面链接基本一致,其实也没多少原创,但是在整理总结的过程中梳理了一边Access Token 和Refresh Token的知识,当你在组织语言解释代码的时候,才无情的揭露了自己的无知与浅薄,受益匪浅~

Token Based Authentication using ASP.NET Web API 2, Owin, and Identity

Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin

其他参考资料是在较多,有的看一点就关掉了。有的作为参考,列举一二

Web API与OAuth:既生access token,何生refresh token

在ASP.NET中基于Owin OAuth使用Client Credentials Grant授权发放Token

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

ASP.NET Web API与Owin OAuth:调用与用户相关的Web API

C#进阶系列——WebApi 身份认证解决方案:Basic基础认证

最后本文示例代码地址:Github

在ASP.NET Web API 2中使用Owin OAuth 刷新令牌(示例代码)的更多相关文章

  1. 在ASP.NET Web API 2中使用Owin基于Token令牌的身份验证

    基于令牌的身份验证 基于令牌的身份验证主要区别于以前常用的常用的基于cookie的身份验证,基于cookie的身份验证在B/S架构中使用比较多,但是在Web Api中因其特殊性,基于cookie的身份 ...

  2. ASP.NET Web API 2 中的属性路由使用(转载)

    转载地址:ASP.NET Web API 2 中的属性路由使用

  3. 在ASP.NET Web API项目中使用Hangfire实现后台任务处理

    当前项目中有这样一个需求:由前端用户的一个操作,需要触发到不同设备的消息推送.由于推送这个具体功能,我们采用了第三方的服务.而这个服务调用有时候可能会有延时,为此,我们希望将消息推送与用户前端操作实现 ...

  4. ASP.NET Web API 2 中的特性路由

    ASP.NET MVC 5.1 开始已经支持基于特性的路由(http://attributerouting.net),ASP.NET WEB API 2 同时也支持了这一特性. 启用特性路 由只需要在 ...

  5. ASP.NET Web API 2中的错误处理

    前几天在webapi项目中遇到一个问题:Controller构造函数中抛出异常时全局过滤器捕获不到,于是网搜一把写下这篇博客作为总结. HttpResponseException 通常在WebAPI的 ...

  6. [翻译]ASP.NET Web API 2 中的全局错误处理

    目录 已存在的选项 解决方案预览 设计原则 什么时候去用 方案详情 示例 附录: 基类详情 原文链接 Global Error Handling in ASP.NET Web API 2 由于翻译水平 ...

  7. (转)【ASP.NET Web API】Authentication with OWIN

    概述 本文说明了如何使用 OWIN 来实现 ASP.NET Web API 的验证功能,以及在客户端与服务器的交互过程中,避免重复提交用户名和密码的机制. 客户端可以分为两类: JavaScript: ...

  8. integration asp.net web api with autofac and owin

    There is an example project showing Web API in conjunction with OWIN self hosting https://github.com ...

  9. swaggerui在asp.net web api core 中的应用

    Swaggerui 可以为我们的webapi提供美观的在线文档,如下图: 实现步骤: NuGet Packages  Install-Package Swashbuckle.AspNetCore 在s ...

随机推荐

  1. 【已采纳】新项目第一次怎么上传到github里面

      言归正传,最近学习了怎么将新创建的本地代码上传到github上,这里简单的记录一下,我喜欢使用命令行,这里全用命令行来实现,不了解git命令的可以去了解下. 第一步:建立git仓库 cd到你的本地 ...

  2. STM32F4 串口IAP程序要点

    1. IAP(bootloader)程序 1.1 内部Flash地址分配 /* Start of the Flash address */ #define STM32_FLASH_BASE 0x080 ...

  3. React 受控组件和非受控组件

    需求用户名自动获取 onChange用户状态发生改变 就获取值 就是时时获取值 使用onChange 点击按钮 获取密码 只要绑定了点击事件 就可以获取值 通过 let usercont=event. ...

  4. E07【餐厅】What would you recommend?

    核心句型 What would you recommend? 你有什么推荐吗? 场景对话: A:What do  you  want  to  eat? 你想吃点什么? B:I don't know. ...

  5. 微信小程序 - 结构目录 | 配置介绍

    结构目录 小程序框架提供了自己的视图层描述语言 WXML 和 WXSS,以及 JavaScript,并在视图层与逻辑层间提供了数据传输和事件系统,让开发者能够专注于数据与逻辑. 一.小程序文件结构和传 ...

  6. USACO River Crossing

    洛谷 P2904 [USACO08MAR]跨河River Crossing https://www.luogu.org/problem/P2904 JDOJ 2574: USACO 2008 Mar ...

  7. plsql导入.dmp步骤

    https://blog.csdn.net/yudianxiaoxiao/article/details/78231143     plsql导入.sql和.dmp文件时,会经常用到,对于初学者来说可 ...

  8. Educational Codeforces Round 57 (Rated for Div. 2) D dp

    https://codeforces.com/contest/1096/problem/D 题意 给一个串s,删掉一个字符的代价为a[i],问使得s的子串不含"hard"的最小代价 ...

  9. 测试总结(没有rank1)

    一个初三蒟蒻不可能rank1.jpg T1: 给出两个单词 (开始单词和结束单词) 以及一个词典. 找出从开始单词转换到结束单词,所需要的最短转换序列.转换的规则如下:1.每次只能改变一个字母2.转换 ...

  10. C#面试题(一)

    一.背景 最近学习巩固C#的基础,看到<小胖李的面试之旅>的文章,把他的面试题目拿来总结以及学习. 二.面试题 1.wcf想用http协议通信怎么做? 答:在配置文件里面配置,因为wcf默 ...