5

.netcore 急速接入第三方登录,不看后悔

 3 years ago
source link: https://www.daqianduan.com/19161.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

6zaQJbV.png!mobile

新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。

如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:

https://oauthlogin.net/

前言

此次带来得这个小项目是 OAuth2 登录组件,看到 Java 平台 JustAuth 项目很方便的接入第三方平台登录,心里痒痒啊,搜了一大圈,发现我大 .netcore 能用的可说是少之又少,而且代码写得一塌糊涂,全在一个库里,代码风格也看不惯,所以下定决定,操起键盘,开干。

关于 OAuth2 的一些基础、原理介绍文章太多了,写的好的不在少数,在页尾我提供了几个链接,喜欢的朋友看一下,这里就不深入解释,直入主题。

如何使用

这里拿接入 github 登录做演示,新建 Asp.NET Core Web应用程序 项目,名叫 GithubLogin (PS:你可以自己起个更牛×的名字),选择模型视图控制器这个,当然你可以选择其他的。

第一步:安装

安装这个 nuget 包:

Install-Package MrHuo.OAuth.Github -Version 1.0.0

第二步:配置

打开 appsettings.json 写入下面的配置:

{
  "oauth": {
    "github": {
      "app_id": "github_app_id",
      "app_key": "github_app_key",
      "redirect_uri": "https://oauthlogin.net/oauth/githubcallback",
      "scope": "repo"
    }
  }
}

这里的配置可以通过 https://github.com/settings/applications/new 来注册, redirect_uri 可以填写本地 localhost 地址的,超级方便,这也是为什么使用 github 登录做演示的原因。

meaeYzf.png!mobile

创建完成后,在这个界面里生成 client secret

qAzymq.png!mobile

输入密码,生成成功后是这样的:

MzIRnef.png!mobile

把界面里的 Client IDClient secret ,连同上一个界面里填写的 Authorization callback URL 全部填写到配置文件对应位置。现在配置文件 appsettings.json 是这样的:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "oauth": {
    "github": {
      "app_id": "c95fxxxxxx0d09",
      "app_key": "c6a73xxxxxx6375",
      "redirect_uri": "http://localhost:5000/oauth/githubcallback",
      "scope": "repo"
    }
  }
}

下面的 scope 暂且不管他,你想深入了解它的作用的话,后面再说。

第三步:写代码

Startup.cs 文件中注入组件:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddSingleton(new GithubOAuth(OAuthConfig.LoadFrom(Configuration, "oauth:github")));
}

文件中其他代码没有修改,只加了这一行而已。

新建一个 OAuthController 类,代码如下:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MrHuo.OAuth.Github;
namespace GithubLogin.Controllers
{
    public class OAuthController: Controller
    {
        [HttpGet("oauth/github")]
        public IActionResult Github([FromServices] GithubOAuth githubOAuth)
        {
            return Redirect(githubOAuth.GetAuthorizeUrl());
        }
        [HttpGet("oauth/githubcallback")]
        public async Task<IActionResult> GithubCallback(
            [FromServices] GithubOAuth githubOAuth,
            [FromQuery] string code)
        {
            return Json(await githubOAuth.AuthorizeCallback(code));
        }
    }
}

你没看错,就这点代码就好了。我们来运行一下试试:

zYZVRjU.png!mobile

项目运行之后,在地址栏里输入下面这个地址: http://localhost:5000/oauth/github ,因为我们没有修改任何代码,没有在视图上做任何链接,所以就劳烦手动啦~~

回车之后,顺利跳转到 github 授权:

3mI7N3F.png!mobile

点击绿色的 Authorize 按钮之后稍等片刻,你会看到下面这个结果:

JJz22ym.png!mobile

顺利拿到了用户信息(PS:请忽略我少的可怜的粉丝,曾经我不强求 –ToT)

yimMzy.png!mobile

好了,到这里我的表演结束了,可以看到接入流程非常流程,卡人主要是在申请这些步骤。下面讲讲原理之类的,随便说一些…如果觉得我啰嗦,那么就不用往下看了,因为下面我会更啰嗦。

当然,除了 github 现在已经接入了12个平台,其中 QQ 和抖音我没有注册到应用,无法测试,所以暂时没有 nuget 包,一个人的力量总是有限的,在这里我请求各位有闲时间或者有 appid 资源的大佬,为这个小项目做一些贡献,是她走的远一些。

6fIZnq6.png!mobile

更多的 nuget 包,进这里 https://www.nuget.org/profiles/mrhuo 或者在 VS nuget 包管理器里搜索 MrHuo.OAuth ,就可以了。

请忽略 nuget 上其他几个 垃圾 包,那是很多年很多年以前写的,舍不得删。

开发背景

第三方平台登录说白了就是实现 OAuth2 协议,很多平台比如支付宝、百度、github、微软,甚至是抖音、快手很多平台都提供了开放接口。但是,很多平台会在这个标准协议的基础上增加、修改一些东西,比如:标准协议里,获取 authorize code 时应提供 client_id ,微信公众平台非要把它改成 appid 。再比如:获取用户信息时,只需要 access_token 参数,微信公众平台这边非要提供一个 openid ,当然这是在所难免的,因为各个平台实际业务还是千差万别,无法做到完全的统一,那这就给我们开发者带来一个困扰,开发第三方登录时很困难,当然,开发一两个也无所谓,要是多了呢?

假如有这么一个产品经理,他想接入很多的登录方式,让使用者无论使用哪种平台,都能在这里顺利登录,找到回家的路呢(PS:产品经理你别跑,看我40米的大刀)。

R7JN7bI.png!mobile

无疑,给我们一个考验,如何做到一个标准化,可配置,可扩展呢?这就是一个需要深究的问题。下面我就说说我肝这个项目的一些想法,说的不好别喷我,我还年轻(PS:三十多岁老大叔别装嫩),还要脸……

jeyiIrF.png!mobile

制定标准

看了很多文档之后,我们会发现,万变不离其宗,总有规律可循,总的来说,有下面3个步骤:

  1. GetAuthorizeUrl

这一步通过 client_idredirect_uri 等几个参数来获取授权 url ,跳转到这个 url 之后将在第三方平台上完成登录,完成登录之后会跳转到上面提供的 redirect_uri 这个地址,并且带上一个 code 参数。

  1. GetAccessToken

这一步里,拿到上面的 code 之后去第三方平台换 access_token

  1. GetUserInfo

这一步并非必须,但是我们既然是做第三方登录,登录之后还是需要和自己平台的一些业务绑定用户账号,或者使用现有信息注册一个用户,这个方法就显得尤为重要了。

到此,就这3个步骤,我觉得时需要制定在标准里面的,所以我就写了下面这个接口来规范它:

/// <summary>
/// OAuth 登录 API 接口规范
/// </summary>
public interface IOAuthLoginApi<TAccessTokenModel, TUserInfoModel>
    where TAccessTokenModel : IAccessTokenModel
    where TUserInfoModel : IUserInfoModel
{
    /// <summary>
    /// 获取跳转授权的 URL
    /// </summary>
    /// <param name="state"></param>
    /// <returns></returns>
    string GetAuthorizeUrl(string state = "");

    /// <summary>
    /// 异步获取 AccessToken
    /// </summary>
    /// <param name="code"></param>
    /// <param name="state"></param>
    /// <returns></returns>
    Task<TAccessTokenModel> GetAccessTokenAsync(string code, string state = "");

    /// <summary>
    /// 异步获取用户详细信息
    /// </summary>
    /// <param name="accessTokenModel"></param>
    /// <returns></returns>
    Task<TUserInfoModel> GetUserInfoAsync(TAccessTokenModel accessTokenModel);
}

可以看到我将 AccessTokenUserInfo 做成了泛型参数,因为他们是这个规范里的可变部分。代码中 state 参数的作用呢就是为了防止 CORS 攻击做的防伪验证,这里暂不做解释,其他文档里都有这个参数的解释。

ZvQVzaQ.png!mobile

如何扩展新的平台

这里拿 Gitee 来做演示:

第一步:找平台对应 OAuth 文档,找到获取用户信息接口返回JSON,转换为 C# 实体类。如下:

根据自己需要和接口标准,扩展用户属性

public class GiteeUserModel : IUserInfoModel
{
    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("avatar_url")]
    public string Avatar { get; set; }

    [JsonPropertyName("message")]
    public string ErrorMessage { get; set; }

    [JsonPropertyName("email")]
    public string Email { get; set; }

    [JsonPropertyName("blog")]
    public string Blog { get; set; }

    //...其他属性类似如上
}

这里使用了 .netcore 内置的 Json 序列化库,据说性能提高了不少!

第二步:写对应平台的授权接口

/// <summary>
/// https://gitee.com/api/v5/oauth_doc#/
/// </summary>
public class GiteeOAuth : OAuthLoginBase<GiteeUserModel>
{
    public GiteeOAuth(OAuthConfig oauthConfig) : base(oauthConfig) { }
    protected override string AuthorizeUrl => "https://gitee.com/oauth/authorize";
    protected override string AccessTokenUrl => "https://gitee.com/oauth/token";
    protected override string UserInfoUrl => "https://gitee.com/api/v5/user";
}

加上注释,总共十行,如你所见,非常方便。如果该平台协议遵循 OAuth2 标准开发,那么就这么几行就好了。

当然,如果不按规矩自定义字段的平台,也可以扩展,比如微信公众平台。

WechatAccessTokenModel.cs AccessToken 类扩展

namespace MrHuo.OAuth.Wechat
{
    public class WechatAccessTokenModel : DefaultAccessTokenModel
    {
        [JsonPropertyName("openid")]
        public string OpenId { get; set; }
    }
}

继承自 DefaultAccessTokenModel ,新增字段 OpenId ,因为获取用户信息需要获取 OpenId ,所以这里需要它。

WechatUserInfoModel.cs 用户信息类

using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace MrHuo.OAuth.Wechat
{
    public class WechatUserInfoModel : IUserInfoModel
    {
        [JsonPropertyName("nickname")]
        public string Name { get; set; }

        [JsonPropertyName("headimgurl")]
        public string Avatar { get; set; }

        [JsonPropertyName("language")]
        public string Language { get; set; }

        [JsonPropertyName("openid")]
        public string Openid { get; set; }

        [JsonPropertyName("sex")]
        public int Sex { get; set; }

        [JsonPropertyName("province")]
        public string Province { get; set; }

        [JsonPropertyName("city")]
        public string City { get; set; }

        [JsonPropertyName("country")]
        public string Country { get; set; }

        /// <summary>
        /// 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
        /// </summary>
        [JsonPropertyName("privilege")]
        public List<string> Privilege { get; set; }

        [JsonPropertyName("unionid")]
        public string UnionId { get; set; }

        [JsonPropertyName("errmsg")]
        public string ErrorMessage { get; set; }
    }
}

这里用户信息字段上边的 [JsonPropertyName("xxxx")] 完全按照文档里的字段写,否则获取不到正确的值。如果不需要太多的字段,自行删减。

WechatOAuth.cs 核心类

using System.Collections.Generic;
namespace MrHuo.OAuth.Wechat
{
    /// <summary>
    /// Wechat OAuth 相关文档参考:
    /// <para>https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html</para>
    /// </summary>
    public class WechatOAuth : OAuthLoginBase<WechatAccessTokenModel, WechatUserInfoModel>
    {
        public WechatOAuth(OAuthConfig oauthConfig) : base(oauthConfig) { }
        protected override string AuthorizeUrl => "https://open.weixin.qq.com/connect/oauth2/authorize";
        protected override string AccessTokenUrl => "https://api.weixin.qq.com/sns/oauth2/access_token";
        protected override string UserInfoUrl => "https://api.weixin.qq.com/sns/userinfo";
        protected override Dictionary<string, string> BuildAuthorizeParams(string state)
        {
            return new Dictionary<string, string>()
            {
                ["response_type"] = "code",
                ["appid"] = oauthConfig.AppId,
                ["redirect_uri"] = System.Web.HttpUtility.UrlEncode(oauthConfig.RedirectUri),
                ["scope"] = oauthConfig.Scope,
                ["state"] = state
            };
        }
        public override string GetAuthorizeUrl(string state = "")
        {
            return $"{base.GetAuthorizeUrl(state)}#wechat_redirect";
        }
        protected override Dictionary<string, string> BuildGetAccessTokenParams(Dictionary<string, string> authorizeCallbackParams)
        {
            return new Dictionary<string, string>()
            {
                ["grant_type"] = "authorization_code",
                ["appid"] = $"{oauthConfig.AppId}",
                ["secret"] = $"{oauthConfig.AppKey}",
                ["code"] = $"{authorizeCallbackParams["code"]}"
            };
        }
        protected override Dictionary<string, string> BuildGetUserInfoParams(WechatAccessTokenModel accessTokenModel)
        {
            return new Dictionary<string, string>()
            {
                ["access_token"] = accessTokenModel.AccessToken,
                ["openid"] = accessTokenModel.OpenId,
                ["lang"] = "zh_CN",
            };
        }
    }
}

乍一看好多内容,懵了?先别懵,我一个一个来说一下:

protected override Dictionary<string, string> BuildAuthorizeParams(string state)
{
    return new Dictionary<string, string>()
    {
        ["response_type"] = "code",
        ["appid"] = oauthConfig.AppId,
        ["redirect_uri"] = System.Web.HttpUtility.UrlEncode(oauthConfig.RedirectUri),
        ["scope"] = oauthConfig.Scope,
        ["state"] = state
    };
}

细心的读者发现了,这一段就是为了构造 Authorize Url 时后边的参数列表,返回一个 Dictionary<string, string> 即可,以为微信公众号把 client_id 字段修改为 appid ,所以这里需要处理一下。

public override string GetAuthorizeUrl(string state = "")
{
    return $"{base.GetAuthorizeUrl(state)}#wechat_redirect";
}

这一段,在 Authorize Url 后边缀了个 #wechat_redirect ,虽然不知道微信在这个参数上做了什么文章(PS:知道的朋友,言传一下~~),但是他文档里写就给他写上吧。

protected override Dictionary<string, string> BuildGetAccessTokenParams(Dictionary<string, string> authorizeCallbackParams)
{
    return new Dictionary<string, string>()
    {
        ["grant_type"] = "authorization_code",
        ["appid"] = $"{oauthConfig.AppId}",
        ["secret"] = $"{oauthConfig.AppKey}",
        ["code"] = $"{authorizeCallbackParams["code"]}"
    };
}

同理,这一段是为了构造 GetAccessToken 接口参数。

protected override Dictionary<string, string> BuildGetUserInfoParams(WechatAccessTokenModel accessTokenModel)
{
    return new Dictionary<string, string>()
    {
        ["access_token"] = accessTokenModel.AccessToken,
        ["openid"] = accessTokenModel.OpenId,
        ["lang"] = "zh_CN",
    };
}

同理,这一段是为了构造 GetUserInfo 接口参数。

可以看到哈,这个框架本者自由、开放的原则,任何能自定义的地方,都可以自定义。还有我原本的出发点,并非只针对 OAuth 登录这一个方向,我想把他平台里面提供的 API 全部接入进来,因为扩展太容易了,但是吧,时间精力有限,再说人上了年纪,过了30岁,脑袋就不怎么灵光了,所以机会留给年轻人。

加入贡献

期待 更多的朋友能加入到这个项目中,贡献代码也好,贡献 appid 资源做测试也好,提供意见建议也好。如果你也感兴趣,请联系我。

如果觉得有用帮到你了,贡献幼儿园之星 :star:,点个关注, fork 走一波~~(PS: 手动调皮)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK