文章目录
- 前言
- 签名证书(Signing Credential)
- 客户端存储(Client Store)
- 资源存储(Resource Store)
- 持久化授权存储(Persisted Grant Store)
- 资源拥有者验证器(Resource Owner Validator)
- 重定向地址验证器(Redirect Uri Validator)
- 扩展授权验证器(Extension Grant Validator)
- OAuth2.0的实践运用场景
前言
本篇文章主要是讲解葫芦藤项目中对IdentityServer的实践使用,为了使您对本篇文章中所讲述的内容有深刻的认识,并且在阅读时避免感到乏味,文中的内容不会涉及太多的基础理论知识,而更多的是采用动手实践的方式进行讲解,所以在阅读此篇文章前假定您已经掌握了OAuth2.0的基础知识,如您事先并未了解OAuth2.0,请参阅一下阮一峰老师的文章《理解OAuth2.0》, ASP.NET Core 认证与授权,可以看看博客 雨夜朦胧,另外IdentityServer的相关文章也可以参考博客 晓晨Master。
葫芦藤前端地址:https://account.suuyuu.cn (验证码获取后,输入123456即可)
葫芦藤后端地址:https://account-web.suuyuu.cn
葫芦藤源码地址:https://github.com/fuluteam/fulusso (帮忙点个小星星哦)
团队博文地址:https://www.cnblogs.com/fulu
签名证书(Signing Credential)
IdentityServer支持X.509证书(包括原始文件和对Windows证书存储库的引用)、RSA密钥和EC密钥,用于令牌签名和验证。每个密钥都可以配置一个(兼容的)签名算法,如RS256、RS384、RS512、PS256、PS384、PS512、ES256、ES384或ES512。
通常情况下,我们使用的是针对开发场景创建的临时证书 AddDeveloperSigningCredential,
生产环境怎么办呢?IdentityServer还提供了AddSigningCredential用来装载证书文件,
为此我们需要准备一个X.509证书,下面是在控制台项目中用于生成证书的代码,完整代码请参考项目:https://github.com/fuluteam/ICH.BouncyCastle
//颁发者DN
var issuer = new X509Name(
new ArrayList{X509Name.C,X509Name.O,X509Name.OU,X509Name.L,X509Name.ST},
new Hashtable{[X509Name.C] = "CN",[X509Name.O] = "Fulu Newwork",[X509Name.OU] = "Fulu RSA CA 2020",[X509Name.L] = "Wuhan",[X509Name.ST] = "Hubei"});
//使用者DN
var subject = new X509Name(new ArrayList{X509Name.C,X509Name.O,X509Name.CN}, new Hashtable {[X509Name.C] = "CN",[X509Name.O] = "ICH",[X509Name.CN] = "*.fulu.com"});
//生成证书文件
CertificateGenerator.GenerateCertificate(newCertificateGenerator.GenerateCertificateOptions { Path = "mypfx.pfx",Issuer = issuer, Subject = subject });
执行代码后,在项目编译输出目录中,会看到一个mypfx.pfx的文件,此时我们的证书就创建成功啦。
接着怎么使用呢,看下面代码:
var certificate2 = new X509Certificate2("mypfx.pfx", "password", X509KeyStorageFlags.Exportable);
identityServerBuilder.AddSigningCredential(certificate2);
大家可能会问,葫芦藤中怎么不是这么写的呢,其实葫芦藤项目中是将证书文件的流数据转成了二进制字符串,这样就可以写在配置文件中了:
using (var fs = new FileStream(options.Path, FileMode.Open))
{
var bytes = new byte[fs.Length];
fs.Read(bytes, 0, bytes.Length);
var pfxHexString = Hex.ToHexString(bytes);
}
然后在这么使用:
identityServerBuilder.AddSigningCredential(new X509Certificate2(Hex.Decode(appSettings.X509RawCertData), appSettings.X509CertPwd));
客户端存储(Client Store)
在葫芦藤项目中,我们创建了一个ClientStore类,继承自接口IClientStore,实现其方法代码如下:
public class ClientStore : IClientStore
{
private readonly IClientCacheStrategy _clientInCacheRepository;
public ClientStore(IClientCacheStrategy clientInCacheRepository)
{
_clientInCacheRepository = clientInCacheRepository;
}
public async Task<Client> FindClientByIdAsync(string clientId)
{
var clientEntity = await _clientInCacheRepository.GetClientByIdAsync(clientId.ToInt32());
if (clientEntity == null)
{
return null;
}
return new Client
{
ClientId = clientId,
AllowedScopes = new[] { "api", "get_user_info" },
ClientSecrets = new[] { new Secret(clientEntity.ClientSecret.Sha256()) },
AllowedGrantTypes = new[]
{
GrantType.AuthorizationCode, //授权码模式
GrantType.ClientCredentials, //客户端模式
GrantType.ResourceOwnerPassword, //密码模式
CustomGrantType.External, //自定义模式——三方(移动端)模式
CustomGrantType.Sms //自定义——短信模式
},
AllowOfflineAccess = false,
RedirectUris = string.IsNullOrWhiteSpace(clientEntity.RedirectUri) ? null : clientEntity.RedirectUri.Split(';'),
RequireConsent = false,
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 7200,
ClientClaimsPrefix = "",
Claims = new[] { new Claim(JwtClaimTypes.Role, "Client") }
};
}
}
通过代码可以看到,通过clientId从缓存中读取Client的相关信息构建并返回,这里我们为所有的Client简单的设置了统一的AllowedGrantTypes,这是一种偷懒的做法,应当按需授予GrantType,例如通常情况下我们只应默认给应用分配AuthorizationCode或者ClientCredentials,ResourceOwnerPassword需要谨慎授予(需要用户对Client高度信任)。
资源存储(Resource Store)
由于历史原因,在葫芦藤中,我们并没有通过IdentityServer对api资源进行访问保护(后续会提供我们的实现方式),我们为所有Client设置了相同的Scope。
持久化授权存储(Persisted Grant Store)
葫芦藤中,我们使用了Redis来持久化数据,
通过EntityFramework Core持久化配置和操作数据,请参考
https://www.cnblogs.com/stulzq/p/8120518.html
https://github.com/IdentityServer/IdentityServer4.EntityFramework
IPersistedGrantStore接口中定义了如下6个方法:
/// <summary>Interface for persisting any type of grant.</summary>
public interface IPersistedGrantStore
{
/// <summary>Stores the grant.</summary>
/// <param name="grant">The grant.</param>
/// <returns></returns>
Task StoreAsync(PersistedGrant grant);
/// <summary>Gets the grant.</summary>
/// <param name="key">The key.</param>
/// <returns></returns>
Task<PersistedGrant> GetAsync(string key);
/// <summary>Gets all grants for a given subject id.</summary>
/// <param name="subjectId">The subject identifier.</param>
/// <returns></returns>
Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId);
/// <summary>Removes the grant by key.</summary>
/// <param name="key">The key.</param>
/// <returns></returns>
Task RemoveAsync(string key);
/// <summary>
/// Removes all grants for a given subject id and client id combination.
/// </summary>
/// <param name="subjectId">The subject identifier.</param>
/// <param name="clientId">The client identifier.</param>
/// <returns></returns>
Task RemoveAllAsync(string subjectId, string clientId);
/// <summary>
/// Removes all grants of a give type for a given subject id and client id combination.
/// </summary>
/// <param name="subjectId">The subject identifier.</param>
/// <param name="clientId">The client identifier.</param>
/// <param name="type">The type.</param>
/// <returns></returns>
Task RemoveAllAsync(string subjectId, string clientId, string type);
}
PersistedGrant的结构如下:
/// <summary>A model for a persisted grant</summary>
public class PersistedGrant
{
/// <summary>Gets or sets the key.</summary>
/// <value>The key.</value>
public string Key { get; set; }
/// <summary>Gets the type.</summary>
/// <value>The type.</value>
public string Type { get; set; }
/// <summary>Gets the subject identifier.</summary>
/// <value>The subject identifier.</value>
public string SubjectId { get; set; }
/// <summary>Gets the client identifier.</summary>
/// <value>The client identifier.</value>
public string ClientId { get; set; }
/// <summary>Gets or sets the creation time.</summary>
/// <value>The creation time.</value>
public DateTime CreationTime { get; set; }
/// <summary>Gets or sets the expiration.</summary>
/// <value>The expiration.</value>
public DateTime? Expiration { get; set; }
/// <summary>Gets or sets the data.</summary>
/// <value>The data.</value>
public string Data { get; set; }
}
可以看出主要是针对PersistedGrant对象的操作,通过观察GetAsync和RemoveAsync方法的入参均为key,我们在StoreAsync中将PersistedGrant中的Key作为缓存key,将PersistedGrant对象以hash的方式存入缓存中,并设置过期时间(注意将UTC时间转换为本地时间)
public async Task StoreAsync(PersistedGrant grant)
{
//var expiresIn = grant.Expiration - DateTimeOffset.UtcNow;
var db = await _redisCache.GetDatabaseAsync();
var trans = db.CreateTransaction();
var expiry = grant.Expiration.Value.ToLocalTime();
db.HashSetAsync(grant.Key, GetHashEntries(grant)); //GetHashEntries是将对象PersistedGrant转换为HashEntry数组
db.KeyExpireAsync(grant.Key, expiry);
await trans.ExecuteAsync();
}
同时,把GetAsync和RemoveAsync的代码填上:
public async Task<PersistedGrant> GetAsync(string key)
{
var db = await _redisCache.GetDatabaseAsync();
var items = await db.HashGetAllAsync(key);
return GetPersistedGrant(items); //将HashEntry数组转换为PersistedGrant对象
}
public asyn