开源项目葫芦藤:IdentityServer4的实现及其运用

本文详细介绍了开源项目葫芦藤中IdentityServer4的实现,包括签名证书、客户端存储、资源存储、持久化授权存储的配置和使用。此外,还探讨了OAuth2.0的实践场景,如基于角色的授权、客户端授权模式、授权码模式、密码模式以及自定义授权模式(短信、第三方登录)。文中还提供了具体的代码示例和配置方法。
摘要由CSDN通过智能技术生成

前言

本篇文章主要是讲解葫芦藤项目中对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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值