身份验证和 ASP.NET Web API 中的授权
IdentityServer4 Reference Token
IdentityServer4 1.0.0 documentation(官网)
业务逻辑
● 搭建 授权服务器 和 资源服务器
● 给App客户端发放 AppId 和 AppSecret
● 用户向App客户端提供自己的 账号 和 密码
● App客户端将AppId、AppSecret、账号、密码提交到 授权服务器
● 授权服务器通过授权,发放token和refresh_token
● 客户端通过 token 与 资源服务器 进行对接,并对 token 进行管理,防止失效
Authentication:认证,身份验证,确定用户是谁;想要说明白的是 你是谁(你的身份是什么)
Authorization:授权,确定用户能做什么,不能做什么;想要说明白的是 你能做什么(得到了什么权限)
授权筛选器(Authorization filter)在 action 之前运行。若请求未授权,返回错误,action 不运行。
在 action 内部,可以用 ApiController.User 属性获取主体对象,做进一步的控制。
[Authorize] 属性
AuthorizeAttribute 是内置的授权筛选器。用户未通过身份验证时,它返回 HTTP 401 状态码。可以在全局,控制和 action 三个级别应用它。
参考网址:ASP.NET MVC 筛选器
AuthorizeAttribute:基于用户和角色进行授权。
AuthorizationFilterAttribute:不基于用户和角色的同步授权。
IAuthorizationFilter:实现此接口执行异步授权逻辑。例如,授权逻辑中有对 IO 或网络的异步调用。(CPU-bound的授权逻辑更适合从 AuthorizationFilterAttribute 派生,这样不必写异步方法)。
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new AuthorizeAttribute());
}
[Authorize]
public class ValuesController : ApiController
{
public IEnumerable Get()
{
return new string[] { "value1", "value2" };
}
}
public class ValuesController : ApiController
{
[Authorize]
public string Get(int id)
{
return "value";
}
}
[Authorize]
public class ValuesController : ApiController
{
[AllowAnonymous]
public string Get(int id)
{
return "value";
}
}
[Authorize(Roles = "Administrators", Users = "Alice,Bob")]
public class ValuesController : ApiController
{
public string Get(int id)
{
return "value";
}
}
1、NuGet 引用
Microsoft.AspNetCore.Authentication.OAuth
IdentityServer4
2、资源配置(Config.cs)
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace OAuth2IdentityServer.OAuth2
{
public class Config
{
public static IEnumerable GetIdentityResourceResources()
{
return new List
{
new IdentityResources.OpenId(), //必须要添加,否则报无效的scope错误
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List {"role"}
}
};
}
public static IEnumerable GetScopes()
{
return new List
{
new Scope
{
Name = "api1",
Description = "My API",
},
//如果想带有RefreshToken,那么必须设置:StandardScopes.OfflineAccess
//StandardScopes.OfflineAccess,
};
}
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource("clientservice", "CAS Client Service"),
new ApiResource("agentservice", "CAS Agent Service"),
new ApiResource("productservice", "CAS Product Service")
{
ApiSecrets = { new Secret("api1pwd".Sha256()) }
},
new ApiResource
{
Name = "customAPI",
DisplayName = "Custom API",
Description = "Custom API Access",
UserClaims = new List {"role"},
ApiSecrets = new List {new Secret("scopeSecret".Sha256())},
Scopes = new List
{
new Scope("customAPI.read"),
new Scope("customAPI.write")
}
}
};
}
public static IEnumerable GetResources()
{
return new List
{
new ApiResource { Name = "ImageResource", Scopes ={ new Scope ("ImageResource") }},//Scopes必须配置,否则获取token时返回 invalid_scope
new ApiResource { Name = "FileResourse" },
new ApiResource { Name ="Api", Scopes = { new Scope ("Api") }}
};
}
///
/// Define which uses will use this IdentityServer
///
///
public static IEnumerable GetUsers()
{
return new[]
{
new TestUser
{
SubjectId = "10001",
Username = "[email protected]",
Password = "edisonpassword"
},
new TestUser
{
SubjectId = "10002",
Username = "[email protected]",
Password = "andypassword"
},
new TestUser
{
SubjectId = "10003",
Username = "[email protected]",
Password = "leopassword"
},
new TestUser
{
SubjectId = "5BE86359-073C-434B-AD2D-A3932222DABE",
Username = "scott",
Password = "password",
Claims = new List
{
new Claim(JwtClaimTypes.Email, "[email protected]"),
new Claim(JwtClaimTypes.Role, "admin")
}
}
};
}
}
}
3、资源配置(Clients.cs)
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace OAuth2IdentityServer.OAuth2
{
public class Clients
{
///
/// 授权模式:客户端模式 GrantTypes.ClientCredentials
///
///
public static IEnumerable GetClients()
{
return new List
{
//client credentials client(OK)
new Client
{
ClientId = "ClientIdOK",
AllowedGrantTypes = GrantTypes.ClientCredentials,//授权模式:客户端模式
AllowedScopes = { "ImageResource","Api" }, //允许访问的资源 GetResources()中配置的
ClientSecrets = { new Secret { Value= "ClientSecret".Sha256(), Expiration = DateTime.Now.AddMinutes(5)} }
},
//client credentials client(OK)
new Client
{
ClientId = "client1",
ClientName = "Example Client Credentials Client Application",
AllowedGrantTypes = GrantTypes.ClientCredentials,//授权模式:客户端模式
AllowedScopes = //对应的是 “OAuth2.Config.GetApiResources()” 或 “OAuth2.Config.GetResources()”中的资源
{
"ImageResource", "Api",
//IdentityServerConstants.StandardScopes.OpenId, //加上此参数报错
//IdentityServerConstants.StandardScopes.Profile,//加上此参数报错
//IdentityServerConstants.StandardScopes.OfflineAccess,//加上此参数报错
},
ClientSecrets = { new Secret("181a7853053b4be9b0ac9d9c709f3ecd".Sha256()) },
AllowedCorsOrigins = new List { "http://localhost:44389" },
RedirectUris = { "http://localhost:6321/Home/AuthCode" },//登录成功重定向地址
PostLogoutRedirectUris = { "http://localhost:44389/" },//退出重定向地址
AccessTokenLifetime = 3600, //AccessToken过期时间, in seconds (defaults to 3600 seconds / 1 hour)
AuthorizationCodeLifetime = 300, //设置AuthorizationCode的有效时间,in seconds (defaults to 300 seconds / 5 minutes)
AbsoluteRefreshTokenLifetime = 2592000, //RefreshToken的最大过期时间,in seconds. Defaults to 2592000 seconds / 30 day
},
// resource owner password grant client
new Client
{
ClientId = "client2",
ClientName = "Example Client Credentials Client Application",
AccessTokenType = AccessTokenType.Jwt,
//AccessTokenType = AccessTokenType.Reference,
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = { new Secret("ClientSecret".Sha256()) },
AllowedCorsOrigins = new List { "http://localhost:44389" },
RefreshTokenExpiration = TokenExpiration.Sliding,
SlidingRefreshTokenLifetime = 5*60,
UpdateAccessTokenClaimsOnRefresh = true,
AllowOfflineAccess = true, //刷新令牌来进行长时间的API访问
AllowedScopes = new List
{
"ImageResource",
"Api",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
},
RedirectUris = { "http://localhost:6321/Home/AuthCode" },
PostLogoutRedirectUris = { "http://localhost:6321/" },
AccessTokenLifetime = 3600, //AccessToken过期时间, in seconds (defaults to 3600 seconds / 1 hour)
AuthorizationCodeLifetime = 300, //设置AuthorizationCode的有效时间,in seconds (defaults to 300 seconds / 5 minutes)
AbsoluteRefreshTokenLifetime = 2592000, //RefreshToken的最大过期时间,in seconds. Defaults to 2592000 seconds / 30 day
},
//Implicit
new Client
{
ClientId = "openIdConnectClient",
ClientName = "Example Implicit Client Application",
AllowedGrantTypes = GrantTypes.Implicit,
AllowedScopes = new List
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"role",
"customAPI.write"
},
RedirectUris = new List {"https://localhost:44330/signin-oidc"},
PostLogoutRedirectUris = new List {"https://localhost:44330"},
AccessTokenLifetime = 3600, //AccessToken过期时间, in seconds (defaults to 3600 seconds / 1 hour)
AuthorizationCodeLifetime = 300, //设置AuthorizationCode的有效时间,in seconds (defaults to 300 seconds / 5 minutes)
AbsoluteRefreshTokenLifetime = 2592000, //RefreshToken的最大过期时间,in seconds. Defaults to 2592000 seconds / 30 day
}
};
}
public static IEnumerable GetClientsTest()
{
return new List
{
new Client
{
ClientId = "ClientId",
AllowedGrantTypes = GrantTypes.ClientCredentials,//授权模式:客户端模式
AllowedScopes ={ "ImageResource","Api" }, //允许访问的资源 GetResources()中配置的
ClientSecrets ={ new Secret { Value= "ClientSecret".Sha256(), Expiration=DateTime.Now.AddMinutes(5)} }
}
};
}
}
}
4、ResourceOwnerPasswordValidator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Validation;
namespace OAuth2IdentityServer.OAuth2
{
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly IUserRepository _userRepo;//数据库仓储类
private readonly IReferenceTokenStore _reference;
private readonly IPersistedGrantStore _persisted;
public ResourceOwnerPasswordValidator(IUserRepository userRepo, IReferenceTokenStore reference, IPersistedGrantStore persisted)
{
_userRepo = userRepo;
_reference = reference;
_persisted = persisted;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
if (context.Request.GrantType == GrantType.AuthorizationCode)
{
string a = "";
}
else if (context.Request.GrantType == GrantType.ResourceOwnerPassword)
{
#region 获取所有请求参数:grant_type、client_Id、client_secret、username、password、eid、device_id
var validatedRequestDictionary = context.Request.Raw.AllKeys.ToDictionary(s => s, s => context.Request.Raw[s]);
string eid = validatedRequestDictionary["eid"];
string device_id = validatedRequestDictionary["device_id"];
#endregion
}
//根据context.UserName和context.Password与数据库的数据做校验,判断是否合法
if (context.UserName == "wjk" && context.Password == "123")
{
_reference.RemoveReferenceTokensAsync(context.UserName, clientId);
_persisted.RemoveAllAsync(context.UserName, clientId);
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, "[email protected]"),
new Claim(JwtClaimTypes.Role,"admin")
};
}
}
}
5、ProfileService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Services;
namespace OAuth2IdentityServer.OAuth2
{
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;
}
}
}
6、Startup.cs(“认证服务器”配置 IdentityServer,颁发 token)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OAuth2IdentityServer.OAuth2;
namespace OAuth2IdentityServer
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
#region 对应 grant_type = Client_Credentials 模式(测试通过 - OK)
//services.AddIdentityServer()
// .AddDeveloperSigningCredential()
// .AddInMemoryApiResources(Config.GetResources())
// .AddInMemoryClients(Clients.GetClients());
#endregion
#region 对应 grant_type = Resource_Owner_Password 模式(测试通过 - OK)
//services.AddIdentityServer()
// .AddDeveloperSigningCredential()
// .AddInMemoryApiResources(Config.GetResources())
// .AddInMemoryClients(Clients.GetClients())
// .AddResourceOwnerValidator()
// .AddProfileService();
#endregion
#region 同时兼容 Client_Credentials 和 Resource_Owner_Password 模式(测试通过 - OK)
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetResources())
.AddInMemoryClients(Clients.GetClients())
.AddResourceOwnerValidator()
.AddProfileService();
#endregion
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
7、Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace OAuth2IdentityServer
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup();
}
}
8、启动项目之后,浏览器输入 https://localhost:44305/.well-known/openid-configuration 查看文档(端口号以项目运行的真实地址为准)
9、获取token(Client Credentials 模式)
10、获取token(Resource Owner Password 模式)
扩展参数、扩展参数、扩展参数
注意、注意、注意、除了必要参数还可以添加扩展参数,比如:eid,device_id
刷新令牌:获取 token 并且包含刷新令牌 refresh_token(Resource Owner Password 模式)
refresh_token 的作用就是,在 access_token 过期的时候,不需要再通过一些凭证申请 access_token,而是直接通过 refresh_token 就可以重新申请 access_token。如果标记了offline_access,则生成 refreshToken,否则不生成。
*
利用refresh_token(刷新令牌),更新access_token(token值)
refresh_token 的作用就是,在 access_token 过期的时候,不需要再通过一些凭证申请 access_token,而是直接通过 refresh_token 就可以重新申请 access_token。
如果 Clients 的 AllowedScopes 包含 IdentityServerConstants.StandardScopes.OpenId 或 IdentityServerConstants.StandardScopes.Profile 响应信息Response中会包含 id_token 。
*
利用 token 获取当前登录信息(connect/userinfo)
*、资源服务器,Startup.cs 配置如下:
using IdentityModel;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace ImageResourceApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(config =>
{
config.DefaultScheme = "Bearer";
}).AddIdentityServerAuthentication(option =>
{
option.Authority = "https://localhost:44308"; //认证服务的url
option.ApiName = "ImageResource";
option.ApiSecret = "ClientSecret".ToSha256();// 访问的secret
option.SaveToken = true;
//options.EnableCaching = true;
//options.CacheDuration = TimeSpan.FromMinutes(10);
option.RequireHttpsMetadata = false;//如果授权认证服务器访问地址是https开头,设置成true,如果授权认证服务器访问地址是http开头,设置成false【注意注意】
});
方式2(待验证)
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = Configuration["IdentityAuthentication:Authority"];
options.RequireHttpsMetadata = true;
options.Audience = Configuration["IdentityAuthentication:ApiName"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,//是否验证失效时间
ClockSkew = TimeSpan.FromSeconds(30),
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
//Token expired
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
context.Response.ContentType = "application/json";
context.Response.WriteAsync(JsonConvert.SerializeObject(new { code = "401", msg = "Unauthorized" }));
return Task.CompletedTask;
}
};
});
方式2(待验证)
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
//使用认证
app.UseAuthentication();
app.UseMvc();
}
}
}
*、资源服务器:在需要进行授权验证的资源接口(controller控制器或方法)上设置AuthorizeAttribute:
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ImageResourceApi.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}
*、客户端 发送web请求 资源服务器,PostMan方式如下:
【注意:】“认证服务器”中\OAuth2\Clients.cs =>AllowedScopes 要和 “资源服务器” Startup.cs => option.ApiName 的值相匹配
*、
*、
*、