ABP目前的認證方式有兩種,一種是基於Cookie的登錄認證,一種是基於token的登錄認證。使用Cookie的認證方式一般在PC端用得比較多,使用token的認證方式一般在移動端用得比較多。ABP自帶的Token認證方式通過UseOAuthBearerAuthentication啟用的,既然已經自帶了Token的認證方式,為什么還要使用OAuth2呢?使用此方式是無法實現Token的刷新的,Token過期后必須通過用戶名和密碼重新登錄,這樣客戶端會彈出登錄框讓用戶登錄,用戶體驗不是很好,當然也可以在客戶端存儲用戶名和密碼,發現Token過期后,在后台自動登錄,這樣用戶也是不知道的,只是存在賬號安全問題(其實這些都不是問題,主要原因是使用OAuth2后B格更高)。下面我們來看一下怎么在ABP中使用OAuth2:
1.到ABP的官網上下載一個自動生成的項目模板
2.添加OAuth相關的代碼
a) 添加一個SimpleAuthorizationServerProvider類,用於驗證客戶端和用戶名密碼,網上能夠找到類似的代碼,直接拿來修改一下就可以

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); } }
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(); } }
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; } }
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(); }

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; } }
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.問題總結
- 登錄成功后需要將登錄后的Identity放到ticket里面,否則使用獲取到的access_token訪問受保護的API時,會提示用戶未登錄
- 不要在.Api項目的Module里添加如下代碼(網上有些使用OAuth的例子里添加了如下代碼),添加了該代碼后就只能使用Token的方式進行登錄認證了,Cookie的認證方式會失效,最終的效果就是網站后台輸入了正確的用戶名和密碼也沒法登錄。
Configuration.Modules.AbpWebApi().HttpConfiguration.SuppressDefaultHostAuthentication();
- 如果要支持多租戶登錄,需要將對應參數傳遞過去,可以直接放到QueryString里面
- 除了以上3點,其他和不在ABP里使用OAuth2是一樣的
完整源代碼下載地址:http://files.cnblogs.com/files/loyldg/UsingOAuth2InABP.src.rar