ASP.NET IDENTITY Token based authentication
▌背景
備註:
|
實作上主要參考 TAISEER (MVP of Microsoft)此篇文章:
▌環境
l Visual Studio 2015
l WEB API 2.2
l 套件版本:
Microsoft.Owin.Host.SystemWeb 3.0.1
Microsoft.Owin.Host.SystemWeb 3.0.1
Microsoft ASP.NET Identity Owin 2.2.1
Microsoft ASP.NET Identity
EntityFramework 2.2.1
Microsoft ASP.NET Web API 2.2 OWIN
5.2.3
Microsoft.Owin.Security.OAuth 3.0.1
▌實作 : 授權(認證)Authentication
Server
▋Create a WebApi with OWIN startup
在WebApi專案中加入以下套件,
▋加入ASP.NET Identity
順便一併加入Enable Cross-Origin Resource Sharing
(CORS) ~
▋建立Identity的DbContext
public class AuthContext : IdentityDbContext<IdentityUser>
{
public AuthContext() : base("AuthContext")
{
}
}
|
並在Config加入對應的Connection
String
<connectionStrings>
<add name="AuthContext" connectionString="Data
Source=XXXX;Initial Catalog=JB;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
|
因此不用擔心後端資料庫的部分。
▋建立資料庫的Authentication Repository
l UserModel
/// <summary>
/// User information
for authorization
/// </summary>
public class UserModel
{
[Required]
[Display(Name="User Name")]
public String UserName { get; set; }
[Required]
[StringLength(100,
MinimumLength=6, ErrorMessage="The {0}'s length must be between {2}~{1}
characters long")]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public String Password { get; set; }
[DataType(DataType.Password)]
[Display(Name="Confirm password")]
[Compare("Password", ErrorMessage="The password
and confirmation password do not match.")]
public String ConfirmPassword { get; set; }
}
|
l Authentication Repository
public class AuthRepository : IDisposable
{
private AuthContext _ctx = null;
private UserManager<IdentityUser> _userManager = null;
public AuthRepository()
{
this._ctx = new AuthContext();
this._userManager = new UserManager<IdentityUser>(
new UserStore<IdentityUser>(this._ctx));
//設定可允許UserName包含特殊字元及空白
this._userManager.UserValidator =
new UserValidator<IdentityUser>(this._userManager)
{
AllowOnlyAlphanumericUserNames = false
};
}
public async Task<IdentityResult> RegisterUser(UserModel userModel)
{
var user = new IdentityUser
{
UserName=userModel.UserName
};
var result = await this._userManager.CreateAsync(user,
userModel.Password);
return result;
}
public async Task<IdentityResult>
Unregisteruser(String
userName)
{
try
{
var user = await this._userManager.FindByNameAsync(userName);
var identityResult = await this._userManager.DeleteAsync(user);
return identityResult;
}
catch (Exception)
{
throw;
}
}
public async Task<IdentityUser> FindUser(String userName, String password)
{
var user = await this._userManager.FindAsync(userName,
password);
return user;
}
public async Task<IQueryable<IdentityUser>>
GetAllUsers()
{
return this._userManager.Users;
}
public void Dispose()
{
this._ctx.Dispose();
this._userManager.Dispose();
}
}
|
▋建立Authentication的Restful Services,包含取得所有使用者, 註冊及註銷使用者
很直覺的就是拿上一步的Repository來使用即可。
AccountController :
public class AccountController : ApiController
{
private AuthRepository _authRepo = null;
public AccountController()
{
this._authRepo = new AuthRepository();
}
[AllowAnonymous]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[HttpGet]
public async Task<IQueryable<UserModel>> GetAllUsers()
{
var identityUsers = await this._authRepo.GetAllUsers();
var users = identityUsers.Select(x
=> new UserModel()
{
UserName = x.UserName,
Password = String.Empty,
ConfirmPassword = String.Empty
});
return users;
}
[AllowAnonymous]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[HttpPost]
public async Task<HttpResponseMessage> Unregister(IEnumerable<UserModel> users)
{
try
{
foreach(var user in users)
{
await this._authRepo.Unregisteruser(user.UserName);
}
return new HttpResponseMessage(HttpStatusCode.OK);
}
catch (Exception ex)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
[AllowAnonymous]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[HttpPost]
public async Task<HttpResponseMessage> Register(UserModel userModel)
{
if (!ModelState.IsValid) //userModel的驗證失敗
{
if (ModelState.Values != null &&
ModelState.Values.Count>0)
{
foreach (var errorMsg in ModelState.Values)
{
Console.WriteLine(
String.Format("Error :
{0}",errorMsg.Errors.FirstOrDefault().ErrorMessage));
}
}
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
IdentityResult result = await this._authRepo.RegisterUser(userModel);
if(!result.Succeeded)
{
if(result.Errors!=null)
{
foreach(var error in result.Errors)
{
Console.WriteLine(String.Format("Error :
{0}",error));
}
}
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
|
▋Enable the Identity protocol
最後一步是建立
Authorization Server Provider及啟用整個認證機制。
n ValidateClientAuthentication
MSDN :
若Web 應用程式接受 Basic 驗證認證,且呈現在要求標頭中,則可呼叫 context.TryGetBasicCredentials(out clientId, out clientSecret) 以取得這些值。若並未呼叫 context.Validated,不會進一步處理要求。 |
簡而言之,如果只做Identity的驗證,則可在此方法直接呼叫context.Validated()接受用戶端要求Token的Request。
如果有在Http Header加上Basic Authentication,則可呼叫
TryGetBasicCredentials(out clientId, out clientSecret)
來取得資訊並做第一層驗證。 (可參考下面註解掉的程式碼)
TryGetBasicCredentials(out clientId, out clientSecret)
來取得資訊並做第一層驗證。 (可參考下面註解掉的程式碼)
n GrantResourceOwnerCredentials
當
Token 端點的要求與 "password" 的
"grant_type" 抵達時呼叫。 這會在使用者直接提供名稱和密碼認證到用戶端應用程式的使用者介面時發生
|
亦即我們在這裡放上要檢核ID/PWD的程式碼。
BearerAuthorizationServerProvider 程式碼
public class BearerAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(
OAuthValidateClientAuthenticationContext context)
{
context.Validated();
#region TryGetBasicCredentials
//可對Http Basic Authentication做第一層認證檢查 (Http Header)
/*
string clientId, clientSecret;
if (context.TryGetBasicCredentials(out
clientId, out clientSecret))
{
if
(clientId.Equals("XXX") &&
clientSecret.Equals("XXX"))
context.Validated();
else
context.Rejected();
}
else
{
context.Rejected();
}
*/
#endregion
}
public override async Task GrantResourceOwnerCredentials(
OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add(
"Access-Control-Allow-Origin", new[] { "*" });
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName,
context.Password);
if (user == null)
{
context.SetError(
"invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub",
context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}
|
最後,在OWIN Startup類別,啟用Identity。
PS. 因已在Startup.cs註冊WebApi的Route設定,因此可取消(或註解)在Global.asax的部分。
PS. 因已在Startup.cs註冊WebApi的Route設定,因此可取消(或註解)在Global.asax的部分。
/// <summary>
/// OWIN Startup
/// </summary>
public class Startup
{
/// <summary>
/// Configuration
/// </summary>
/// <param name="appBuilder"></param>
/// <remarks>注意方法名稱必須為Configuration</remarks>
public void Configuration(IAppBuilder appBuilder)
{
HttpConfiguration config = new HttpConfiguration();
//Configure OAuth
this.configureOAuth(appBuilder);
//Configure WebApi Route
this.configureWebApiRoute(config);
appBuilder.UseWebApi(config);
//Enable CORS
config.EnableCors();
}
private void configureWebApiRoute(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
private void configureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions oAuthServerOpts =
new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true, //是否允許Http傳輸(上線後以Https較安全)
//指定取得Token路徑為 : http://localhost:port/token
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(2), //Token使用期限為取得後N天
Provider = new BearerAuthorizationServerProvider()
};
//Token Generation
app.UseOAuthAuthorizationServer(oAuthServerOpts);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
|
▌測試
2. Get the token
(Request)
PS. Http Body :
grant_type=password&username=Leia Skywalker&password=12345678
(Response)
此時如果沒有意外,我們已經成功取得token;
將access_token後的值複製下來。
(Request)
PS. Http Body :
grant_type=password&username=Leia Skywalker&password=12345678
(Response)
此時如果沒有意外,我們已經成功取得token;
將access_token後的值複製下來。
3. Authorization
還記得在AccountController有一個取得所有使用者的HTTP GET方法,請把它改成需要授權才能使用。
還記得在AccountController有一個取得所有使用者的HTTP GET方法,請把它改成需要授權才能使用。
//[AllowAnonymous]
[Authorize]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[HttpGet] public async Task<IQueryable<UserModel>> GetAllUsers()
{ … }
|
不使用Token做認證的結果 ~ HTTP 401 Unauthorized
在Header加上
Authorization : bearer [上面取得的token] , 成功過了認證。
當此token的有效期結束後,則必須重新再次取得!
(至於註銷Unregister使用者,請再自行測試囉~ )
▌Reference
沒有留言:
張貼留言