ASP.NET OWIN OAuth:refresh token的持久化

前一篇博文中,我们初步地了解了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[50];
        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>();
        }
    }
}

然后在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(1),
            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

时间: 2024-10-29 19:08:19

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

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

之前写过2篇关于refresh token的生成与持久化的博文:1)Web API与OAuth:既生access token,何生refresh token:2)ASP.NET OWIN OAuth:refresh token的持久化. 之后我们在CNBlogsRefreshTokenProvider中这样实现了refresh token的生成与持久化: public class CNBlogsRefreshTokenProvider : AuthenticationTokenProvider

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

在ASP.NET OWIN OAuth(Microsoft.Owin.Security.OAuth)中,access token 的默认加密方法是: 1) System.Security.Cryptography.DpapiDataProtector.Protect() 2) Convert.ToBase64String() 3) .TrimEnd('=').Replace('+', '-').Replace('/', '_'); access token 的默认解密方法是: 1) System

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

最近同事用iOS App调用Open API时遇到一个问题:在access token过期后,用refresh token刷新access token时,服务器响应"invalid_grant"错误:而在access token没有过期的情况下,能正常刷新access token. 先查看了一下OAuth规范中的“Refreshing an Expired Access Token”流程图,以确认客户端的操作流程有没有问题. 问题发生在上图中的(G)操作步骤.iOS App就是按上图的

在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),平白添加程序

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

如何基于Microsoft.Owin.Security.OAuth,使用Client CredentialsGrant授权方式给客户端发放access token? Client CredentialsGrant的授权方式就是只验证客户端(Client),不验证用户(Resource Owner),只要客户端通过验证就发accesstoken.举一个对应的应用场景例子,比如我们想提供一个“获取网站首页最新博文列表”的WebAPI给客户端App调用.由于这个数据与用户无关,所以不涉及用户登录与授权

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

在前一篇博文中,我们基于 ASP.NET Web API 与 OWIN OAuth 以 Resource Owner Password Credentials Grant 的授权方式( grant_type=password )获取到了 access token,并以这个 token 成功调用了与当前用户(resource owner)关联的 Web API. 本以为搞定了 access token 就搞定了 Web API 的验证与授权问题,可是发现 OAuth 中还有一种 token,叫 r

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

参考页面: http://www.yuanjiaocheng.net/webapi/web-api-route.html http://www.yuanjiaocheng.net/webapi/parameter-binding.html http://www.yuanjiaocheng.net/webapi/action-method-returntype.html http://www.yuanjiaocheng.net/webapi/web-api-reqresq-format.html

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-token/ In this article , you will learn how to deal with the refresh token when you use jwt (JSON Web Token) as your access_token. Backgroud Many peo

Owin.OAuth

概念 OAuth2四种模式 授权码模式(authorization code) 简化模式(implicit) 密码模式(resource owner password credentials) 客户端模式(client credentials) 授权码模式 QQ,微信,等方式 用户访问客户端,后者将前者导向认证服务器. 用户选择是否给予客户端授权. 假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码. 客户端收到