ABP中使用OAuth2(Resource Owner Password Credentials Grant模式)


      ABP目前的認證方式有兩種,一種是基於Cookie的登錄認證,一種是基於token的登錄認證。使用Cookie的認證方式一般在PC端用得比較多,使用token的認證方式一般在移動端用得比較多。ABP自帶的Token認證方式通過UseOAuthBearerAuthentication啟用的,既然已經自帶了Token的認證方式,為什么還要使用OAuth2呢?使用此方式是無法實現Token的刷新的,Token過期后必須通過用戶名和密碼重新登錄,這樣客戶端會彈出登錄框讓用戶登錄,用戶體驗不是很好,當然也可以在客戶端存儲用戶名和密碼,發現Token過期后,在后台自動登錄,這樣用戶也是不知道的,只是存在賬號安全問題(其實這些都不是問題,主要原因是使用OAuth2后B格更高)。下面我們來看一下怎么在ABP中使用OAuth2:

1.到ABP的官網上下載一個自動生成的項目模板

2.添加OAuth相關的代碼

  a) 添加一個SimpleAuthorizationServerProvider類,用於驗證客戶端和用戶名密碼,網上能夠找到類似的代碼,直接拿來修改一下就可以

作者:loyldg 出處:http://www.cnblogs.com/loyldg/ 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。如有問題,可以郵件:loyldg@126.com 聯系我,非常感謝。
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider, ITransientDependency
    {
        /// <summary>
        /// The _user manager
        /// </summary>
        private readonly UserManager _userManager;

        public SimpleAuthorizationServerProvider(UserManager userManager)
        {
            _userManager = userManager;
        }

        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            string clientId;
            string clientSecret;
            if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
            {
                context.TryGetFormCredentials(out clientId, out clientSecret);
            }
            var isValidClient = string.CompareOrdinal(clientId, "app") == 0 &&
                                string.CompareOrdinal(clientSecret, "app") == 0;
            if (isValidClient)
            {
                context.OwinContext.Set("as:client_id", clientId);
                context.Validated(clientId);
            }
            else
            {
                context.SetError("invalid client");                
            }
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var tenantId = context.Request.Query["tenantId"];
            var result = await GetLoginResultAsync(context, context.UserName, context.Password, tenantId);
            if (result.Result == AbpLoginResultType.Success)
            {
                //var claimsIdentity = result.Identity;                
                var claimsIdentity = new ClaimsIdentity(result.Identity);
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
                var ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());                
                context.Validated(ticket);
            }
        }

        public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
        {
            var originalClient = context.OwinContext.Get<string>("as:client_id"); 
            var currentClient = context.ClientId;

            // enforce client binding of refresh token
            if (originalClient != currentClient)
            {
                context.Rejected();
                return;
            }

            // chance to change authentication ticket for refresh token requests
            var newId = new ClaimsIdentity(context.Ticket.Identity);
            newId.AddClaim(new Claim("newClaim", "refreshToken"));

            var newTicket = new AuthenticationTicket(newId, context.Ticket.Properties);
            context.Validated(newTicket);            
        }

        private async Task<AbpUserManager<Tenant, Role, User>.AbpLoginResult> GetLoginResultAsync(OAuthGrantResourceOwnerCredentialsContext context, string usernameOrEmailAddress, string password, string tenancyName)
        {
            var loginResult = await _userManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);

            switch (loginResult.Result)
            {
                case AbpLoginResultType.Success:
                    return loginResult;
                default:
                    CreateExceptionForFailedLoginAttempt(context, loginResult.Result, usernameOrEmailAddress, tenancyName);
                    //throw CreateExceptionForFailedLoginAttempt(context,loginResult.Result, usernameOrEmailAddress, tenancyName);
                    return loginResult;
            }
        }

        private void CreateExceptionForFailedLoginAttempt(OAuthGrantResourceOwnerCredentialsContext context, AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName)
        {
            switch (result)
            {
                case AbpLoginResultType.Success:
                    throw new ApplicationException("Don't call this method with a success result!");
                case AbpLoginResultType.InvalidUserNameOrEmailAddress:
                case AbpLoginResultType.InvalidPassword:
                    context.SetError(L("LoginFailed"), L("InvalidUserNameOrPassword"));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), ("InvalidUserNameOrPassword"));
                case AbpLoginResultType.InvalidTenancyName:
                    context.SetError(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName", tenancyName));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), string.Format("ThereIsNoTenantDefinedWithName{0}", tenancyName));
                case AbpLoginResultType.TenantIsNotActive:
                    context.SetError(L("LoginFailed"), L("TenantIsNotActive", tenancyName));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), string.Format("TenantIsNotActive {0}", tenancyName));
                case AbpLoginResultType.UserIsNotActive:
                    context.SetError(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), string.Format("UserIsNotActiveAndCanNotLogin {0}", usernameOrEmailAddress));
                case AbpLoginResultType.UserEmailIsNotConfirmed:
                    context.SetError(L("LoginFailed"), L("UserEmailIsNotConfirmedAndCanNotLogin"));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), ("UserEmailIsNotConfirmedAndCanNotLogin"));
                //default: //Can not fall to default actually. But other result types can be added in the future and we may forget to handle it
                //    //Logger.Warn("Unhandled login fail reason: " + result);
                //    return new UserFriendlyException(("LoginFailed"));
            }
        }
        
        private static string L(string name, params object[] args)
        {
            //return new LocalizedString(name);
            return IocManager.Instance.Resolve<ILocalizationService>().L(name, args);
        }

    }
View Code

  b)添加一個SimpleRefreshTokenProvider類,用於刷新Token

public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider, ITransientDependency
    {
        private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var guid = Guid.NewGuid().ToString("N");

            // maybe only create a handle the first time, then re-use for same client
            // copy properties and set the desired lifetime of refresh token
            var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
            {
                IssuedUtc = context.Ticket.Properties.IssuedUtc,
                ExpiresUtc = DateTime.UtcNow.AddYears(1)
            };
            var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

            //_refreshTokens.TryAdd(guid, context.Ticket);
            _refreshTokens.TryAdd(guid, refreshTokenTicket);

            // consider storing only the hash of the handle
            context.SetToken(guid);
        }

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            AuthenticationTicket ticket;
            if (_refreshTokens.TryRemove(context.Token, out ticket))
            {
                context.SetTicket(ticket);
            }
        }

        public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

        public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }
    }
View Code

  c)添加OAuth的配置信息

/// <summary>
    /// Class OAuthOptions.
    /// </summary>
    public class OAuthOptions
    {
        /// <summary>
        /// Gets or sets the server options.
        /// </summary>
        /// <value>The server options.</value>
        private static OAuthAuthorizationServerOptions _serverOptions;

        /// <summary>
        /// Creates the server options.
        /// </summary>
        /// <returns>OAuthAuthorizationServerOptions.</returns>
        public static OAuthAuthorizationServerOptions CreateServerOptions()
        {
            if (_serverOptions == null)
            {
                var provider = IocManager.Instance.Resolve<SimpleAuthorizationServerProvider>();
                var refreshTokenProvider = IocManager.Instance.Resolve<SimpleRefreshTokenProvider>();                
                _serverOptions = new OAuthAuthorizationServerOptions
                {
                    TokenEndpointPath = new PathString("/oauth/token"),
                    Provider = provider,                    
                    RefreshTokenProvider = refreshTokenProvider,
                    AccessTokenExpireTimeSpan = TimeSpan.FromDays(3),
                    AllowInsecureHttp = true                    
                };
            }
            return _serverOptions;
        }
    }
View Code

  d)在.web項目里添加啟用OAuth的代碼,在Startup類的Configure方法里添加如下代碼

    app.UseOAuthAuthorizationServer(OAuthOptions.CreateServerOptions());

3.編寫測試服務,用於測試

 /// <summary>
    /// 測試接口
    /// </summary>
    public interface ITestAppService : IApplicationService
    {
        /// <summary>
        /// 獲取測試信息,可以匿名訪問
        /// </summary>
        /// <returns>返回測試信息</returns>        
        string GetTestInfo1();
               
        /// <summary>
        /// 訪問此API需要用戶名密碼正確才行
        /// </summary>
        /// <returns></returns>
        List<TestDto> GetTestInfo2();
    }
View Code
public class TestAppService :ApplicationService, ITestAppService
    {
        public string GetTestInfo1()
        {
            return DateTime.Now.ToShortTimeString();
        }

        [AbpAuthorize]
        public List<TestDto> GetTestInfo2()
        {
            var list = new List<TestDto>();
            for (int i = 0; i < 5; i++)
            {
                var dto = new TestDto
                {
                    Id = i + 1,
                    Name = "name" + i
                };

                list.Add(dto);
            }

            return list;
        }
    }
View Code

4.測試

  a) 登錄,需要傳遞的參數如下:

grant_type:該值固定為password
client_id:客戶id
client_secret:客戶密鑰
username:用戶名
password:密碼

  如果已經將client_id和client_secret放到Header里,則不需要傳遞client_id和client_secret,后台先從Header里解析,如果沒有找到,則從請求的參數里查找,但是為了更符合標准,推薦將client_id和client_secret放到Header里,服務端獲取client_id和client_secret對應代碼如下:

    if (!context.TryGetBasicCredentials(out clientId, out clientSecret))

            {

                context.TryGetFormCredentials(out clientId, out clientSecret);

            }

  登錄傳遞的參數信息和登錄成功后返回的信息如下:

   b) 刷新Token,需要傳遞的參數   

grant_type:refresh_token
refresh_token:通過登錄獲取到的refresh_token
client_id:客戶id
client_secret:客戶密鑰

  和登錄一樣,client_id和client_secret推薦放到Header里

  刷新傳遞的參數信息和登錄成功后返回的信息如下:

  c) 通過Token訪問受保護的API時,需要在Header里添加對應的Token,格式化如下:

  Authorization: Bearer access_token 將access_token替換為對應的值即可

  access_token正確時訪問api,返回的信息如下:

    access_token不正確或者過期后調用受保護的API返回的信息如下:

5.問題總結

  1. 登錄成功后需要將登錄后的Identity放到ticket里面,否則使用獲取到的access_token訪問受保護的API時,會提示用戶未登錄
  2. 不要在.Api項目的Module里添加如下代碼(網上有些使用OAuth的例子里添加了如下代碼),添加了該代碼后就只能使用Token的方式進行登錄認證了,Cookie的認證方式會失效,最終的效果就是網站后台輸入了正確的用戶名和密碼也沒法登錄。
     Configuration.Modules.AbpWebApi().HttpConfiguration.SuppressDefaultHostAuthentication();
  3. 如果要支持多租戶登錄,需要將對應參數傳遞過去,可以直接放到QueryString里面
  4. 除了以上3點,其他和不在ABP里使用OAuth2是一樣的

 完整源代碼下載地址:http://files.cnblogs.com/files/loyldg/UsingOAuth2InABP.src.rar


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM