前一篇博文中,我们初步地了解了refresh token的用途——它是用于刷新access token的一种token,并且用简单的示例代码体验了一下获取refresh token并且用它刷新access token。在这篇博文中,我们来进一步探索refresh token。

之前只知道refresh token是用于刷新access token的,却不知道refresh token凭什么可以刷新access token?知其然,却不知其所以然。

这是由于之前没有发现refresh token与access token有1个非常重要的区别——Refresh token只是一种标识,不包含任何信息;而access token是经过序列化并加密的授权信息,发送到服务器时,会被解密并从中读取授权信息。正是因为access token包含的是信息,信息是易变的,所以它的过期时间很短;正是因为refresh token只是一种标识,不易变,所以生命周期可以很长。这才是既生access token,何生refresh token背后的真正原因。

在前一篇博文中,我们将refresh token存储在ConcurrentDictionary类型的静态变量中,只要程序重启,refresh token及相关信息就会丢失。为了给refresh token的生命周期保驾护航,我们不得不干一件经常干的事情——持久化,这篇博文也是因此而生。

要持久化,首先想到的就是Entity Framework与数据库,但我们目前的Web API只有2个客户端,一个是iOS App,一个是单元测试代码,用EF+数据库有如杀鸡用牛刀。何不换一种简单的方式?直接序列化为josn格式,然后保存在文件中。这么想,也这么干了。

下面就来分享一下我们如何用文件存储实现refresh token的持久化。

首先定义一个RefreshToken实体:

public class RefreshToken
{
public string Id { get; set; } public string UserName { get; set; } public Guid ClientId { get; set; } public DateTime IssuedUtc { get; set; } public DateTime ExpiresUtc { get; set; } public string ProtectedTicket { get; set; }
}

这个RefreshToken实体不仅仅包含refresh token(对应于这里的Id属性),而且包含refresh token所关联的信息。因为refresh token是用于刷新accesss token的,如果没有这些关联信息,就无法生成access token。

接下来,我们在Application层定义一个与RefreshToken相关的服务接口IRefreshTokenService。虽然只是一个很简单的程序,我们还是使用n层架构来做,不管多小的项目,分离关注、减少依赖总是有帮助的,最起码可以增添写代码的乐趣。

namespace CNBlogs.OpenAPI.Application.Interfaces
{
public interface IRefreshTokenService
{
Task<RefreshToken> Get(string Id);
Task<bool> Save(RefreshToken refreshToken);
Task<bool> Remove(string Id);
}
}

IRefreshTokenService接口定义了3个方法:Get()用于在刷新access token时获取RefreshToken,Save()与Remove()用于在生成refresh token时将新RefreshToken保存并将旧RefreshToken删除。

定义好IRefreshTokenService接口之后,就可以专注OAuth部分的实现,持久化的实现部分暂且丢在一边(分离关注[注意力]的好处在这里就体现啦)。

OAuth部分的实现主要在CNBlogsRefreshTokenProvider(继承自AuthenticationTokenProvider),实现代码如下:

public class CNBlogsRefreshTokenProvider : AuthenticationTokenProvider
{
private IRefreshTokenService _refreshTokenService; public CNBlogsRefreshTokenProvider(IRefreshTokenService refreshTokenService)
{
_refreshTokenService = refreshTokenService;
} public override async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clietId = context.OwinContext.Get<string>("as:client_id");
if (string.IsNullOrEmpty(clietId)) return; var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
if (string.IsNullOrEmpty(refreshTokenLifeTime)) return; //generate access token
RandomNumberGenerator cryptoRandomDataGenerator = new RNGCryptoServiceProvider();
byte[] buffer = new byte[5];
cryptoRandomDataGenerator.GetBytes(buffer);
var refreshTokenId = Convert.ToBase64String(buffer).TrimEnd('=').Replace('+', '-').Replace('/', '_'); var refreshToken = new RefreshToken()
{
Id = refreshTokenId,
ClientId = new Guid(clietId),
UserName = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddSeconds(Convert.ToDouble(refreshTokenLifeTime)),
ProtectedTicket = context.SerializeTicket()
}; context.Ticket.Properties.IssuedUtc = refreshToken.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = refreshToken.ExpiresUtc; if (await _refreshTokenService.Save(refreshToken))
{
context.SetToken(refreshTokenId);
}
} public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var refreshToken = await _refreshTokenService.Get(context.Token); if (refreshToken != null)
{
context.DeserializeTicket(refreshToken.ProtectedTicket);
var result = await _refreshTokenService.Remove(context.Token);
}
}
}

代码解读:

  • 为了调用IRefreshTokenService,我们将之通过CNBlogsRefreshTokenProvider的构造函数注入。
  • CreateAsync() 中用RNGCryptoServiceProvider生成refresh token,并获取相关信息(比如clientId, refreshTokenLifeTime, ProtectedTicket),创建RefreshToken,调用 IRefreshTokenService.Save() 进行持久化保存。
  • ReceiveAsync() 中调用 IRefreshTokenService.Get() 获取 RefreshToken,用它反序列出生成access token所需的ticket,从持久化中删除旧的refresh token(刷新access token时,refresh token也会重新生成)。

由于在CNBlogsRefreshTokenProvider中需要获取Client的clientId与refreshTokenLifeTime信息,所以我们需要在CNBlogsAuthorizationServerProvider中提供这个信息,在ValidateClientAuthentication重载方法中添加如下的代码:

context.OwinContext.Set<string>("as:client_id", clientId);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());

以下是精简过的CNBlogsAuthorizationServerProvider完整实现代码(我们对client也用文件存储进行了持久化):

public class CNBlogsAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private IClientService _clientService; public CNBlogsAuthorizationServerProvider(IClientService clientService)
{
_clientService = clientService;
} public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret; //省略了return之前context.SetError的代码
if (!context.TryGetBasicCredentials(out clientId, out clientSecret)) { return; } var client = await _clientService.Get(clientId);
if (client == null) { return; }
if (client.Secret != clientSecret) { return;} context.OwinContext.Set<string>("as:client_id", clientId);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); context.Validated(clientId);
} public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); context.Validated(oAuthIdentity);
} public override async Task GrantResourceOwnerCredentials(
OAuthGrantResourceOwnerCredentialsContext context)
{
//验证context.UserName与context.Password
var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
context.Validated(oAuthIdentity);
} public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var newId = new ClaimsIdentity(context.Ticket.Identity);
newId.AddClaim(new Claim("newClaim", "refreshToken"));
var newTicket = new AuthenticationTicket(newId, context.Ticket.Properties);
context.Validated(newTicket);
}
}

OAuth部分的主要代码完成后,接下来丢开OAuth,专心实现持久化部分的代码(分层带来的关注分离的好处再次体现)。

先实现Repository层的代码(Application层的接口已完成),定义IRefreshTokenRepository接口:

namespace CNBlogs.OpenAPI.Repository.Interfaces
{
public interface IRefreshTokenRepository
{
Task<RefreshToken> FindById(string Id); Task<bool> Insert(RefreshToken refreshToken); Task<bool> Delete(string Id);
}
}

然后以RefreshTokenRepository实现IRefreshTokenRepository接口,用文件存储进行持久化的实现代码都在这里(就是json的序列化与反序列化):

namespace CNBlogs.OpenAPI.Repository.FileStorage
{
public class RefreshTokenRepository : IRefreshTokenRepository
{
private string _jsonFilePath;
private List<RefreshToken> _refreshTokens; public RefreshTokenRepository()
{
_jsonFilePath = HostingEnvironment.MapPath("~/App_Data/RefreshToken.json");
if (File.Exists(_jsonFilePath))
{
var json = File.ReadAllText(_jsonFilePath);
_refreshTokens = JsonConvert.DeserializeObject<List<RefreshToken>>(json); }
if(_refreshTokens == null) _refreshTokens = new List<RefreshToken>();
} public async Task<RefreshToken> FindById(string Id)
{
return _refreshTokens.Where(x => x.Id == Id).FirstOrDefault();
} public async Task<bool> Insert(RefreshToken refreshToken)
{
_refreshTokens.Add(refreshToken);
await WriteJsonToFile();
return true;
} public async Task<bool> Delete(string Id)
{
_refreshTokens.RemoveAll(x => x.Id == Id);
await WriteJsonToFile();
return true;
} private async Task WriteJsonToFile()
{
using (var tw = TextWriter.Synchronized(new StreamWriter(_jsonFilePath, false)))
{
await tw.WriteAsync(JsonConvert.SerializeObject(_refreshTokens, Formatting.Indented));
}
}
}
}

接着就是Application层接口IRefreshTokenService的实现(调用Repository层的接口):

namespace CNBlogs.OpenAPI.Application.Services
{
public class RefreshTokenService : IRefreshTokenService
{
private IRefreshTokenRepository _refreshTokenRepository; public RefreshTokenService(IRefreshTokenRepository refreshTokenRepository)
{
_refreshTokenRepository = refreshTokenRepository;
} public async Task<RefreshToken> Get(string Id)
{
return await _refreshTokenRepository.FindById(Id);
} public async Task<bool> Save(RefreshToken refreshToken)
{
return await _refreshTokenRepository.Insert(refreshToken);
} public async Task<bool> Remove(string Id)
{
return await _refreshTokenRepository.Delete(Id);
}
}
}

好了,主要工作都已完成:

1)Web层的CNBlogsAuthorizationServerProvider与CNBlogsRefreshTokenProvider

2)Domain层的实体RefreshToken

3)Application层的IRefreshTokenService与RefreshTokenService.cs

4)Repository层的IRefreshTokenRepository与RefreshTokenRepository

麻雀虽小,五脏俱全。

最后就剩下一些收尾工作了。

由于调用的接口都是通过构造函数注入的,需要做一些依赖注入的工作,实现DependencyInjectionConfig:

namespace OpenAPI.App_Start
{
public static class DependencyInjectionConfig
{
public static void Register()
{
var containter = IocContainer.Default = new IocUnityContainer();
containter.RegisterType<IRefreshTokenService, RefreshTokenService>();
containter.RegisterType<IRefreshTokenRepository, RefreshTokenRepository>();
}
}
}

(注:IocContainer是我们内部用的组件,封装了Unity)

然后在Application_Start中调用它。

到这里就万事俱备,只欠东风了。

只要在Startup.Auth.cs中通过IOC容器解析出CNBlogsAuthorizationServerProvider与CNBlogsRefreshTokenProvider的实例,东风就来了。

public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; } public void ConfigureAuth(IAppBuilder app)
{
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = IocContainer.Resolver.Resolve<CNBlogsAuthorizationServerProvider>(),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(),
AllowInsecureHttp = true,
RefreshTokenProvider = IocContainer.Resolver.Resolve<CNBlogsRefreshTokenProvider>()
}; app.UseOAuthBearerTokens(OAuthOptions);
}
}

至此,开发第一版给iOS App用的Web API所面临的OAuth问题基本解决了。这些博文只是解决实际问题之后的一点记载,希望能让想基于ASP.NET OWIN OAuth开发Web API的朋友少走一些弯路。

【参考资料】

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

ASP.NET OWIN OAuth:refresh token的持久化的更多相关文章

  1. ASP.NET OWIN OAuth:遇到的2个refresh token问题

    之前写过2篇关于refresh token的生成与持久化的博文:1)Web API与OAuth:既生access token,何生refresh token:2)ASP.NET OWIN OAuth: ...

  2. ASP.NET OAuth:access token的加密解密,client secret与refresh token的生成

    在 ASP.NET OWIN OAuth(Microsoft.Owin.Security.OAuth)中,access token 的默认加密方法是: 1) System.Security.Crypt ...

  3. asp.net core使用identity+jwt保护你的webapi(三)——refresh token

    前言 上一篇已经介绍了identity的注册,登录,获取jwt token,本篇来完成refresh token. 开始 开始之前先说明一下为什么需要refresh token. 虽然jwt toke ...

  4. ASP.NET OAuth:解决refresh token无法刷新access token的问题

    最近同事用iOS App调用Open API时遇到一个问题:在access token过期后,用refresh token刷新access token时,服务器响应"invalid_gran ...

  5. 在ASP.NET Web API 2中使用Owin OAuth 刷新令牌(示例代码)

    在上篇文章介绍了Web Api中使用令牌进行授权的后端实现方法,基于WebApi2和OWIN OAuth实现了获取access token,使用token访问需授权的资源信息.本文将介绍在Web Ap ...

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

    在前一篇博文中,我们基于 ASP.NET Web API 与 OWIN OAuth 以 Resource Owner Password Credentials Grant 的授权方式( grant_t ...

  7. 在WebApi中基于Owin OAuth使用授权发放Token

    如何基于Microsoft.Owin.Security.OAuth,使用Client Credentials Grant授权方式给客户端发放access token? Client Credentia ...

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

    在前一篇博文中,我们通过以 OAuth 的 Client Credential Grant 授权方式(只验证调用客户端,不验证登录用户)拿到的 Access Token ,成功调用了与用户无关的 We ...

  9. Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token

    来源:   https://www.c-sharpcorner.com/article/handle-refresh-token-using-asp-net-core-2-0-and-json-web ...

随机推荐

  1. Angular2学习笔记(1)

    Angular2学习笔记(1) 1. 写在前面 之前基于Electron写过一个Markdown编辑器.就其功能而言,主要功能已经实现,一些小的不影响使用的功能由于时间关系还没有完成:但就代码而言,之 ...

  2. 【.net 深呼吸】细说CodeDom(1):结构大观

    CodeDom 是啥东东?Html Dom听过吧,XML Dom听过吧.DOM一般可翻译为 文档对象模型,那 Code + DOM呢,自然是指代码文档模型了.如果你从来没接触过 CodeDom,你大概 ...

  3. Spring基于AOP的事务管理

                                  Spring基于AOP的事务管理 事务 事务是一系列动作,这一系列动作综合在一起组成一个完整的工作单元,如果有任何一个动作执行失败,那么事务 ...

  4. Bootstrap-Select 动态加载数据的小记

    关于前端框架系列的可以参考我我刚学Bootstrap时候写的LoT.UI http://www.cnblogs.com/dunitian/p/4822808.html#lotui bootstrap- ...

  5. Android业务组件化之现状分析与探讨

    前言: 从个人经历来说的话,从事APP开发这么多年来,所接触的APP的体积变得越来越大,业务的也变得越来越复杂,总来来说只有一句话:这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一 ...

  6. Angular企业级开发(1)-AngularJS简介

    AngularJS介绍 AngularJS是一个功能完善的JavaScript前端框架,同时是基于MVC(Model-View-Controller理念的框架,使用它能够高效的开发桌面web app和 ...

  7. 【Reading Note】算法读书杂记

    1 排序 排序基本信息 稳定性:排序前大的数在排序后,大的数依然保持不变就是稳定排序,反之不稳定 内外排序:根据待排序的记录是否放在内存里面区分的.诸如:插入排序(直接插入&希尔).交换排序( ...

  8. C# 索引器,实现IEnumerable接口的GetEnumerator()方法

    当自定义类需要实现索引时,可以在类中实现索引器. 用Table作为例子,Table由多个Row组成,Row由多个Cell组成, 我们需要实现自定义的table[0],row[0] 索引器定义格式为 [ ...

  9. 著名ERP厂商的SSO单点登录解决方案介绍一

          SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次主要的登录映射到其他应用中用于同一个用户 ...

  10. 分布式学习系列【dubbo入门实践】

    分布式学习系列[dubbo入门实践] dubbo架构 组成部分:provider,consumer,registry,monitor: provider,consumer注册,订阅类似于消息队列的注册 ...