参考: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之后,重定向的地址

浙公网安备 33010602011771号