Owin + WebApi + OAuth2 搭建授權模式(授權碼模式 Part I)


  最近想要整理自己代碼封裝成庫,也十分想把自己的設計思路貼出來讓大家指正,奈何時間真的不隨人意。

  想要使用 OWIN 做中間件服務,該服務中包含 管線、授權 兩部分。於是決定使用 webapi 、OAuth2 來做。

  在搭建途中,幾乎是步步遇坎,由於對 OAuth2 內部流轉的不了解,在網上到處找大牛的文獻介紹,也整理不少,最后貼出。

  在捋順出驗證整個內部過程后,遇到了如何使用 js 來發送請求達到驗證,以及解決了遇到的跨域問題。

  目前僅整理出了 授權碼模式 ,閑言少敘,說說自己的理解吧。

 

1. 授權碼理論,此部分摘要網上介紹較為詳細的貼圖

 

1.1 結合例子來說,當我們與某網站進行合作,需要得到他們的授權信息,在雙方協商后,確立了

  1.1.1 http://127.0.0.1:10000 對方授權地址

  1.1.2 grant_type : authorization_code 授權碼模式

  1.1.3 response_type : code 授權類型

  1.1.4 client_id : lightxun 客戶端ID

  1.1.5 redirect_uri : http://localhost:58632 返回接收 authorization_code 的地址

  1.1.6 state : login 狀態,我用來做標識當前請求狀態

1.2 當我們在某網站進行登錄時,會可以快捷的使用QQ、微博等賬號進行授權登錄。那么我們第一步點擊登錄方式,頁面會調轉到 對方授權地址,同時攜帶以上參數,最終獲得授權碼,觸發【A】Authorization Request

<a href="http://127.0.0.1:10000/authorize?grant_type=authorization_code&response_type=code&client_id=lightxun&redirect_uri=http://localhost:58632/&state=login" target="_blank">authorize</a>

 

  1.2.1  在某網站后台授權中 首先進行驗證被注冊的重定向url, 此處我的做法,在其內部將傳來的 client_id 與 之前協商的 client_id 進行對比,如無誤,則通過驗證之前協商的 redirect_uri,為了安全,防止釣魚,該方法對應為 OpenAuthorizationServerProvider 下的 ValidateClientRedirectUri 方法。此類為繼承於 OAuthAuthorizationServerProvider ,並重寫其中幾部重要的處理方法。

/// <summary>
/// 驗證 redirect_uri, 用於驗證被注冊的跳轉Url
/// </summary>
public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
  //驗證uri 為了安全,防釣魚
  if(context.ClientId == OpenAuthorizationClients.Client.Id)
  {
    //將傳來的redirectUri 與 參數驗證對比, 所以該參數最好取自數據庫
    context.Validated(OpenAuthorizationClients.Client.RedirectUri);
  }
}

 

  1.2.2 在通過了上面的方法驗證后,會驗證 authorization_code 請求,該方法對應為 OpenAuthorizationServerProvider 下的 ValidateAuthorizeRequest 方法

/// <summary>
/// 驗證 authorization_code 的請求
/// </summary>
public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
{
  // IsAuthorizationCodeGrantType : 如果“response_type”查詢字符串參數為“code”,則為 True
  // IsImplicitGrantType : 如果“response_type”查詢字符串參數為“token”,則為 True
  if (context.AuthorizeRequest.ClientId == OpenAuthorizationClients.Client.Id &&
    (context.AuthorizeRequest.IsAuthorizationCodeGrantType || context.AuthorizeRequest.IsImplicitGrantType))
  {
    // 滿足以上條件, 標記為已驗證
    context.Validated();
   }
  else
  {
    context.Rejected();
  }
}

 

  1.2.3 接着開始處理 authorization_code 請求,來生成授權碼,該過程當中整理了一下邏輯,通過 state 來判斷當前請求的狀態, 如果是 login 則證明需要登錄,登錄后會將state修改為 validate 並重新發送驗證請求。如果是 validate 則說明已成功登錄,可以生成授權碼了。該方法對應為 OpenAuthorizationServerProvider 下的 AuthorizeEndpoint 方法

/// <summary>
/// 處理登錄邏輯
/// <summary>
[HttpPost] [Route(
"OAuth/Login")] public Model.ApiResult Login([FromBody]dynamic obj) {   ///驗證用戶名密碼   IOwinContext _context = (OwinContext)Request.Properties["MS_OwinContext"];   IOwinRequest _request = _context.Request;   IOwinResponse _response = _context.Response;   string _redirectUri = HttpUtility.UrlDecode(_request.Headers["redirect_uri"]);   string _clientId = _request.Headers["client_id"];   string _host = _request.Host.Value;   return new Model.ApiResult   {     Data = $"/authorize?grant_type=authorization_code&response_type=code&client_id={_clientId}&redirect_uri={_redirectUri}&state=validate",     Msg = "for test"   }; 、}

 

/// <summary>
/// 生成 authorization_code(authorization code 授權方式)、生成 access_token (implicit 授權模式)
/// </summary>
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{

  //implicit 授權方式
  if (context.AuthorizeRequest.IsImplicitGrantType)
  {
    var identity = new ClaimsIdentity("Bearer");
    context.OwinContext.Authentication.SignIn(identity);
    context.RequestCompleted();
  }
  //authorization code 授權方式
  else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
  {
    // 通過state 來判斷, 是登錄還是 已登錄的獲取 code階段
    switch (context.AuthorizeRequest.State)
    {
      //如果是登錄狀態, 則直接跳轉, 進行賬戶驗證
      case "login":
        context.Response.Redirect("http://" + context.Request.Host.Value + "/Page/OAuth/Login.html");
        context.RequestCompleted();
        break;
      case "validate":
        var redirectUri = context.Request.Query["redirect_uri"];
        var clientId = context.Request.Query["client_id"];
        var identity = new ClaimsIdentity(new GenericIdentity(clientId, OAuthDefaults.AuthenticationType));

        var authorizeCodeContext = new AuthenticationTokenCreateContext(
          context.OwinContext,
          context.Options.AuthorizationCodeFormat,
          new AuthenticationTicket(
            identity,
            new AuthenticationProperties(new Dictionary<string, string>
            {
              {"client_id", clientId},
              {"redirect_uri", redirectUri}
            })
            {
              IssuedUtc = DateTimeOffset.UtcNow,
              ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AuthorizationCodeExpireTimeSpan)
            }));

        await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
        context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
        context.RequestCompleted();
        break;
      default:

        break;
    }
  }
}

 

  1.2.4 生成 authorization_code 並返回 , 該方法對應 OpenAuthorizationCodeProvider 下的 Create 方法。該類繼承於 AuthenticationTokenProvider, 觸發【B】Authorization Grant

/// <summary>
/// 生成 authorization_code
/// </summary>
public override void Create(AuthenticationTokenCreateContext context)
{
  context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
  _authenticationCodes[context.Token] = context.SerializeTicket();
}

 

1.3 在接收到授權碼之后,我們攜帶授權碼、授權類型、重定向地址,以及設置請求header中加入 Authorization 參數。去尋要 token。觸發【C】Authorization Grant

  1.3.1 發送請求 js 代碼如下

$.ajax({
  async: true,
  type: 'post',
  url: 'http://127.0.0.1:10000/token',
  beforeSend: function(xhr){
    xhr.setRequestHeader('Authorization', "Basic " + Base64_Encode("lightxun:shinichi"))
  },
  data: {
    grant_type: 'authorization_code',
    code: _code,  //授權碼
    redirect_uri: "http://localhost:58632/"
  },
  dataType: 'json',
  contentType: 'application/json;charset=utf-8',
  success: function (data) {
    _token = data.access_token;
    _refreshToken = data.refresh_token;
  }
});

 

  1.3.2 后台接收請求處理,首先驗證 client 身份信息(ClientId 及 ClientSecret),該方法對應 OpenAuthorizationServerProvider 下的 ValidateClientAuthentication

/// <summary>
/// 驗證Client的身份(ClientId以及ClientSecret)
/// 驗證 client 信息, 驗證從Basic架構的請求頭或Form表單提交過來的客戶端憑證
/// </summary>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
  string clientId;
  string clientSecret;
  if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
  {
    context.TryGetFormCredentials(out clientId, out clientSecret);
  }

  if (clientId != OpenAuthorizationClients.Client.Id)
  {
    context.SetError("invalid_client", "client is not valid");
    return;
  }
  context.Validated();
}

 

  1.3.3  驗證后,開始將 authorization_code 解析成 access_token,該方法對應 OpenAuthorizationCodeProvider 下的 Receive

/// <summary>
/// 由 authorization_code 解析成 access_token
/// </summary>
public override void Receive(AuthenticationTokenReceiveContext context)
{
  string value;
  if (_authenticationCodes.TryRemove(context.Token, out value))
  {
    context.DeserializeTicket(value);
  }
}

 

  1.3.4 驗證 token,該方法對應 OpenAuthorizationServerProvider 下的 ValidateTokenRequest

/// <summary>
/// 驗證 access_token 的請求
/// </summary>
public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
{
  if (context.TokenRequest.IsAuthorizationCodeGrantType || context.TokenRequest.IsRefreshTokenGrantType)
  {
    context.Validated();
  }
  else
  {
    context.Rejected();
  }
}

 

  1.3.5 生成 token,該方法對應 OpenRefreshTokenProvider 下的 Create 。觸發【D】Access Token

/// <summary>
/// 生成 refresh_token
/// </summary>
public override void Create(AuthenticationTokenCreateContext context)
{
  context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
  context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60);

  context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
  _refreshTokens[context.Token] = context.SerializeTicket();
}

 

1.4 最后我們攜帶着 token 去請求資源即可。觸發【E】Access Token 和 【F】Protected Resource

$.ajax({
  async: true,
  type: 'post',
  url: 'http://127.0.0.1:10000/token',
  beforeSend: function (xhr) {
    xhr.setRequestHeader('Authorization', "Basic " + Base64_Encode("lightxun:shinichi"))
  },
  data: {
    grant_type: 'refresh_token',
    refresh_token: _refreshToken,
  },
  dataType: 'json',
  contentType: 'application/json;charset=utf-8',
  success: function (data) {
    _token = data.access_token;
    _refreshToken = data.refresh_token;
  }
})

 

今天整理的有點兒多,還有許多沒有寫到位,后續慢慢補充,也會把全額代碼貼出來,包括 OAuth 部分全額配置及代碼。

期間參考過的大牛博文連接如下

https://www.cnblogs.com/xishuai/p/aspnet-webapi-owin-oauth2.html

https://www.cnblogs.com/YamatAmain/p/5029466.html

https://www.code996.cn/post/2018/token-front/

https://www.cnblogs.com/xizz/archive/2015/12/18/5056195.html

https://cloud.tencent.com/developer/article/1090017

https://cloud.tencent.com/developer/article/1340117

https://cloud.tencent.com/developer/article/1157890

https://cloud.tencent.com/developer/article/1096046

http://blogread.cn/it/article/7808?f=wb_blogread

---- 以下為跨域文獻

http://jcblog.net.cn/2016/07/19/webapi%E8%B7%A8%E5%9F%9F%E8%AF%B7%E6%B1%82%EF%BC%88cors%EF%BC%89%E9%85%8D%E7%BD%AE/

https://www.cnblogs.com/baiyunchen/p/5769884.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM