65.9K
CodeProject 正在变化。 阅读更多。
Home

GitHub 分析:Oauth Killer 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (24投票s)

2017 年 3 月 22 日

GPL3

7分钟阅读

viewsIcon

27672

downloadIcon

540

一个简单的 POC,演示如何使用 GitHub API 和 Oauth 创建实际应用程序

引言

本文将介绍如何实现一个使用 OAuth 协议与外部系统集成的非常简单的应用程序。这是一个展示,说明开发者如何在几个快速步骤中创建有用的东西是多么容易,以及公司如何开放某些服务以促生此类应用程序是多么方便。如果您想跳过所有文章,直接查看最终结果,可以尝试 GitHub 分析应用程序。

所有代码均可在 GitHub 上找到。

背景

通过这个项目,我想做一些具体的东西,最终为社区提供一些有用的东西。因此,我从市场尚未满足的需求出发。当然,我是一名开发者,我的经验帮助我发现了 GitHub 的一个小小不足。当我发布我的开源项目 wlog 时,我可以看到有多少人下载了我的应用程序,但 GitHub UI 中没有这样的功能。我说的不是克隆或源代码下载的数量,而是简单的发布(releases)数量。搜索后,我发现了很多工具,很多人说我们只需要调用 API。嗯,这将是这个应用程序的主要功能。我还想免费与他人分享,所以我需要一个免费的托管系统来实现它。此外,我想尽快完成项目,将其推向市场并获得一些反馈。

目标

因此,总结一下,我的目标是

  1. 创建一个简单但功能齐全且准备好使用的应用程序
  2. 集成尽可能多的外部系统,以最大化已完成的工作
  3. 解决具体问题(显示 GitHub 上的资产下载)
  4. 演示将应用程序与 OAuth 系统集成是多么容易

所需材料

 解决方案注意
平台.NET 4.5 ASP.NET MVC 
托管AppHarbor免费 ASP.NET 托管,支持 CI
GitHub APIOctokit完成所有艰苦的工作!
绘制徽章Shields.io一个很棒的项目,可以生成 SVG 徽章

OAuth 协议

OAuth 2 协议是一个授权框架,被几乎所有需要向用户提供对其资源有限访问权限的系统所采用。这通常通过 HTTP 进行,并在 API 或集成场景中得到广泛应用。主要原则是将用户信任和第三方访问授权委托给授权服务器。
OAuth 协议非常易于理解,我认为如今大多数开发者都听说过它,所以我只会用几句话来介绍它,提醒一下新手。

基本上,我们需要实现五个简单的步骤。

OAuth 步骤 1:身份验证

当用户尝试进行身份验证时,会向认证服务器发送一个请求。这可以通过简单地构建一个 URL 并重定向到它来完成。这是本项目中的代码片段。

    public ActionResult Login()
        {
            var client = AppConfig.Current.GetClient();
            // NOTE: this is not required, but highly recommended!
            // ask the ASP.NET Membership provider to generate a random value 
            // and store it in the current user's session
            string csrf = Membership.GeneratePassword(24, 1);
            Session["CSRF:State"] = csrf;

            var request = new OauthLoginRequest(AppConfig.Current.ClientId)
            {
                Scopes = { "user", "notifications" },
                State = csrf
            };

            // NOTE: user must be navigated to this URL
            var oauthLoginUrl = client.Oauth.GetGitHubLoginUrl(request);

            return Redirect(oauthLoginUrl.ToString());
        }

OAuth 步骤 2:用户授权

授权服务器将提示用户授予外部应用程序权限。此步骤完全由用户和授权服务器完成,因此我们只需等待此操作完成:之后,我们将通过重定向收到通知。

OAuth 步骤 2:代码

在用户批准授权请求后,我们将获得 code 参数,该参数可用于后续调用以获取实际的 token。Code 参数通常是一次性的,并且在很短的时间内会过期。我们还可以向步骤 1 中构建的重定向 URL 添加一个 state 参数。这可能很有用,因为它由服务器返回,可以用来验证请求来源。

OAuth 步骤 3:Token

关于身份验证的最后一个调用是(最终)获取 token。在此示例中,根据 octokit 文档,它存储在 session 中。

此代码片段涵盖了步骤 2 和 3。

    public ActionResult Authorize(string code, string state)
        {
                var client = AppConfig.Current.GetClient();

                if (String.IsNullOrEmpty(code))
                    return RedirectToAction("Index");

                var expectedState = Session["CSRF:State"] as string;
                if (state != expectedState) 
                throw new InvalidOperationException("SECURITY CHECK FAIL!");

                Session["CSRF:State"] = null;

                var request = new OauthTokenRequest(AppConfig.Current.ClientId
                                                   , AppConfig.Current.ClientSecret
                                                   , code);
                var token = client.Oauth.CreateAccessToken(request).Result;
                Session["OAuthToken"] = token.AccessToken;

               return new RedirectResult("~/Stats/");
        }

GitHub API 集成

GitHub 集成主要通过 octokit.net 库完成。这是一个很棒的框架,文档齐全,您可以在官方网站上找到很多内容。此应用程序中的所有 OAuth 流都使用了该库提供的功能,只需遵循教程即可。客户端模型封装了所有服务,并且非常容易使用 GitHub API 开发应用程序。所以,我不想浪费更多时间来谈论它有多酷以及它有多容易使用:只需一个代码片段来说明如何访问 GitHub 信息。

        public ActionResult Index()
        {
            var accessToken = Session["OAuthToken"] as string;
            if (accessToken != null)
            {
                var phv=new ProductHeaderValue(AppConfig.Current.AppName);
                GitHubClient client = new GitHubClient(phv);
                client.Credentials = new Credentials(accessToken);
               
                var repositories = client.Repository.GetAllForCurrent().Result;
                var user = client.User.Current().Result;                

                var totalDownload = 0;
                var totalStars = 0;
                var repoDownload = 0;
                var releasesCount = 0;
                
                //Iterate over all repos to integrate info with stats
                List<repostats> repoList = new List<repostats>();
                foreach (var currentRepo in repositories)
                {
                    repoDownload = 0;
                    releasesCount = 0;
                  
                    var relase = client.Repository.Release.GetAll(currentRepo.Id).Result;
                    if (relase.Count > 0)
                    {
                        for (int i = 0; i < relase.Count; i++)
                        {
                            for (int k = 0; k < relase[i].Assets.Count; k++)
                            {
                                var currentCount= relase[i].Assets[k].DownloadCount;
                                totalDownload += currentCount;
                                repoDownload  += currentCount;
                                releasesCount++;
                            }
                        }                      
                    }                    

                    repoList.Add(new RepoStats()
                    {
                        Repo = currentRepo,
                        TotalDownload = repoDownload,
                        ReleasesCount= releasesCount
                    });
                }
                
                 // Fill the model with data computed
                return View(new UserStats()
                {
                    Repositories = repoList,
                    User= user,
                    TotalDownload= totalDownload,
                    TotalReleases=repoModel.Sum(x=>x.ReleasesCount),
                    TotalStars=repoModel.Sum(x=>x.Repo.StargazersCount),
                    DiskUsage= repoModel.Sum(x=>x.Repo.Size)
                } );               
            }
            return new RedirectResult("~/");
        }

Shields.IO

Shields.io 是一个有趣的工具,可以将 URL 转换为类似这样的徽章 Shields.io 徽章示例。酷之处在于每个图像都以矢量方式生成,因此在服务器端(抱歉说得如此简单),所需要做的就是生成文本。这样,实时渲染图像以生成它们的负载就不再需要了,并且减少到 XML 操作。在客户端,就像这个例子一样,这使得事情变得简单得多,因为只需组合一个 URL,徽章就会自动生成。这里有一段代码可以生成一个带有每个存储库下载资产数量的徽章。

const string badgeTemplate = "https://img.shields.io/badge/{0}-{1}-{2}.svg";

/// <summary>
/// Exposet method that computes count and produce images using shield.io service
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ActionResult RepositoryDownloads( long id=0)
{
    if (id == 0) throw new Exception("Repository Id Missing");            
    int total=  GetDownloadCountForRepository(id);
    string url = string.Format(badgeTemplate, "downloads", total, "orange");
    return DownloadFile(url, "repositoryDownload.svg", true);
}

/// <summary>
/// Compute download count for a given repository id
/// Note: all assets of all versions are summed together
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private int GetDownloadCountForRepository(long id)
{
    int total = 0;
    GitHubClient client = GetClientForRequest(this);
    var repository = client.Repository.Get(id).Result;
    foreach (var rel in client.Repository.Release.GetAll(id).Result)
    {
        foreach (var asset in rel.Assets)
        {
            total += asset.DownloadCount;
        }
    }
    return total;
}

/// <summary>
/// Generic methods that download  and serves files
/// </summary>
/// <param name="url"> url of file to be downloaded</param>
/// <param name="filename">name of file to be served</param>
/// <param name="inline">show inline or download as attachment</param>
/// <returns></returns>
private ActionResult DownloadFile(string url, string filename, bool inline)
{
    WebClient client = new WebClient();
    var bytes = client.DownloadData(url);
    string contentType = MimeMapping.GetMimeMapping(filename);
    var cd = new System.Net.Mime.ContentDisposition
    {
        FileName = filename,
        Inline = inline,
    };

    Response.AppendHeader("Content-Disposition", cd.ToString());
    return File(bytes, contentType);
} 

AppHarbor

最后,我需要找到一个托管解决方案。此外,我希望找到一个能够帮助我进行部署并能够进行一些持续集成的解决方案。为此,我选择了 AppHarbor 的免费版本,它可以轻松配置为连接到 GitHub 代码并在预定义分支的每次提交时进行部署。这样,我就可以管理所有发布部分。AppHarbor 有很多限制(例如,可写磁盘但会定期清除,如果应用程序不活动则关闭,…),但在我的情况下,当我只需要运行代码时,这是一个很好的解决方案,尤其因为它允许我免费开始。

关于托管部分,一些有趣的点

  • 持续部署:GitHub 仓库已连接到 AppHarbor,因此在生产分支上的所有提交都会将新版本部署到服务器。在我过去参与的开源项目中,我更倾向于将持续集成与持续部署分开,我使用了 AppVeyor,这是一个很棒的工具。在这种情况下,为了简化,我只使用 AppHarbor 来管理部署部分。我发现对于一个简单的应用程序来说,这可能是一个不错的选择,可以随着应用程序的发展而改变。
  • 变量覆盖:因为这个应用程序包含一些敏感数据,比如 API 的客户端密钥(如果你问的话,仓库里的那个是假的…),我使用了 AppArbor 的变量覆盖功能。这意味着应用程序密钥在构件生成后被覆盖,因此正在运行的应用程序将使用生产设置。
  • 定价:能够免费获得这样的东西非常酷,所以对于开发和进行实验来说,这是一个非常有趣的选择。没有成本和设置时间,我花了半个小时就轻松设置好了一切,所以我对此有非常积极的体验。定价怎么样?抱歉,您可以查看定价页面并自行评估。进行价格比较不是本文的重点。

“GitHub 分析”应用程序

经过这长篇介绍,关于这个应用程序还有什么要说的呢?在解释了单个组件如何工作之后,我只需要一些胶水将它们粘合在一起。我所做的就是找到一个漂亮的 HTML 模板来创建主页,放置一个按钮来执行登录操作,在用户通过身份验证后将其重定向到私有区域,并使用 API 创建一个简单的仪表板。下面是应用程序工作的一个示例。如果您想自己尝试,只需点击此链接下载代码。

结论

如今,我们使用着成百上千个应用程序,我认为将来,越来越多的公司将采用这种模式来鼓励第三方应用程序。OAuth2 是在系统之间共享身份验证的最常见方式,我认为每个开放系统都应该拥抱它(…统计数据证实了我的观点)。

在这种情况下,我想展示如何通过简单地集成一个已有的工作组件,就能轻松创建一个独立的解决方案。当然,正如罗马不是一天建成的,应用程序也不是。如果你仔细看,除了漂亮的图形,你会看到一长串问题(性能、API 速率限制、糟糕的代码、缺失的功能…)。但现在不是看这些细节的时候。这只是一个 POC(概念验证),其主要目的是验证一个想法,也许能学到一些有用的东西。

集成对于优化上市时间至关重要,并利用已完成/正在工作的项目来保证性能。

历史

  • 2017-04-08:添加了代码片段
  • 2017-03-23:首个版本
© . All rights reserved.