• 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

  • 相关阅读:
    前天晚上终于可以骑自行车了
    第一篇cnblog!
    使用web服务查询数据库的例子(上)
    使用网络提供的web服务开发航班查询程序
    使用web服务查询数据库的例子(下)
    .NET技术与企业级解决方案研究应用
    用正则表达式解析url
    基于原型的类继承
    Pub/Sub模式
    函数调用apply
  • 原文地址:https://www.cnblogs.com/lightxun/p/9915672.html
Copyright © 2020-2023  润新知