ASP.NET IDENTITY Token based authentication
▌背景
延續先前的實作, 我們將在OWIN Identity Authentication 加入Role(腳色)的設定, 達成在需要認證的類別或方法中,可進一步用Role來設定權限。
▌相關文章
▌環境
l Visual Studio 2015
l WEB API 2.2
l Entity Framework 6
▌實作
▋新增Role DTO類別
在OWIN Identity Code first的資料庫中,已實作並建立好Role相關資料表:
l AspNetRoles
l AspNetUserRoles
有興趣可稍微看一下它的Entity Framework POCO :
稍後會提到Microsoft.AspNet.Identity.RoleManager 這個類別,它已提供了大部分的Role操作(CRUD)方法。
所以我們只要實作提供介接的介面(Web Api)即可。 至於Web Api的Role DTO設計如下:
/// <summary>
/// Role DTO
/// </summary>
public class RoleModel
{
/// <summary>
/// Role Name
/// </summary>
public String Name { get; set; }
}
|
▋Web Api
Web Api的部分沒什麼好說的,就是提供CRUD的API,因為暫時沒有更新(Update)的需求,所以實作如下;
PS. 我們稍後會將實際操作資料庫的方法包裝成一個Role Repository。
l
RoleController
public class RoleController : BaseController
{
public RoleController()
{
}
[HttpGet]
public async Task<IQueryable<RoleModel>> GetAllRoles()
{
var identityRoles = await RoleRepositorySingleton.GetInstance.GetRoles(x
=> true);
return identityRoles.Select(x => new RoleModel
{
Name = x.Name
});
}
[HttpPost]
public async Task<HttpResponseMessage> Create(RoleModel roleModel)
{
#region Model Validation
var isModelValidationOk = base.ValidateDtoModel();
if (!isModelValidationOk)
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
#endregion
#region Create the role into database
var result = await RoleRepositorySingleton.GetInstance.Create(new IdentityRole(roleModel.Name));
if (!result.Succeeded)
{
if (result.Errors != null)
{
LogUtility.LogErrorMsg(result.Errors);
}
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
else
{
LogUtility.Logger.Info(String.Format("Created a
role : {0}", roleModel.Name));
return new HttpResponseMessage(HttpStatusCode.Created);
}
#endregion
}
[HttpPost]
public async Task<HttpResponseMessage> Remove(IEnumerable<RoleModel> roleModels)
{
foreach (var roleModel in roleModels)
{
var result = await RoleRepositorySingleton.GetInstance.Delete(roleModel.Name);
if (!result.Succeeded)
{
if (result.Errors != null)
{
LogUtility.LogErrorMsg(result.Errors);
}
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
else
{
LogUtility.Logger.Info(String.Format("The role is
removed : {0}", roleModel.Name));
}
}
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
|
l BaseController
public class BaseController : ApiController
{
protected bool ValidateDtoModel()
{
bool isSuccess = false;
try
{
if (!ModelState.IsValid) //userModel的驗證失敗
{
if (ModelState.Values != null &&
ModelState.Values.Count > 0)
{
foreach (var errorMsg in
ModelState.Values)
{
Logger.Error(
String.Format("Model
Validation Error : {0}", errorMsg.Errors.FirstOrDefault().ErrorMessage));
}
}
isSuccess = false;
}
else
{
isSuccess = true;
}
return isSuccess;
}
catch (Exception)
{
throw;
}
}
}
|
▋Role Repository
接下來開始時做Role Repository,將前面提到的Microsoft.AspNet.Identity.RoleManager 封裝在此類別,另外考慮到效能,在此將其建立為Singleton類別。
l Role Repository
public class RoleRepositorySingleton : BaseRepository
{
public static RoleRepositorySingleton GetInstance
{
get
{ return InnerClass.instance; }
}
private class InnerClass
{
static InnerClass() { }
internal static readonly RoleRepositorySingleton instance =
new RoleRepositorySingleton();
}
private RoleRepositorySingleton():base() {}
/// <summary>
/// Create a role
/// </summary>
/// <param name="role">IdentityRole
object</param>
/// <returns>IdentityResult</returns>
public async Task<IdentityResult> Create(IdentityRole role)
{
return await base._roleManager.CreateAsync(role);
}
/// <summary>
/// Delete a role and the relations
between users and it
/// </summary>
/// <param name="roleName">New Role Name</param>
/// <returns>IdentityResult</returns>
public async Task<IdentityResult> Delete(String roleName)
{
var role = await this.FindRole(roleName);
return await base._roleManager.DeleteAsync(role);
}
/// <summary>
/// Get all roles of an user
/// </summary>
/// <param name="filter">Filter</param>
/// <returns>IdentityRole
collections</returns>
public async Task<IQueryable<IdentityRole>> GetRoles(Func<IdentityRole,bool> filter)
{
var roles = base._roleManager.Roles;
if (filter!=null)
{
roles =
roles.Where(filter).AsQueryable();
}
return roles;
}
/// <summary>
/// Find the
/// </summary>
/// <param name="roleName"></param>
/// <returns></returns>
public async Task<IdentityRole> FindRole(String roleName)
{
var role = await base._roleManager.FindByNameAsync(roleName);
return role;
}
/// <summary>
/// Find Roles of an user with user's
name
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public async Task<IQueryable<IdentityRole>>
FindRoles(String userName)
{
try
{
var user = base._userManager.FindByName(userName);
var roles = base._roleManager.Roles.Where(x =>
x.Users.Where(y => y.UserId.Equals(user.Id)).Count() > 0);
return roles;
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Check if the role exist
/// </summary>
/// <param name="name">Role name</param>
/// <returns>true(Exist)/false(Not
exist)</returns>
public async Task<bool> IsRoleExist(String name)
{
var isExist = await base._roleManager.RoleExistsAsync(name);
return isExist;
}
}
|
l Base Repository
public class BaseRepository : IDisposable
{
protected AuthContext _ctx = null;
//private AuthUserManager
_userManager = null;
protected UserManager<EnhancedIdentityUser> _userManager
= null;
protected RoleManager<IdentityRole> _roleManager
= null;
public BaseRepository()
{
this._ctx = new AuthContext();
//Intialize the Identity manager
instances
this._userManager = UserManagerFactory.Create(this._ctx);
this._roleManager = RoleManagerFactory.Create(this._ctx);
}
public virtual void Dispose()
{
this._ctx.Dispose();
this._userManager.Dispose();
this._roleManager.Dispose();
}
}
|
到這邊我們已經完成了所有的介面和操作方法,接下來會開始設定Identity Claims
讓Authentication Server可以提供授權給特定Role的Identity (Token)。
▋設定Identity Claims
開啟先前實作好的 BearerAuthorizationServerProvider 這個類別 (繼承自 OAuthAuthorizationServerProvider ),
除了原本加入的Claim Type : 使用者名稱
identity.AddClaim(new Claim(ClaimTypes.Name,
context.UserName));
|
現在要加入Claim Type : Role
例如底下這行代碼將加入Admin這個腳色的授權Claim給Identity (Token)。
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
|
當然使用者在要求Token時,並不會夾帶它屬於那些Role,這時候上面剛才實做的 Role Repository就可以派上用場,直接用User
Name去資料庫找到所有對應的Role。
完整程式碼請參考如下:
l BearerAuthorizationServerProvider
public class BearerAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(
OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
IdentityUser user = await AuthRepositorySingleton.GetInstance.FindUser(context.UserName,
context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user
name or password is incorrect.");
return;
}
else if (!user.EmailConfirmed)
{
context.SetError("invalid_grant", "Denied
because the email hadn't been confirmed.");
return;
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
this.setClaimsForIdentity(identity,
context);
context.Validated(identity);
}
private void setClaimsForIdentity(ClaimsIdentity identity, OAuthGrantResourceOwnerCredentialsContext context)
{
try
{
#region Add Claim : User's Name
identity.AddClaim(new Claim(ClaimTypes.Name,
context.UserName));
#endregion
#region Add Claim : User's Roles
var identityRoles = RoleRepositorySingleton.GetInstance.FindRoles(context.UserName).Result;
foreach (IdentityRole role in identityRoles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
}
#endregion
}
catch (Exception)
{
throw;
}
}
}
|
▋Securing WebApi with both Registered User and Role
最後的部分,當然是把我們的服務類別或方法設定需要Role的授權!
我們一開始實作的Web Api : RoleController 裡面的新增、刪除、查詢方法,一般來說不會讓一般使用者使用,可設定只具有系統管理者腳色(Admin)的使用者才能使用,這時候我們就可以很方便的在Web Api的Http method加上 [Authorize(Roles = "Admin")] ,達到保護的效果。
PS. 如果需要設定多個腳色,則以逗號區隔,如 [Authorize(Roles = "Admin, SysOwner")]
範例:
[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<HttpResponseMessage> Create(RoleModel roleModel)
{ …. }
|
▋其他:更新註冊使用者的DTO類別
Hold on a
minute! 雖然以上已經完成整個Role的管理及授權、驗證, 不過通常會在第一次註冊使用者的時候,就指定其腳色吧!! 這時候請調整註冊使用者的DTO類別~~!
l
UserModel : 只列出新增的代碼
public class UserModel
{
//Skip the original properties ...
[Display(Name = "Roles")]
public List<RoleModel> Roles { get; set; }
}
|
並且在Web Api的註冊方法裡面,加上新增該使用者腳色的代碼:
[AllowAnonymous]
[HttpPost]
public async Task<HttpResponseMessage> Register(UserModel userModel)
{
try
{
#region Model Validation
var isModelValidationOk = base.ValidateDtoModel();
if (!isModelValidationOk)
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
#endregion
#region Register Identity User
var identityUser = await this.registerUser(userModel);
if (identityUser == null) //Fail
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
#endregion
#region Set the user's roles
if (userModel.Roles != null && userModel.Roles.Count > 0)
{
await this.setRoleToUser(userModel.UserName, userModel.Password,
userModel.Roles);
}
#endregion
#region Send a confirmation email
await this.sendConfirmEmail(identityUser);
#endregion
LogUtility.Logger.Info(String.Format("Registered
an user : {0}", userModel.UserName));
return new HttpResponseMessage(HttpStatusCode.Created);
}
catch (Exception ex)
{
LogUtility.LogErrorMsg(ex);
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
private async Task setRoleToUser(string userName, string password, List<RoleModel> roles)
{
IdentityResult result =
await AuthRepositorySingleton.GetInstance.SetRoleToUser(
userName,
password, roles);
if (!result.Succeeded)
{
if (result.Errors != null)
LogUtility.LogErrorMsg(result.Errors);
}
}
|
PS. 至於刪除使用者的時候,並不需要加上刪除使用者及對應腳色的關聯 (資料表名稱:AspNetUserRoles), OWIN Identity的封裝程式碼會幫我們完成,這部分可自行試看看!
▌測試
我們在Web Api的
GetAllRoles (查詢所有腳色)方法,加上指定的Role :
▋Register users with specific roles
註冊了三個使用者及其所對應的腳色:
Name
|
Role
|
Leia
|
Guest
|
Lily
|
Admin
Mother
|
JB
|
Admin
Father
|
參考JSON如下:
關聯表的結果和預期相同。
▋Test the Web Api
接下來分別以三個使用者 (順序請參考上表,分別為Leia → Lily → JB ),取得各自的Token,再對我們的GetAllRoles方法送出Request,
以下是Http Response結果,果然只有具有Father或Mother腳色權限的Identity,才能通過授權並正確取得資料。
▌Reference
沒有留言:
張貼留言