zoukankan      html  css  js  c++  java
  • 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>();
            }
        }
    }

    (注: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(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 

  • 相关阅读:
    IOS UI NavigationController结构
    IOS UI 自定义navigationBar布局
    IOS UI 代码界面跳转常用方式
    IOS OC 多态(白话)
    IOS OC NSArray&NSMutableArray
    IOS OC NSString基础知识
    NSTimer做一个小计时器
    IOS UI 代码创建UIButton,UITextField,UILabel
    [HNOI2010]平面图判定
    [SDOI2017]树点涂色
  • 原文地址:https://www.cnblogs.com/dudu/p/oauth-refresh-token-persistence.html
Copyright © 2011-2022 走看看