zoukankan      html  css  js  c++  java
  • 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是一样的

     完整源代码下载地址:https://files.cnblogs.com/files/loyldg/UsingOAuth2InABP.src.rar

    作者:loyldg
    出处:http://www.cnblogs.com/loyldg/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。如有问题,可以邮件:loyldg@126.com  联系我,非常感谢。

  • 相关阅读:
    BZOJ3992 [SDOI2015]序列统计 【生成函数 + 多项式快速幂】
    BZOJ3993 [SDOI2015]星际战争 【二分 + 网络流】
    BZOJ3325 [Scoi2013]密码 【manacher】
    BZOJ3534 [Sdoi2014]重建 【矩阵树定理】
    BZOJ3507 [Cqoi2014]通配符匹配 【哈希 + 贪心】
    BZOJ2285 [SDOI2011]保密 【01分数规划 + 网络流】
    BZOJ4556 [Tjoi2016&Heoi2016]字符串 【后缀数组 + 主席树 + 二分 + ST表】
    BZOJ4817 [Sdoi2017]树点涂色 【LCT + 线段树】
    BZOJ1195 [HNOI2006]最短母串 【状压dp】
    malloc的使用、用malloc动态分配内存以适应用户的需求的源代码实例
  • 原文地址:https://www.cnblogs.com/loyldg/p/using-oauth2-in-abp.html
Copyright © 2011-2022 走看看