参考:IdentityServer4实现Token登录以及权限控制_identityserver4服务端怎么在登录时获取token-CSDN博客
前言:
1. OAuth 2.0是有关如何颁发访问令牌的规范,提供Access Token用于访问服务接口的
2. OpenID Connect是有关如何发行ID令牌的规范,提供Id Token用于用户身份标识(非敏感信息),Id Token是基于JWT格式
3. IdentityServer4服务中心默认提供接口/connect/token获取access token
4. IdentityServer4新版本新增ApiScope配置保护API资源,并使用ApiScope结合策略授权完成了一个简单的权限控制
1.基本步骤
注册服务
builder.Services.AddIdentityServer() //配置证书 .AddDeveloperSigningCredential() //配置API资源 .AddInMemoryApiResources(Config.GetApiResources()) //配置身份资源 .AddInMemoryIdentityResources(Config.GetIdentityResources()) //客户端信息配置 .AddInMemoryClients(Config.GetClients(configuration)) //用户验证 .AddResourceOwnerValidator<ResourcePasswordValidator>()
//扩展claims
.AddProfileService<ProfileService>();
//测试用户 .AddTestUsers(Config.GetUsers());
public class Config { public static IEnumerable GetIdentityResources() { return new List { new IdentityResources.OpenId(), new IdentityResources.Profile() }; } public static IEnumerable GetApis() { return new List { new ApiResource("OaIdService4",new List(){JwtClaimTypes.Subject}) }; } public static IEnumerable GetClients() { var clientList = AppSettings.app<List>("AuthClient") ?? new List(); return clientList.Select(s => new Client() { ClientId = s.ClientId, AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, //AccessToken过期时间(秒),默认为86400秒/1天 AccessTokenLifetime = s.AccessTokenLifetime, //RefreshToken生命周期以秒为单位。默认为1296000秒/30天 SlidingRefreshTokenLifetime = s.RefreshTokenLifetime, //刷新令牌时,将刷新RefreshToken的生命周期。RefreshToken的总生命周期不会超过AbsoluteRefreshTokenLifetime。 RefreshTokenExpiration = TokenExpiration.Sliding, //AllowOfflineAccess 允许使用刷新令牌的方式来获取新的令牌 AllowOfflineAccess = true, ClientSecrets = { new Secret(s.ClientSecret.Sha256()) }, AllowedScopes = { "OaIdService4", StandardScopes.OfflineAccess,//如果要获取refresh_tokens ,必须在scopes中加上OfflineAccess } }); } }
public class ProfileService : IProfileService { private readonly ILogger logger; public ProfileService(ILogger logger) { this.logger = logger; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { try { var claims = context.Subject.Claims.ToList(); context.IssuedClaims = claims.ToList(); } catch (Exception ex) { logger.LogError(ex.ToString()); } } public async Task IsActiveAsync(IsActiveContext context) { context.IsActive = true; } }
public class ResourcePasswordValidator : IResourceOwnerPasswordValidator { private readonly ITBSysUserBLL tbSysUserService = Container.Resolve(); public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var loginQuery = new MethodLoginQuery(); var method = context.Request.Raw["method"]; if (!method.ToIsEmpty()) { loginQuery.Method = EnumHelper.GetEnum(StringToValue.Set.ChangeInt(method)); } var manageName = context.Request.Raw["manage"]; if (!manageName.ToIsEmpty()) { var n = StringToValue.Set.ChangeInt(manageName, 0); if (n > 0) { if (Enum.IsDefined(typeof(EManageName), n)) { loginQuery.AuthManage = new EManageName[] { EnumHelper.GetEnum(n) }; } loginQuery.ManageName = EnumHelper.GetEnum(n); } } loginQuery.UserName = context.UserName; loginQuery.Password = context.Password; var claimList = new List() { new Claim(DevKeyCode.LoginManage, manageName)}; if (loginQuery.AuthManage?.Length > 0) { claimList.Add(new Claim(DevKeyCode.AuthManage, loginQuery.AuthManage.Select(s => s.ToEString()).ToArray().ToJoin())); } var appkey = context.Request.Raw["appkey"]; if (string.IsNullOrEmpty(appkey)) { appkey = Guid.NewGuid().ToString().Replace("-", "").ToLower(); } claimList.Add(new Claim(DevKeyCode.EncryptKey, appkey)); var loginResult = tbSysUserService.SSOLogin(loginQuery); if (loginResult.Success) { var userInfo = loginResult.Data as UserAuthInfo; context.Result = new GrantValidationResult( subject: "userInfo", authenticationMethod: OidcConstants.AuthenticationMethods.Password, claims: GetUserClaims(userInfo, claimList.ToArray()) ); } else { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, Convert.ToInt32(loginResult.Code).ToString()); } } private Claim[] GetUserClaims(UserAuthInfo user, params Claim[] claimParam) { var list = new List { new Claim(DevKeyCode.SSOLoginUser,JHSC.Helper.Serializations.SerializationHelper.SerializeJson(user)) }; if (claimParam?.Length > 0) { list.AddRange(claimParam.ToArray()); } return list.ToArray(); } }
调用中间件
app.UseIdentityServer();
WebApi调用
注入认证服务引用:IdentityServer4.AccessTokenValidation 2.5.0
public static class AuthenticationIds4Setup { public static void AddAuthenticationIds4Setup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options => { options.RequireHttpsMetadata = false; options.Authority = AppSettings.app("IdentityServer", "Authority"); options.ApiName = AppSettings.app("IdentityServer", "ApiName"); }); } }
private async Task LoginIn(LoginQuery query) { if (string.IsNullOrEmpty(query.UserName) || string.IsNullOrEmpty(query.Password) || string.IsNullOrEmpty(query.LoginManage)) { return ResponseApp(null, ResultEnum.LackParam); } var appKey = Guid.NewGuid().ToString().Replace("-", "").ToLower(); //密钥 var httpClient = new HttpClient(); var parameters = new Dictionary<string, string> { { "manage", query.LoginManage }, { "method", Convert.ToInt32(query.Method).ToString() }, { "client_id", currentAuth.ClientId }, { "client_secret", currentAuth.ClientSecret }, { "grant_type", "password" }, { "username", query.UserName }, { "password", System.Web.HttpUtility.UrlDecode(query.Password) }, { "appkey", appKey } }; var response = await httpClient.PostAsync($"{authUrl}/connect/token", new FormUrlEncodedContent(parameters)); var responseValue = await response.Content.ReadAsStringAsync(); if (response.StatusCode == System.Net.HttpStatusCode.OK) { cacheDbService.RemoveModules(query.LoginManage, query.UserName); var authResult = SerializationHelper.DeserializeJson(responseValue); return ResponseApp(new { authResult.access_token, authResult.token_type, authResult.refresh_token, authResult.expires_in, appKey }); } else { var errorResult = SerializationHelper.DeserializeJson(responseValue); ResultEnum errorEnum = ResultEnum.LoginFail; if (errorResult?.error == "invalid_grant") { if (!string.IsNullOrEmpty(errorResult.error_description)) { var errorNum = StringToValue.Set.ChangeNInt(errorResult.error_description); if (errorNum.HasValue) { errorEnum = EnumHelper.GetEnum(errorNum.Value); } } } return ResponseApp(errorEnum); } }
[HttpPost] public async Task RefreshToken() { var query = Decrypt(ESetType.AuthKey); var httpClient = new HttpClient(); var parameters = new Dictionary<string, string>(); parameters.Add("client_id", currentAuth.ClientId); parameters.Add("client_secret", currentAuth.ClientSecret); parameters.Add("grant_type", "refresh_token"); parameters.Add("refresh_token", query.Refresh_token); var response = await httpClient.PostAsync($"{authUrl}/connect/token", new FormUrlEncodedContent(parameters)); var responseValue = await response.Content.ReadAsStringAsync(); dynamic objJson = SerializationHelper.DeserializeJson(responseValue); if (response.StatusCode == System.Net.HttpStatusCode.OK) { var authResult = SerializationHelper.DeserializeJson(responseValue); return ResponseApp(authResult,ResultEnum.Success); } else { return ResponseApp(ResultEnum.DataEx); } }
授权模式
- 客户端模式(Client Credentials):和用户无关,用于应用程序与 API 资源的直接交互场景。
- 密码模式(resource owner password credentials):和用户有关,一般用于第三方登录。
- 简化模式-With OpenID(implicit grant type):仅限 OpenID 认证服务,用于第三方用户登录及获取用户信息,不包含授权。
- 简化模式-With OpenID & OAuth(JS 客户端调用):包含 OpenID 认证服务和 OAuth 授权,但只针对 JS 调用(URL 参数获取),一般用于前端或无线端。
- 混合模式-With OpenID & OAuth(Hybrid Flow):推荐使用,包含 OpenID 认证服务和 OAuth 授权,但针对的是后端服务调用。
1).简单模式 (client_credentials)
客户端模式只对客户端进行授权,不涉及到用户信息。如果你的api需要提供到第三方应用,第三方应用自己做用户授权,不需要用到你的用户资源,就可以用客户端模式,只对客户端进行授权访问api资源。这是一种最简单的模式,
只要client请求,我们就将AccessToken发送给它。这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的
2).用户密码模式(password)
需要客户端提供用户名和密码,密码模式相较于客户端凭证模式。通过User的用户名和密码向Identity Server申请访问令牌。 这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用
3).隐藏式 (implicit)
https://localhost:6005/connect/authorize?client_id=Implicit&redirect_uri=http://localhost:5000/Home&response_type=token&scope=WebApi
有些 Web 应用是前后端分离的纯前端应用,没有后端。这时就必须将令牌储存在前端。 这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。
隐藏式的认证步骤为:
第一步,A 网站提供一个链接,要求用户跳转到 认证中心,授权用户数据给 A 网站使用。
第二步,用户跳转到 认证中心,登录后同意给予 A 网站授权。这时,认证中心就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
4).授权码模式(code)
https://localhost:6005/connect/authorize?client_id=GrantCode&redirect_uri=http://localhost:5000/Home&response_type=code&scope=WebApi
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。 这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
1.在浏览器中访问OAuth2 服务器的认证接口:
http://localhost:8020/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://localhost:8080
- response_type=code : 代表期望的请求响应类型为authorization code
- client_id=test: client_id为你需要使用的客户端id
- redirect_uri=http://localhost:8080 : redirect_uri是成功获取token之后,重定向的地址
2.访问认证接口成功之后,浏览器会跳转到OAuth2配置的登录页或者默认的security登录,正确输入用户名/密码之后。浏览器将会在重定向的地址上返回一个code。如下:
http://localhost:8080?code=W3ixVa
- code=W3ixVa : code就是OAuth2服务器返回的
3.然后使用获取到的code范围OAuth2认证服务器取到access_token,如下:
http://localhost:8020/oauth/token?grant_type=authorization_code&code=W3ixVa&client_id=test&client_secret=secret&redirect_uri=http://localhost:8080
- grant_type=authorization_code : grant_type为认证类型,当前为授权码模式
- code=W3ixVa : code为上面获取到的code
- client_id=test : client_id 与上面获取code的client_id需要一致
- client_secret=secret : 为client_id对应的客户端的密钥
- redirect_uri=http://localhost:8080 : : redirect_uri是成功获取token之后,重定向的地址
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!