天猫精灵对接2(OAuth 搭建) ASP.NET WebApi OWIN 实现 OAuth 2.0 ASP.NET WebApi OWIN 实现 OAuth 2.0

根据 接入方式及流程 中的说明,可知,搭建过程中,我们需要自己整一个 OAuth 的授权平台,具体说明可以参考蟋蟀大哥的文章  ASP.NET WebApi OWIN 实现 OAuth 2.0 ,我的实际代码也是基于文章给出的源码修改的。

首先认真研究一次文档:

(1)AliGenie在开发商开放平台或者其他第三方平台注册一个应用,获取到相应的Client idClient secret

(2)AliGenie 应用向开发商OAuth2.0服务发起一个授权请求

(3)开发商OAuth2.0服务向用户展示一个授权页面,用户可进行登陆授权

(4)用户授权AliGenie客户端应用后,进行回跳到AliGenie 的回调地址上并带上code相关参数

(5)AilGenie回调地址上根据code会去合作方Oauth 的服务上换取 access_token

(6)通过access_token,天猫精灵设备控制时通过该access_token进行访问合作方的服务

关键字已经用颜色标明,也就是说,我们需要一个授权平台,授权页,code 换取 access_token 的功能,分配给 天猫的 clien_id,client_secret.

最主要的还是平台,具体的平台怎么搭建就不说了,参考一下上面给出的链接,照着做或者改就行了。

因为我们对接天猫精灵主要用的是授权码模式,所以我们主看授权码的方式,授权码模式 的流程请查看 ASP.NET WebApi OWIN 实现 OAuth 2.0 

这儿顺便说一下,如果你用的是 .net 4.5 的情况下,安装 OWIN ,用NuGet安装时会报错,提示不兼容对应的框架,所以用控制台安装旧版的,

  • Owin 对应:  Install-Package Microsoft.Owin -Version 3.1.0
  • Microsoft.Owin.Host.SystemWeb 对应:Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.1.0
  • Microsoft.Owin.Security.OAuth 对应:Install-Package Microsoft.Owin.Security.OAuth -Version 3.1.0
  • Microsoft.Owin.Security.Cookies 对应:Install-Package Microsoft.Owin.Security.Cookies -Version 3.1.0
  • Microsoft.AspNet.Identity.Owin 对应:Install-Package Microsoft.AspNet.Identity.Owin -Version 2.1.0

我们直接以对接天猫精灵的流程来简单说一下代码应该怎么改吧。

根据官方文档提示,我们要给他天猫精灵配置一个 client_id ,client_secrete,检查源码知道,它已经默认写死了,client_id=xishuai,client_secrete=123  .我们先记下,等会再配置。

按着天猫精灵的流程,我们还需要一个 适配手机端访问的授权的 H5 页,那我们添加一个控制器(Home),加个 Index 方法用来返回视图,再加一个 Index2 准备用来接收它提交过来的数据,再给它加个视图,名字就定为 Index ,在里面放上几个 input 标签,准备用来存放天猫传过来的参数的(后续再隐藏起来),样式就先不处理了,晚点再说。

控制器:

        public ActionResult Index()
        {
            return View();
        }


        [HttpPost]
        public ActionResult Index2()
        {
            return View();
        }

视图:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div> 
        <form id="form1" method="post" action="@Url.Action("Index2")">
            <input type="text" name="clientId" value="xishuai" />
            <br />
            <input type="text" name="redirect_uri" value="" />
            <br />
            
            <input type="text" name="response_type" value="" />
            <br />
            <input type="text" name="state" value="" />
            <br />
            <input type="submit" value="提交" />
        </form>
        
    </div>
</body>
</html>

看一下 官方文档-接入方式及流程 ,在第5点得知,请求的链接如下:

原文档链接:https://xxx.com/auth/authorize?redirect_uri=https%3A%2F%2Fopen.bot.tmall.com%2Foauth%2Fcallback%3FskillId%3D11111111%26token%3DXXXXXXXXXX&client_id=XXXXXXXXX&response_type=code&state=111
对应到我们这儿的链接将会是:
https://xxx.com/Home/Index?redirect_uri=https%3A%2F%2Fopen.bot.tmall.com%2Foauth%2Fcallback%3FskillId%3D11111111%26token%3DXXXXXXXXXX&client_id=XXXXXXXXX&response_type=code&state=111

redirect_uri: 处理完成的返回地址,这个我们不应该去动它.
client_id: 在合作方上注册的应用Id(也就是我们刚才说到的准备给他配置的那个 client_id 值:xishuai )

state: 在我代码中,我是直接无视了这个参数的。项目目前没有用到它。

既然知道有这么多参数,那我们整理一下,在控制器接收一下,返回到视图界面对应的地方吧.代码如下:

Home 控制器中的 Index 方法:

        public ActionResult Index()
        {
            string clientId = Request.QueryString["client_id"] + "";
            string redirect_uri = Request.QueryString["redirect_uri"] + "";
            string response_type = Request.QueryString["response_type"] + "";
            string state = Request.QueryString["state"] + "";
           
            ViewBag.clientId = clientId;
            ViewBag.redirect_uri = redirect_uri;
            ViewBag.response_type = response_type;
            ViewBag.state = state;

            return View();
        }

Index 视图:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div> 
        <form id="form1" method="post" action="@Url.Action("Index2")">
            <input type="text" name="clientId" value="@ViewBag.clientId" />
            <br />
            <input type="text" name="redirect_uri" value="@ViewBag.redirect_uri" />
            <br />
            <input type="text" name="response_type" value="@ViewBag.response_type" />
            <br />
            <input type="text" name="state" value="@ViewBag.state" />
            <br />
            <input type="submit" value="提交" />
        </form>
        
    </div>
</body>
</html>

我们看一下测试用例的代码,其中 OAuth_AuthorizationCode_Test 测试用例对应着授权码模式,也就是说,我们需要看下它原本是怎么请求的,依葫芦画瓢,简单过一篇代码之后,发现 GetAuthorizationCode() 这个方法很可疑,跟进去,可以看到,它确实是发起了请求授权服务。我们简单分析一下,它利用 HttpClient 发起了一个 /authorize 请求,传递了grant_type,response_type,client_id,redirect_uri 这几个参数,也就是说,我们也可以模仿他这样操作,那么在  Index2 方法中这样写:

        public async Task<ActionResult> Index2()
        {
            string clientId = Request.Form["clientId"] + "";
            string redirect_uri = Request.Form["redirect_uri"] + "";
            string state = Request.Form["state"] + "";
            string urlEncode = HttpUtility.UrlEncode(redirect_uri);
            return Redirect("~/authorize?grant_type=authorization_code&response_type=code&client_id="+clientId+"&redirect_uri="+urlEncode);
        }

因为我们是在同一个服务器,所以我们不再用 HttpClient 去发起请求了,直接上跳转。

直接更新上服务器,并上 天猫的控制台 填写 信息,

账户授权链接填入: https://YourWebSite/Home/Index

ClientID填入:xishuai

Client Secret 填入:123

Access Token URL 填入:https://YourWebSite/NoAddress (随便写一个,只是为了先测试第一步是不是正常的先)

厂商登出 URL 留空。

更新上服务器,进入 天猫控制台 中的测试验证,然后点击 账户授权 ,会发现报错!!!提示缺少参数。那我们检查一下代码吧,看下是哪里出的问题,缺少参数,也就是说,我们返回给天猫的URL,参数写少了,或者写错了。我们检查一下。

打开 OpenAuthorizationServerProvider 找到 AuthorizeEndpoint 方法(它是对应着 authorize 请求的处理),可以看到里面有个 redirectUri 的数据,用日志的方式输出来看下(其实不用看了,刚才我们在测试的时候,你直接把 <input type="text" name="redirect_uri" /> 的值复制出来就知道问题出在哪了),redirectUri 在此处的值为:https://open.bot.tmall.com/oauth/callback?skillId=11111111&token=XXXXXXXXXX ,看到这儿,我想结合代码就知道问题出在哪了吧,源码如下:

context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));

修改如下:

        /// <summary>
        /// 生成 authorization_code(authorization code 授权方式)、生成 access_token (implicit 授权模式)
        /// </summary>
        public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
        {
            if (context.AuthorizeRequest.IsImplicitGrantType)
            {
                //implicit 授权方式
                var identity = new ClaimsIdentity("Bearer");
                context.OwinContext.Authentication.SignIn(identity);
                context.RequestCompleted();
            }
            else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
            {
                //authorization code 授权方式
                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);


                var ResUrl = redirectUri;
                if (redirectUri.Contains("?"))
                {
                    ResUrl += "&code=" + Uri.EscapeDataString(authorizeCodeContext.Token);
                }
                else
                {
                    ResUrl += "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token);
                }
                

                //writeLog("ResUrl", ResUrl,"TestToken");
               // writeLog("Token", authorizeCodeContext.Token, "TestToken");
                
                context.Response.Redirect(ResUrl);
                context.RequestCompleted();
            }
        }

这样,就能正常的处理跳转的参数了。更新上服务器,测试,通过!没有提示缺少参数了。但现在提示取不到 access_token 了。也就是说我们到了处理 获取 access_token 这一步了。

获取 access_token 的处理:

我们回头继续看下那个测试用例(OAuth_AuthorizationCode_Test),

        [Fact]
        public async Task OAuth_AuthorizationCode_Test()
        {
            var authorizationCode = GetAuthorizationCode().Result; //获取 authorization_code

            var tokenResponse = GetToken("authorization_code", null, null, null, authorizationCode).Result; //根据 authorization_code 获取 access_token
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

            var response = await _httpClient.GetAsync($"/api/values");
            if (response.StatusCode != HttpStatusCode.OK)
            {
                Console.WriteLine(response.StatusCode);
                Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
            }
            Console.WriteLine(await response.Content.ReadAsStringAsync());
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);

            Thread.Sleep(10000);

            var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result; //根据 refresh_token 获取 access_token
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
            var responseTwo = await _httpClient.GetAsync($"/api/values");
            Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
        }

在获取了 authorization_code 之后,调用了 GetToken 方法,查看 GetToken 方法

        private static async Task<TokenResponse> GetToken(string grantType, string refreshToken = null, string userName = null, string password = null, string authorizationCode = null)
        {//"authorization_code", null, null, null, authorizationCode
            var clientId = "xishuai";
            var clientSecret = "123";
            var parameters = new Dictionary<string, string>();
            parameters.Add("grant_type", grantType);

            if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
            {
                parameters.Add("username", userName);
                parameters.Add("password", password);
            }
            if (!string.IsNullOrEmpty(authorizationCode))
            {
                parameters.Add("code", authorizationCode);
                parameters.Add("redirect_uri", "http://localhost:8333/api/authorization_code"); //和获取 authorization_code 的 redirect_uri 必须一致,不然会报错
            }
            if (!string.IsNullOrEmpty(refreshToken))
            {
                parameters.Add("refresh_token", refreshToken);
            }

            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
                "Basic",
                Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));

            var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
            var responseValue = await response.Content.ReadAsStringAsync();
            if (response.StatusCode != HttpStatusCode.OK)
            {
                Console.WriteLine(response.StatusCode);
                Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
                return null;
            }
            return await response.Content.ReadAsAsync<TokenResponse>();
        }

构建了一个 HttpClient ,组好参数 post 给 /token  .那我们还是依葫芦画瓢,照抄过来用看下。在 Home 控制器下,定义一个 TestToken 方法,再看下官方文档

(B) 通过code换取合作方访问令牌
例:
https://XXXXX/token?grant_type=authorization_code&client_id=XXXXX&client_secret=XXXXXX&code=XXXXXXXX&redirect_uri=https%3A%2F%2Fopen.bot.tmall.com%2Foauth%2Fcallback
请求方法: POST

参数说明:
client_id: 在合厂商平台上注册的应用Id
grant_type: 授权类型 authorization_code
client_secret:在厂商平台上注册应用的secret
code: 授权登陆后回调AliGenie的地址返回的code
redirect_uri: AliGenie回调地址

也就是说,它会发这些参数给我们,那么我们就用 Request.Form 一一接收,然后模仿着GetToken 的方法写。

TestToken 方法如下:

        public async Task<ActionResult> TestToken()
        {
            string grant_type = Request.Form["grant_type"] + "";
            string client_id = Request.Form["client_id"] + "";
            string client_secret = Request.Form["client_secret"] + "";
            string code = Request.Form["code"] + "";
            string redirect_uri = Request.Form["redirect_uri"] + "";

            var parameters = new Dictionary<string, string>();
            parameters.Add("grant_type", grant_type);
            parameters.Add("code", code);
            parameters.Add("redirect_uri", redirect_uri);


            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
           
            HttpClient _httpClient = new HttpClient();
            _httpClient.BaseAddress = new Uri(System.Configuration.ConfigurationManager.AppSettings["serverUrl"]);//从配置文件中读取,服务器的域名
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
                "Basic",
                Convert.ToBase64String(Encoding.ASCII.GetBytes(client_id + ":" + client_secret)));

            var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
            var responseValue = await response.Content.ReadAsStringAsync();

       writeLog("responseValue",resp,"responseValue");

            if (response.StatusCode != HttpStatusCode.OK)
            {
                return Json(new TaskResponseError
                {
                    error = response.StatusCode + "",
                    error_description = (await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage
                });
            }
            var TokenResponse = await response.Content.ReadAsAsync<TokenResponseRight>();

            TokenResponse.expires_in = Convert.ToInt64(System.Configuration.ConfigurationManager.AppSettings["tokenExpiresTime"]);//过期时间也要返回
            writeLog("TokenResponse:", JsonConvert.SerializeObject(TokenResponse) + "", "TestToken");
            return Content(JsonConvert.SerializeObject(TokenResponse), "application/json");//强行指定类型,官方文档特别提示要用 application/json ,之前试过用return Json(xxx); 返回 之后提示参数错误之类的。
        }

OK,更新上服务器,再上 天猫控制台 ,把 Access Token URL 修改一下,修改为:https://YourWebSite/Home/TestToken ,测试跑起来。测试应该是通过的,如果还有问题可以问一下我,2018年6月1日11:47:55 昨天到现在 真机测试的功能 AliGenie 还没修复好,所以我也不好截图给你们看,回头再来补充吧。

refalsh Token 的功能

现在时间:2018年6月1日11:49:04 晚点再来完善内容吧。

未完待续。。。。

猜你喜欢

转载自www.cnblogs.com/Frank-Jan/p/9118105.html