開源項目葫蘆藤:IdentityServer4的實現及其運用

前言

本篇文章主要是講解葫蘆藤項目中對IdentityServer的實踐使用,爲了使您對本篇文章中所講述的內容有深入的認識,而且在閱讀時避免感到乏味,文中的內容不會涉及太多的基礎理論知識,而更多的是採用動手實踐的方式進行講解,因此在閱讀此篇文章前假定您已經掌握了OAuth2.0的基礎知識,如您事先並未瞭解OAuth2.0,請參閱一下阮一峯老師的文章《理解OAuth2.0》, ASP.NET Core 認證與受權,能夠看看博客 雨夜朦朧,另外IdentityServer的相關文章也能夠參考博客 曉晨Masterhtml

葫蘆藤前端地址:https://account.suuyuu.cn (驗證碼獲取後,輸入123456便可)前端

葫蘆藤後端地址:https://account-web.suuyuu.cngit

葫蘆藤源碼地址:https://github.com/fuluteam/fulusso (幫忙點個小星星哦)github

團隊博文地址:https://www.cnblogs.com/fuluweb

簽名證書(Signing Credential)

IdentityServer支持X.509證書(包括原始文件和對Windows證書存儲庫的引用)、RSA密鑰和EC密鑰,用於令牌簽名和驗證。每一個密鑰均可以配置一個(兼容的)簽名算法,如RS25六、RS38四、RS5十二、PS25六、PS38四、PS5十二、ES25六、ES384或ES512。redis

一般狀況下,咱們使用的是針對開發場景建立的臨時證書 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的文件,此時咱們的證書就建立成功啦。
接着怎麼使用呢,看下面代碼:json

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持久化配置和操做數據,請參考
http://www.javashuo.com/article/p-obrxclck-bm.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 async Task RemoveAsync(string key)
{
    var db = await _redisCache.GetDatabaseAsync();
    await db.KeyDeleteAsync(key);
}

接着,GetAllAsync方法,經過subjectId查詢PersistedGrant集合,1對n,所以,咱們在StoreAsync中補上這一層關係,以subjectId爲緩存key,grant.Key爲緩存值存入list集合中;GetAllAsync方法中,經過subjectId取出grant.Key的集合,最終獲得PersistedGrant集合。

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);
    
    db.ListLeftPushAsync(grant.SubjectId, grant.Key);
    db.KeyExpireAsync(grant.SubjectId, expiry);
                
    await trans.ExecuteAsync();
}

public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
{
    if (string.IsNullOrWhiteSpace(subjectId))
        return new List<PersistedGrant>();

    var db = await _redisCache.GetDatabaseAsync();

    var keys = await db.ListRangeAsync(subjectId);

    var list = new List<PersistedGrant>();
    foreach (string key in keys)
    {
        var items = await db.HashGetAllAsync(key);
        list.Add(GetPersistedGrant(items));
    }

    return list;
}

相似的,StoreAsync方法中咱們只需StoreAsync方法中根據RemoveAllAsync方法參數組裝緩存key,grant.Key爲緩存值寫入緩存,對應的RemoveAllAsync中根據參數組裝的key查詢出grant.Key集合,刪除緩存便可。

public async Task StoreAsync(PersistedGrant grant)
{
    var db = await _redisCache.GetDatabaseAsync();

    var trans = db.CreateTransaction();

    var expiry = grant.Expiration.Value.ToLocalTime();

    db.HashSetAsync(grant.Key, GetHashEntries(grant));
    db.KeyExpireAsync(grant.Key, expiry);

    if (!string.IsNullOrEmpty(grant.SubjectId))
    {
        db.ListLeftPushAsync(grant.SubjectId, grant.Key);
        db.KeyExpireAsync(grant.SubjectId, expiry);

        var key1 = $"{grant.SubjectId}:{grant.ClientId}";
        db.ListLeftPushAsync(key1, grant.Key);
        db.KeyExpireAsync(key1, expiry);

        var key2 = $"{grant.SubjectId}:{grant.ClientId}:{grant.Type}";
        db.ListLeftPushAsync(key2, grant.Key);
        db.KeyExpireAsync(key2, expiry);
    }

    await trans.ExecuteAsync();
}

public async Task RemoveAllAsync(string subjectId, string clientId)
{
    if (string.IsNullOrEmpty(subjectId) || string.IsNullOrEmpty(clientId))
        return;
    var db = await _redisCache.GetDatabaseAsync();

    var key = $"{subjectId}:{clientId}";
    var keys = await db.ListRangeAsync(key);
    if (!keys.Any()) return;

    var trans = db.CreateTransaction();
    db.KeyDeleteAsync(keys.ToRedisKeys());
    db.KeyDeleteAsync(key);
    await trans.ExecuteAsync();
}

public async Task RemoveAllAsync(string subjectId, string clientId, string type)
{
    if (string.IsNullOrEmpty(subjectId) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(type))
        return;
    var db = await _redisCache.GetDatabaseAsync();

    var key = $"{subjectId}:{clientId}:{type}";
    var keys = await db.ListRangeAsync(key);
    if (!keys.Any()) return;

    var trans = db.CreateTransaction();
    db.KeyDeleteAsync(keys.ToRedisKeys());
    db.KeyDeleteAsync(key);
    await trans.ExecuteAsync();
}

至此,持久化的代碼填寫完畢;啓動並調試項目,能夠看到PersistedGrant對象以下:

Dingtalk_20201207162726

資源擁有者驗證器(Resource Owner Validator)

若是要使用OAuth 2.0 密碼模式(Resource Owner Password Credentials Grant),則須要實現並註冊IResourceOwnerPasswordValidator接口:

public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
    var result = await _userService.LoginByPasswordAsync(context.UserName, context.Password);
    if (result.Code == 0)
    {
        var claims = await _userService.SaveSuccessLoginInfo(context.Request.ClientId.ToInt32(), result.Data.Id,
         _contextAccessor.HttpContext.GetIp(), UserLoginModel.Password);
        context.Result = new GrantValidationResult(result.Data.Id, OidcConstants.AuthenticationMethods.Password, claims);
    }
    else
    {
        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, result.Message);
    }
}

重定向地址驗證器(Redirect Uri Validator)

用於驗證重定向(受權碼模式)和註銷後重定向Uri的校驗,葫蘆藤項目中重定向地址驗證只驗證域名(不驗證完整的requestedUri地址),且未進行註銷重定向Uri的校驗。

public class RedirectUriValidator : IRedirectUriValidator
{
    public Task<bool> IsRedirectUriValidAsync(string requestedUri, Client client)
    {
        if (client.RedirectUris == null || !client.RedirectUris.Any())
        {
            return Task.FromResult(false);
        }
        var uri = new Uri(requestedUri);
        return Task.FromResult(client.RedirectUris.Any(x => x.Contains(uri.Host)));
    }

    public Task<bool> IsPostLogoutRedirectUriValidAsync(string requestedUri, Client client)
    {
        return Task.FromResult(true);
    }
}

擴展受權驗證器(Extension Grant Validator)

在IdentityServer4中,經過實現IExtensionGrantValidator接口,能夠實現自定義受權。在葫蘆藤項目中,咱們有兩個場景須要用到自定義受權:

  • 經過第三方(QQ、微信)的用戶標識(OpenId)進行登陸(頒發用戶令牌)
  • 經過短信驗證碼進行登陸(頒發用戶令牌)

在IdentityServer4中實現短信驗證碼受權模式,咱們建立了一個SmsGrantValidator類,繼承自IExtensionGrantValidator接口,而後給屬性GrantType取一個名字,此處名稱爲「sms」,實現ValidateAsync方法,方法內進行入參校驗,而後驗證短信驗證碼,驗證經過後取出用戶信息,下面代碼中,當用戶不存在時也能夠自動註冊。代碼以下:

public class SmsGrantValidator : IExtensionGrantValidator
{
    private readonly IHttpContextAccessor _contextAccessor;
    private readonly IValidationComponent _validationComponent;
    private readonly IUserService _userService;

    public SmsGrantValidator(IHttpContextAccessor contextAccessor, IValidationComponent validationComponent, IUserService userService)
    {
        _contextAccessor = contextAccessor;
        _validationComponent = validationComponent;
        _userService = userService;
        GrantType = CustomGrantType.Sms;
    }

    public async Task ValidateAsync(ExtensionGrantValidationContext context)
    {
        var phone = context.Request.Raw.Get("phone");
        var code = context.Request.Raw.Get("code");
        if (string.IsNullOrEmpty(phone) || Regex.IsMatch(phone, RegExp.PhoneNumber) == false)
        {
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "phone is not valid");
            return;
        }
        if (string.IsNullOrEmpty(code))
        {
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "code is not valid");
            return;
        }

        try
        {
            var validSms = await _validationComponent.ValidSmsAsync(phone, code);
            if (!validSms.Data)
            {
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, validSms.Message);
                return;
            }

            var userEntity = await _userService.GetUserByPhoneAsync(phone);
            if (userEntity == null)
            {
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "用戶不存在或未註冊");
                return;
            }
            if (userEntity.Enabled == false)
            {
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "您的帳號已被禁止登陸");
                return;
            }

            await _userService.SaveSuccessLoginInfo(context.Request.ClientId.ToInt32(), userEntity.Id, _contextAccessor.HttpContext.GetIp(),
                 UserLoginModel.SmsCode);
        }
        catch (Exception ex)
        {
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, ex.Message);
        }
    }

    public string GrantType { get; }
}

OAuth2.0的實踐運用場景

基於角色的受權(role-based authorization)

基於角色的受權檢查是聲明性的,開發人員將其嵌入到代碼中、控制器或控制器內的操做,指定當前用戶必須是其成員的角色才能訪問請求的資源,文檔參考《ASP.NET Core 中的基於角色的受權》。

葫蘆藤中定義了兩種角色Claim(聲明),客戶端和用戶,使用客戶端受權模式(client credentials)頒發的令牌,ClaimRole爲Client,使用受權碼模式(authorization code)、密碼模式(resource owner password credentials)、自定義受權模式(短信、第三方)頒發的用戶令牌,ClaimRole爲User

public static class ClaimRoles
{
    /// <summary>
    /// 客戶端
    /// </summary>
    public const string Client = "Client";
    /// <summary>
    /// 用戶
    /// </summary>
    public const string User = "User";
}

在ClientStore中增長返回Client的Claims,JwtClaimTypes.Role爲ClaimRoles.Client,下面是客戶端令牌,能夠看到 "role":"Client"

{"alg":"RS256","kid":"99AA0C1236097972F29789562761D38AAE301918","typ":"JWT","x5t":"maoMEjYJeXLyl4lWJ2HTiq4wGRg"}
{"nbf":1608522625,"exp":1608529825,"iss":"http://localhost:80","aud":"api","client_id":"10000001","role":"Client","scope":["api","get_user_info"]}

在用戶登陸成功後返回的Claims中增長JwtClaimTypes.Role爲ClaimRoles.User,下面是用戶令牌,能夠看到 "role":"User"

{"alg":"RS256","kid":"99AA0C1236097972F29789562761D38AAE301918","typ":"JWT","x5t":"maoMEjYJeXLyl4lWJ2HTiq4wGRg"}
{"nbf":1608522576,"exp":1608529776,"iss":"http://localhost:80","aud":"api","client_id":"10000001","sub":"df09efff-0074-4dca-91c3-e38180c5e4ac","auth_time":1608522576,"idp":"local","id":"df09efff-0074-4dca-91c3-e38180c5e4ac","open_id":"07E8E30B56D256EF8C440019AB6AAA89","name":"1051dfd1-73e5-4e6f-9326-3423bc9b71a3","nickname":"laowang","phone_number":"18627131390","email":"","role":"User","login_ip":"0.0.0.1","login_address":"保留地址","last_login_ip":"0.0.0.1","last_login_address":"保留地址","scope":["api","get_user_info"],"amr":["pwd","mfa"]}

在項目Fulu.Passport.API的Startup文件中,添加對組件Fulu.Service.Authorize的服務注入

services.AddServiceAuthorize(o =>...代碼省略...);
services.AddAuthentication(x =>...代碼省略...).AddJwtBearer(o =>
{
    ...代碼省略...
    o.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = JwtClaimTypes.Name,
        RoleClaimType = ClaimTypes.Role,    //注意,這裏不能使用JwtClaimTypes.Role
    ...代碼省略...
    }
}

接着,只需在Controller或Action上指定屬性便可

[Route("api/[controller]/[action]")]
[ApiController]
[Authorize(Roles = ClaimRoles.Client)]
public class ClientController : ControllerBase
{
    ...省略部分代碼...
    /// <summary>
    /// 獲取應用列表
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [ProducesResponseType(typeof(ActionObjectResult<List<ClientEntity>, Statistic>), 200)]
    public async Task<IActionResult> GetClients()
    {
        var clients = await _clientRepository.TableNoTracking.Where(c => c.Enabled).ToListAsync();
        return ObjectResponse.Ok(clients);
    }
    ...省略部分代碼...

7d9fabdf-deec-125e-96b0-042fef959955

客戶端受權模式(client credentials)

經過客戶端受權模式頒發的令牌,能夠實現對服務資源進行保護。步驟以下:

(A)客戶端10000001向葫後進行身份認證,並要求一個訪問令牌。

(B)葫後驗證客戶端身份後,向客戶端10000001提供訪問令牌。

A步驟中,客戶端10000001發出的HTTP請求,包含如下參數:

  • grant_type:表示受權類型,此處的值固定爲"clientcredentials",必選項。
  • client_id:表示客戶端的ID,必選項。
  • client_secret:表示客戶端密鑰,必選項。
POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1
Host: www.xxx.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w

B步驟中,葫蘆藤向客戶端10000001發放令牌,下面是一個例子。

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk5QUEwQzEyMzYwOTc5NzJGMjk3ODk1NjI3NjFEMzhBQUUzMDE5MTgiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJtYW9NRWpZSmVYTHlsNGxXSjJIVGlxNHdHUmcifQ.eyJuYmYiOjE2MDc0MTQ2MjUsImV4cCI6MTYwNzQyMTgyNSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwicm9sZSI6IkNsaWVudCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkNsaWVudCIsInNjb3BlIjpbImFwaSIsImdldF91c2VyX2luZm8iXX0.ilu1qMxDiXVxsqU6aO-xuyYaLvvj2mxONjYkXtpMs46K7O3_Qc5VsY0ZZaYPoLROAqPulxsWWpxjEiQd10OdRh4IziGAcpYfAfoD80CZxrcuWrWloB5aWncv_PMZcjzKw7Vt3G3g-WkJl4amTta498hZJ3B-N-ReLhl-3ICSMFU8PU_ZVtEB-2lRx93rVyPIaQu_DWmpyW4Bdf2ocYm4RPQAEsvBToEFObbWPG6paLWIjrSN2aQPvsRWziorvlIhyFV5L6oyFIGIrZxdLJTOsvRQaevpV1sbv9pD_Z9PZDbSQiQDbWQv0MfrYB0Npc6VQlIMkL2GPNlQ8NgwyGT1sQ",
    "expires_in": 7200,
    "token_type": "Bearer",
    "scope": "api get_user_info"
}

5c894611-a780-bb6d-487b-e339f16f5c4c

受權碼模式(authorization code)

葫蘆藤項目經過受權碼模式(authorization code)實現了單點登陸,經過受權碼模式拿到用戶令牌。目前葫蘆藤只有一個應用(葫蘆藤安全中心),這裏爲了避免把概念搞混淆,咱們假定百度(客戶端10000002,redirect_uri 爲 http://www.baidu.com)接入了我們的受權體系,固然,百度的前端確定沒有寫如何構造請求步驟的邏輯代碼,所以,咱們下面經過人工模擬請求步驟。

名詞定義

(A)用戶訪問「百前」,「百前」將用戶導向「葫後」。
(B)「葫後」檢查用戶是否須要登陸(是否攜帶了有效的登陸Cookie),如需登陸跳轉到「葫前」。
(C)用戶登陸後,「葫後」將用戶導向百度事先指定的"重定向URI"(redirection URI),同時附上一個受權碼。
(D)「百前」收到受權碼,附上早先的"重定向URI",向「百後」申請令牌,「百後」拿到受權碼以後攜帶密鑰client_secret向「葫後」申請令牌。
(E)「葫後」覈對了受權碼和重定向URI,確認無誤後,向「百後」頒發訪問令牌(access token)。
(F)「百後」將令牌返回給「百前」。

A步驟中,構造的請求地址包含如下參數:

  • response_type:表示受權類型,必選項,此處的值固定爲"code"
  • client_id:表示客戶端的ID,必選項
  • redirect_uri:表示重定向URI,可選項
  • scope:表示申請的權限範圍,可選項
  • state:表示客戶端的當前狀態,能夠指定任意值,認證服務器會原封不動地返回這個值

步驟A中開發人員需向前端人員提供client_id,即上面的client_id,下面是一個例子。

構造以下地址,複製到瀏覽器地址欄中並回車,若是跳轉到登陸頁,請進行登陸。

https://account-web.suuyuu.cn/connect/authorize?client_id=10000002&redirect_uri=https%3A%2F%2Fwww.baidu.com&response_type=code&scope=api&state=STATE

登陸後會重定向redirect_uri到以下地址:

https://www.baidu.com/?code=1MlxrvXuD7TfH-s4dLzcw9ymO0SKDbf5xAlh3ZEHlMo&scope=api&state=STATE

D步驟中,咱們經過臨時受權碼向「葫後」索取令牌,包含如下參數:

  • grant_type:表示使用的受權模式,必選項,此處的值固定爲"authorization_code"。
  • code:表示上一步得到的受權碼,必選項。
  • redirect_uri:表示重定向URI,必選項,且必須與A步驟中的該參數值保持一致。
  • client_id:表示應用ID,必選項。
  • client_secret:表示應用密鑰,必選項。
POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1
Host: account-web.suuyuu.cn
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=1MlxrvXuD7TfH-s4dLzcw9ymO0SKDbf5xAlh3ZEHlMo&redirect_uri=https%3A%2F%2Fwww.baidu.com&client_id=10000002&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w
{
    "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwQzQ3OUY1QUIyQTFERjM2QzE0MkNEQjQ3NjQ1QkEwMzQ1MTg1NUEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjTVI1OWFzcUhmTnNGQ3piUjJSYm9EUlJoVm8ifQ.eyJuYmYiOjE2MDc0MjY0MjcsImV4cCI6MTYwNzQzMzYyNywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwiYXV0aF90aW1lIjoxNjA3NDI2MTk2LCJpZHAiOiJsb2NhbCIsImlkIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwib3Blbl9pZCI6IjA3RThFMzBCNTZEMjU2RUY4QzQ0MDAxOUFCNkFBQTg5IiwibmFtZSI6IjEwNTFkZmQxLTczZTUtNGU2Zi05MzI2LTM0MjNiYzliNzFhMyIsIm5pY2tuYW1lIjoibGFvd2FuZyIsInBob25lX251bWJlciI6IjE4NjI3MTMxMzkwIiwiZW1haWwiOiIiLCJyb2xlIjoiVXNlciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJsb2dpbl9pcCI6IjExMy41Ny4xMTguNTEiLCJsb2dpbl9hZGRyZXNzIjoi5rmW5YyX55yB5q2m5rGJ5biCIiwibGFzdF9sb2dpbl9pcCI6IjExMy41Ny4xMTguNTEiLCJsYXN0X2xvZ2luX2FkZHJlc3MiOiLmuZbljJfnnIHmrabmsYnluIIiLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbIm1mYSJdfQ.ElnHr5Niknq7kzGL8iv1TH0F6NQ21yPrswzSTIZuvetUxztYgQpD-RfgBW2HL6b_rRyQxFjE23gU4lBIEayM8k3M9_sUzZq8E_dFT8LwpsU76-CxepxHft4hn1YG0a5C6QRyjFQoSFVUZXIp663Es7vwRQ6PgsfkHZKXxAqXL-obHj_QLbv6OeciTIRGwYrL9-1_SDQ4esFR2n8LkGGOug55j9QuQEKMCufQLJ-nB3y7A2-0mnNoiuF2BBYSPLamcvMcLe8LbhCITLrHkcUSc6tsSdnEeisS6BMIoiyRq-LR2jJwDD30swTPFd85v6kUBJ3ZnWjeCqsluGGKHrwDLA",
    "expires_in":7200,
    "token_type":"Bearer",
    "scope":"api"
}

87a20dc0-bd01-ef6d-524e-2403f1a9f263

密碼模式(resource owner password credentials)

密碼模式主要用於給可信應用頒發用戶令牌,此類應用有個性化的登陸頁(不依賴單點登陸,葫蘆藤的登陸頁面),如app、小程序、h5等。

  • grant_type:表示受權類型,此處的值固定爲"password",必選項。
  • client_id:表示客戶端的ID,必選項。
  • client_secret:表示客戶端密鑰,必選項。
  • username:用戶名,必選項。
  • password:密碼,必選項。(基於密碼原文的rsa加密串)
POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1
Host: account-web.suuyuu.cn
Content-Type: application/x-www-form-urlencoded

grant_type=password&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w&username=18627131390&password=0200f6389afbcbc624811785c9fbbf5c1b6d7b53b1315a1a43021c0733323fab7625bb9e6594cd30758fa700798421bc189dc223bf696d2438530ffab337809b96bb47ee38f3416bf4b57222050d5f4ad66ee052598ea62ff5ec6f991729956cb692f6f48b758564a46aeff86208581cad9063d3ccd71b551fa4b4b4b983fc1a
{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwQzQ3OUY1QUIyQTFERjM2QzE0MkNEQjQ3NjQ1QkEwMzQ1MTg1NUEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjTVI1OWFzcUhmTnNGQ3piUjJSYm9EUlJoVm8ifQ.eyJuYmYiOjE2MDc1MTE2NTEsImV4cCI6MTYwNzUxODg1MSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwiYXV0aF90aW1lIjoxNjA3NTExNjUxLCJpZHAiOiJsb2NhbCIsImlkIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwib3Blbl9pZCI6IjA3RThFMzBCNTZEMjU2RUY4QzQ0MDAxOUFCNkFBQTg5IiwibmFtZSI6IjEwNTFkZmQxLTczZTUtNGU2Zi05MzI2LTM0MjNiYzliNzFhMyIsIm5pY2tuYW1lIjoibGFvd2FuZyIsInBob25lX251bWJlciI6IjE4NjI3MTMxMzkwIiwiZW1haWwiOiIiLCJyb2xlIjoiVXNlciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJsb2dpbl9pcCI6IjExMy41Ny4xMTguNjEiLCJsb2dpbl9hZGRyZXNzIjoi5rmW5YyX55yB5q2m5rGJ5biCIiwibGFzdF9sb2dpbl9pcCI6IjExMy41Ny4xMTguNjEiLCJsYXN0X2xvZ2luX2FkZHJlc3MiOiLmuZbljJfnnIHmrabmsYnluIIiLCJzY29wZSI6WyJhcGkiLCJnZXRfdXNlcl9pbmZvIl0sImFtciI6WyJwd2QiLCJtZmEiXX0.d3qvhX6KSdm5EgWpUzbjJX2bB1OiUo-285nZ1qsGKpqTQJUH1VHQoJogB0NI-uVYdgIV-y3CMBhFY_fDYQJto43zDf0gDvYxa2eWnX5MWL7Augigi59Icp0YvNDCGd2iT5ztAWpxk1Jww815TtCFtFFGiQfQC75bKLrTW9QvdXr8t4VHcFKGmz92m8g3WL-0eWqAyvk0YuSBvxOd8P8zoocEiiOgVKTSylphSIQxuC8B4MFNf2DoFWDQjNZmDCs7PLh7sniMmLdfilo7T7gAlq9qjUrmQmav4wbDMT8WZqa01WY-LsWq6mZUnbCytgSu7Xrr90b6LAEGn-hxdQ5VHg",
    "expires_in": 7200,
    "token_type": "Bearer",
    "scope": "api get_user_info"
}

82b99de6-bf25-71ad-ad8e-0fd04fafe274

自定義受權模式(短信、第三方)(extension grant)

客戶端經過用戶手機號短信驗證碼或第三方用戶(QQ、WeChat)的用戶惟一標識(OpenId)向認證服務器索要用戶令牌。

以短信驗證碼方式爲例,咱們定義的流程以下:

用戶向客戶端提供本身的手機號和短信驗證碼。客戶端使用這些信息,向認證服務器索要受權。 步驟以下:

(A)用戶向客戶端提供手機號和短信驗證碼。
(B)客戶端將手機號和短信碼發給認證服務器,向後者請求令牌。
(C)認證服務器確認無誤後,向客戶端提供用戶令牌。

B步驟中,客戶端發出的HTTP請求,包含如下參數:

  • grant_type:表示受權類型,此處的值固定爲"sms",必選項。
  • client_id:表示客戶端的ID,必選項。
  • client_secret:表示客戶端的密鑰,必選項。
  • phone:表示手機號,必選項。
  • code:表示短信驗證碼,必選項。

下面是一個請求示例。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1
Host: account-web.suuyuu.cn
Content-Type: application/x-www-form-urlencoded

grant_type=sms&phone=18627131390&code=123456&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w
{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk5QUEwQzEyMzYwOTc5NzJGMjk3ODk1NjI3NjFEMzhBQUUzMDE5MTgiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJtYW9NRWpZSmVYTHlsNGxXSjJIVGlxNHdHUmcifQ.eyJuYmYiOjE2MDczOTU4NTIsImV4cCI6MTYwNzQwMzA1MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiMTg2MjcxMzEzOTAiLCJhdXRoX3RpbWUiOjE2MDczOTU4NTIsImlkcCI6ImxvY2FsIiwiaWQiOiJkZjA5ZWZmZi0wMDc0LTRkY2EtOTFjMy1lMzgxODBjNWU0YWMiLCJvcGVuX2lkIjoiMDdFOEUzMEI1NkQyNTZFRjhDNDQwMDE5QUI2QUFBODkiLCJuYW1lIjoiMTA1MWRmZDEtNzNlNS00ZTZmLTkzMjYtMzQyM2JjOWI3MWEzIiwibmlja25hbWUiOiJsYW93YW5nIiwicGhvbmVfbnVtYmVyIjoiMTg2MjcxMzEzOTAiLCJlbWFpbCI6IiIsInJvbGUiOiJVc2VyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiVXNlciIsImxvZ2luX2lwIjoiMC4wLjAuMSIsImxvZ2luX2FkZHJlc3MiOiLkv53nlZnlnLDlnYAiLCJsYXN0X2xvZ2luX2lwIjoiMC4wLjAuMSIsImxhc3RfbG9naW5fYWRkcmVzcyI6IuS_neeVmeWcsOWdgCIsInNjb3BlIjpbImFwaSIsImdldF91c2VyX2luZm8iXSwiYW1yIjpbInBhc3N3b3JkIiwibWZhIl19.ZQklMJMXObc3vL-gMOWnWIS56ck5_XbDfXjw9Vm6BeYjG4dyz05JTN_YHgU-EIJoM04nmFyjNgGYtqL-28-3MQeHfWhvQf_5dyY1w-DBBCKo1EMEm_ujKTDB1QQTN1XmVTgW7bBkEiv4NK5v3uYqh_s7pv8Csusm4oWZThWPlKLtxWVDtawFzvz4Un-2WATytsLNfluutiLVnpN7INhkdglansTTOCUOdCOLBEEbDzTuLyCnhm00xYtg5GrMAkDohqXLKYD2jSFzIyYTA_oryTFXcJpkGYwIRqRX7bXvAlMR5yE_CTtNWpSnaLJ2GtFv_QFe-YItCtSO-bBd6XQBRA",
    "expires_in": 7200,
    "token_type": "Bearer",
    "scope": "api get_user_info"
}

7aec2234-e725-4898-aa1f-db2d6d668059

第三方受權登陸的編寫與使用

在葫蘆藤項目中咱們提供了釘釘、微信的OAuth組件,並實現了功能,演示地址在 https://account.suuyuu.cn,下面咱們以微信爲例簡單介紹下如何編寫組件及使用。

首先我們閱讀一下網站應用微信登陸開發指南,瞭解一下接入流程。要使用微信登陸,先得在微信·開放平臺註冊成爲開發者,並進行資質認證。

微信開放平臺賬號的開發者資質認證提供更安全、更嚴格的真實性認證、也可以更好的保護企業及用戶的合法權益
開發者資質認證經過後,微信開放平臺賬號下的應用,將得到微信登陸、智能接口、第三方平臺開發等高級能力
審覈費用:中國大陸地區:300元,非中國大陸地區:99美圓

而後在管理中心建立網站應用

20201217161633

對照微信開發指南將須要用到的地址定義到WeChatDefaults.cs中

public static class WeChatDefaults
{
    public const string AuthenticationScheme = "wechat";

    public static readonly string DisplayName = "wechat";
    //第一步:請求CODE
    public static readonly string AuthorizationEndpoint = "https://open.weixin.qq.com/connect/qrconnect";
    //第二步:經過code獲取access_token
    public static readonly string TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token";
    //第三步:獲取用戶我的信息
    public static readonly string UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo";
}

此處惟一要注意的地方,ClaimActions集合的參數來自微信返回的字段

public class WeChatOptions : OAuthOptions
{
    /// <summary>
    /// Initializes a new <see cref="WeChatOptions"/>.
    /// </summary>
    public WeChatOptions()
    {
        CallbackPath = new PathString("/signin-wechat");
        AuthorizationEndpoint = WeChatDefaults.AuthorizationEndpoint;
        TokenEndpoint = WeChatDefaults.TokenEndpoint;
        UserInformationEndpoint = WeChatDefaults.UserInformationEndpoint;
        Scope.Add("snsapi_login");

        ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid");
        ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname");
    }

    /// <summary>
    /// access_type. Set to 'offline' to request a refresh token.
    /// </summary>
    public string AccessType { get; set; }
}
public static class WeChatExtensions
{
    public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder)
        => builder.AddWeChat(WeChatDefaults.AuthenticationScheme, _ => { });

    public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, Action<WeChatOptions> configureOptions)
        => builder.AddWeChat(WeChatDefaults.AuthenticationScheme, configureOptions);

    public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, string authenticationScheme, Action<WeChatOptions> configureOptions)
        => builder.AddWeChat(authenticationScheme, WeChatDefaults.DisplayName, configureOptions);

    public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WeChatOptions> configureOptions)
        => builder.AddOAuth<WeChatOptions, WeChatHandler>(authenticationScheme, displayName, configureOptions);
}

新增一個類WeChatHandler,繼承自OAuthHandler

BuildChallengeUrl(構造客戶端申請認證的URI)

protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
    var state = Options.StateDataFormat.Protect(properties);
    var baseUri = $"{Request.Scheme}{Uri.SchemeDelimiter}{Request.Host}{Request.PathBase}";
    var currentUri = $"{baseUri}{Request.Path}{Request.QueryString}";

    if (string.IsNullOrEmpty(properties.RedirectUri))
    {
        properties.RedirectUri = currentUri;
    }

    var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        {"response_type", "code"},
        {"appid", Uri.EscapeDataString(Options.ClientId)},
        {"redirect_uri", redirectUri},
        {"state", Uri.EscapeDataString(state)}
    };

    var scope = string.Join(",", Options.Scope);
    queryStrings.Add("scope", Uri.EscapeDataString(scope));

    var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);
    return authorizationEndpoint;
}

HandleRemoteAuthenticateAsync(向認證服務器申請令牌獲取用戶信息並建立票據)

protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{

    var state = Request.Query["state"];
    var properties = Options.StateDataFormat.Unprotect(state);

    if (properties == null)
        return HandleRequestResult.Fail("The oauth state was missing or invalid.");
    if (!ValidateCorrelationId(properties))
        return HandleRequestResult.Fail("Correlation failed.", properties);

    var code = Request.Query["code"];
    if (StringValues.IsNullOrEmpty(code))
        return HandleRequestResult.Fail("Code was not found.", properties);

    var redirectUri = !string.IsNullOrEmpty(Options.CallbackPath) ?
        Options.CallbackPath.Value : BuildRedirectUri(Options.CallbackPath);

    var context = new OAuthCodeExchangeContext(properties, code, redirectUri);

    var tokens = await ExchangeCodeAsync(context);

    if (tokens.Error != null)
        return HandleRequestResult.Fail(tokens.Error, properties);
    if (string.IsNullOrEmpty(tokens.AccessToken))
        return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
    var identity = new ClaimsIdentity(ClaimsIssuer);

    if (Options.SaveTokens)
    {
        var authenticationTokenList = new List<AuthenticationToken>
        {
            new AuthenticationToken
            {
                Name = "access_token",
                Value = tokens.AccessToken
            }
        };
        if (!string.IsNullOrEmpty(tokens.RefreshToken))
        {
            authenticationTokenList.Add(new AuthenticationToken
            {
                Name = "refresh_token",
                Value = tokens.RefreshToken
            });
        }

        if (!string.IsNullOrEmpty(tokens.TokenType))
        {
            authenticationTokenList.Add(new AuthenticationToken
            {
                Name = "token_type",
                Value = tokens.TokenType
            });
        }

        if (!string.IsNullOrEmpty(tokens.ExpiresIn) && int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
        {
            var dateTimeOffset = Clock.UtcNow + TimeSpan.FromSeconds(result);
            authenticationTokenList.Add(new AuthenticationToken()
            {
                Name = "expires_at",
                Value = dateTimeOffset.ToString("o", CultureInfo.InvariantCulture)
            });
        }
        properties.StoreTokens(authenticationTokenList);
    }

    var ticket = await CreateTicketAsync(identity, properties, tokens);
    return ticket == null ? HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties) : HandleRequestResult.Success(ticket);
}

此步驟中包含兩個子步驟

ExchangeCodeAsync(交換受權碼Code)

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
{

    var tokenRequestParameters = new List<KeyValuePair<string, string>>
    {
        new KeyValuePair<string, string>("appid", Options.ClientId),
        new KeyValuePair<string, string>("secret", Options.ClientSecret),
        new KeyValuePair<string, string>("code", context.Code),
        new KeyValuePair<string, string>("grant_type", "authorization_code"),
    };

    var urlEncodedContent = new FormUrlEncodedContent(tokenRequestParameters);

    var response =
        await Backchannel.PostAsync(Options.TokenEndpoint, urlEncodedContent, Context.RequestAborted);

    return response.IsSuccessStatusCode ? OAuthTokenResponse.Success(JsonDocument.Parse(await response.Content.ReadAsStringAsync())) : OAuthTokenResponse.Failed(new Exception("OAuth token failure"));
}

CreateTicketAsync(建立票據)

protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity,AuthenticationProperties properties,OAuthTokenResponse tokens)
{
    var openId = tokens.Response.RootElement.GetString("openid");

    var parameters = new Dictionary<string, string>
    {
        {  "openid", openId},
        {  "access_token", tokens.AccessToken }
    };
    var userInfoEndpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, parameters);
    var response = await Backchannel.GetAsync(userInfoEndpoint, Context.RequestAborted);

    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException($"An error occurred when retrieving WeChat user information ({response.StatusCode}). Please check if the authentication information is correct.");
    }

    using (var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()))
    {
        var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme,
            Options, Backchannel, tokens, payload.RootElement);

        context.RunClaimActions();
        await Events.CreatingTicket(context);

        context.Properties.ExpiresUtc = DateTimeOffset.Now.AddMinutes(15);
        return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
    }
}

組件寫好了,怎麼使用呢?在Fulu.Passport.Web項目的Startup.cs文件中添加代碼以下:

public void ConfigureServices(IServiceCollection services)
{
    ......省略部分代碼......
    
    services.AddAuthentication().AddWeChat(o =>
    {
        o.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
        o.ClientId = Configuration["ExternalWeChat:AppId"];
        o.ClientSecret = Configuration["ExternalWeChat:Secret"];
    })
}

接着,在UserController.cs中添加以下代碼:

/// <summary>
/// 外部帳號登陸
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpGet, AllowAnonymous]
public IActionResult ExternalLogin([FromQuery] ExternalLoginModel model)
{
    var authenticationProperties = new AuthenticationProperties()
    {
        RedirectUri = Url.Action(nameof(ExternalLoginCallback)),
        Items =
        {
            { "returnUrl", model.ReturnUrl },
            { "scheme", model.Provider },
        }
    };

    return Challenge(authenticationProperties, model.Provider);
}

/// <summary>
/// 外部登陸回調
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback()
{
    //獲取idsrv.external Cookie 對象
    var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

    var returnUrl = result.Properties.Items["returnUrl"];

    if (result.Succeeded == false)
    {
        return await RedirectErrorResult("error", "External authentication error", returnUrl);
    }
    ......省略部分代碼......

    //刪除 idsrv.external Cookie
    await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
    //寫入 .AspNetCore.Cookies
    await SignIn(userEntity, UserLoginModel.External);

    return Redirect(returnUrl);
}

c21353bc-0e1d-1183-7a54-0a77c7b2772a

福祿ICH·架構組 福祿娃
相關文章
相關標籤/搜索