相關知識點
不再對IdentityServer4做相關介紹,博客園上已經有人出了相關的系列文章,不了解的可以看一下:
蟋蟀大神的:小菜學習編程-IdentityServer4
曉晨Master:IdentityServer4
以及Identity,Claim等相關知識:
Savorboard: ASP.NET Core 之 Identity 入門(一),ASP.NET Core 之 Identity 入門(二)
創建IndentityServer4 服務
創建一個名為QuickstartIdentityServer的ASP.NET Core Web 空項目(asp.net core 2.0),端口5000
NuGet包:
修改Startup.cs 設置使用IdentityServer:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResourceResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddProfileService<ProfileService>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
}
}
添加Config.cs配置IdentityResource,ApiResource以及Client:
public class Config
{
public static IEnumerable<IdentityResource> GetIdentityResourceResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(), //必須要添加,否則報無效的scope錯誤
new IdentityResources.Profile()
};
}
// scopes define the API resources in your system
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client1",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId, //必須要添加,否則報forbidden錯誤
IdentityServerConstants.StandardScopes.Profile},
},
// resource owner password grant client
new Client
{
ClientId = "client2",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId, //必須要添加,否則報forbidden錯誤
IdentityServerConstants.StandardScopes.Profile }
}
};
}
}
因為要使用登錄的時候要使用數據中保存的用戶進行驗證,要實IResourceOwnerPasswordValidator接口:
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public ResourceOwnerPasswordValidator()
{
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
//根據context.UserName和context.Password與數據庫的數據做校驗,判斷是否合法
if (context.UserName=="wjk"&&context.Password=="123")
{
context.Result = new GrantValidationResult(
subject: context.UserName,
authenticationMethod: "custom",
claims: GetUserClaims());
}
else
{
//驗證失敗
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential");
}
}
//可以根據需要設置相應的Claim
private Claim[] GetUserClaims()
{
return new Claim[]
{
new Claim("UserId", 1.ToString()),
new Claim(JwtClaimTypes.Name,"wjk"),
new Claim(JwtClaimTypes.GivenName, "jaycewu"),
new Claim(JwtClaimTypes.FamilyName, "yyy"),
new Claim(JwtClaimTypes.Email, "977865769@qq.com"),
new Claim(JwtClaimTypes.Role,"admin")
};
}
}
IdentityServer提供了接口訪問用戶信息,但是默認返回的數據只有sub,就是上面設置的subject: context.UserName,要返回更多的信息,需要實現IProfileService接口:
public class ProfileService : IProfileService
{
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
try
{
//depending on the scope accessing the user data.
var claims = context.Subject.Claims.ToList();
//set issued claims to return
context.IssuedClaims = claims.ToList();
}
catch (Exception ex)
{
//log your error
}
}
public async Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
}
context.Subject.Claims就是之前實現IResourceOwnerPasswordValidator接口時claims: GetUserClaims()給到的數據。
另外,經過調試發現,顯示執行ResourceOwnerPasswordValidator 里的ValidateAsync,然后執行ProfileService 里的IsActiveAsync,GetProfileDataAsync。
啟動項目,使用postman進行請求就可以獲取到token:
再用token獲取相應的用戶信息:
token認證服務一般是與web程序分開的,上面創建的QuickstartIdentityServer項目就相當於服務端,我們需要寫業務邏輯的web程序就相當於客戶端。當用戶請求web程序的時候,web程序拿着用戶已經登錄取得的token去IdentityServer服務端校驗。
創建web應用
創建一個名為API的ASP.NET Core Web 空項目(asp.net core 2.0),端口5001。
NuGet包:
修改Startup.cs 設置使用IdentityServer進行校驗:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(option=>
{
option.Filters.Add(new TestAuthorizationFilter());
}).AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvc();
}
}
創建IdentityController:
[Route("[controller]")]
public class IdentityController : ControllerBase
{
[HttpGet]
[Authorize]
public IActionResult Get()
{
return new JsonResult("Hello Word");
}
}
分別運行QuickstartIdentityServer,API項目。用生成的token訪問API:
通過上述程序,已經可以做一個前后端分離的登錄功能。
實際上,web應用程序中我們經常需要獲取當前用戶的相關信息進行操作,比如記錄用戶的一些操作日志等。之前說過IdentityServer提供了接口/connect/userinfo來獲取用戶的相關信息。之前我的想法也是web程序中拿着token去請求這個接口來獲取用戶信息,並且第一次獲取后做相應的緩沖。但是感覺有點怪怪的,IdentityServer不可能沒有想到這一點,正常的做法應該是校驗通過會將用戶的信息返回的web程序中。問題又來了,如果IdentityServer真的是這么做的,web程序該怎么獲取到呢,查了官方文檔也沒有找到。然后就拿着"Claim"關鍵字查了一通(之前沒了解過ASP.NET Identity),最后通過HttpContext.User.Claims取到了設置的用戶信息:
修改IdentityController :
[Route("[controller]")]
public class IdentityController : ControllerBase
{
[HttpGet]
[Authorize]
public IActionResult Get()
{
return new JsonResult(from c in HttpContext.User.Claims select new { c.Type, c.Value });
}
}
權限控制
IdentityServer4 也提供了權限管理的功能,大概看了一眼,沒有達到我想要(沒耐心去研究)。
我需要的是針對不同的模塊,功能定義權限碼(字符串),每個權限碼對應相應的功能權限。當用戶進行請求的時候,判斷用戶是否具備相應功能的權限(是否賦予了相應的權限字符串編碼),來達到權限控制。
IdentityServer的校驗是通過Authorize特性來判斷相應的Controller或Action是否需要校驗。這里也通過自定義特性來實現權限的校驗,並且是在原有的Authorize特性上進行擴展。可行的方案繼承AuthorizeAttribute,重寫。可是在.net core中提示沒有OnAuthorization方法可進行重寫。最后參考的ABP的做法,過濾器和特性共同使用。
新建TestAuthorizationFilter.cs
public class TestAuthorizationFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
if (!(context.ActionDescriptor is ControllerActionDescriptor))
{
return;
}
var attributeList = new List<object>();
attributeList.AddRange((context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.GetCustomAttributes(true));
attributeList.AddRange((context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.DeclaringType.GetCustomAttributes(true));
var authorizeAttributes = attributeList.OfType<TestAuthorizeAttribute>().ToList();
var claims = context.HttpContext.User.Claims;
// 從claims取出用戶相關信息,到數據庫中取得用戶具備的權限碼,與當前Controller或Action標識的權限碼做比較
var userPermissions = "User_Edit";
if (!authorizeAttributes.Any(s => s.Permission.Equals(userPermissions)))
{
context.Result = new JsonResult("沒有權限");
}
return;
}
}
新建TestAuthorizeAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class TestAuthorizeAttribute: AuthorizeAttribute
{
public string Permission { get; set; }
public TestAuthorizeAttribute(string permission)
{
Permission = permission;
}
}
將IdentityController [Authorize]改為[TestAuthorize("User_Edit")],再運行API項目。
通過修改權限碼,驗證是否起作用
除了使用過濾器和特性結合使用,貌似還有別的方法,有空再研究。
本文中的源碼