大家好这里是皇鱼
在这篇文章中,我将教会各位如何进行微软登录
本文以C#为例,那么开始
事先声明:本文以微软最新的OAuth2.0授权代码流为参考,请先在MS Entra中注册一个应用程序,接下来按照Mojang的要求填写申请表,通过审核后方可进行本文操作,否则在请求Mojang api的时候无法正常返回


微软登录

获取授权码

首先在你的Azure应用程序中找到重定向URI这一项
重定向URI
点进去,添加一个本地链接,比如127.0.0.1:40935
接下来你需要让用户访问如下url并在本地搭建一个临时网站服务器,端口是上面设置的端口:

    https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
    ?client_id=
    &response_type=code
    &redirect_uri=http://127.0.0.1:40935
    &response_mode=query
    &scope=XboxLive.signin offline_access

此处client_id填写你Azure应用程序的client_id,redirect_uri填写上面添加的本地连接,用户在浏览器登录完成后会跳转到

    上面的回调链接/?code=xxx

你需要通过临时网站服务器提取这个code,这是授权码
C#代码示例:

    public static void stepOne()
    {
        Console.WriteLine("MSL step 1");
        string url = "http://127.0.0.1:40935/";
        string loginurl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=1cbfda79-fc84-47f9-8110-f924da9841ec&response_type=code&redirect_uri=http://127.0.0.1:40935&response_mode=query&scope=XboxLive.signin+offline_access";
        System.Diagnostics.Process.Start("explorer.exe", $"\"{loginurl}\"");

        HttpListener listener = new HttpListener();
        listener.Prefixes.Add(url);
        listener.Start();
        Console.WriteLine("Listening...");

        HttpListenerContext context = listener.GetContext();
        HttpListenerRequest request = context.Request;
        HttpListenerResponse response = context.Response;

        if (request.QueryString["code"] != null)
        {
            string code = request.QueryString["code"];
            string responseString = $"<html><body><center><h1>您已登录到Line Launcher,现在可以关闭此页面。</h1></center></body></html>";
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            response.ContentType = "text/html; charset=UTF-8";
            response.ContentEncoding = Encoding.UTF8;
            System.IO.Stream output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();
            listener.Stop();
            Console.WriteLine("Server stopped.");
            Console.WriteLine("MSL step 2");
            stepTwo(code);

        }
        else
        {
            string responseString = "<html><body><center><h1>登录失败,请重试!</h1><center></body></html>";
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            response.ContentType = "text/html; charset=UTF-8";
            response.ContentEncoding = Encoding.UTF8;
            System.IO.Stream output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();
        }
    }

授权码到授权令牌

首先你需要对https://login.microsoftonline.com/consumers/oauth2/v2.0/token发起post请求,如下是发送的数据:

    var params = new Dictionary<string, string>
    {
            { "client_id", "Azure client_id" },
            { "scope", "XboxLive.signin offline_access" },
            { "code", "上一步的code"},
            { "redirect_uri", "按照官方文档是必填,且需和上一步相同,但实际无作用"},
            { "grant_type", "authorization_code"}
    };

记得设置ContentTypeapplication/x-www-form-urlencodedAcceptapplication/json
如下是返回:

    {
       "token_type":"Bearer",
       "scope":"XboxLive.signin XboxLive.offline_access",
       "expires_in":3600,
       "ext_expires_in":3600,
       "access_token":"<令牌>",
       "refresh_token":"<刷新令牌>"
    }

你需要其中的access_token用于登录,refresh_token用于刷新access_token,因为access_token是有使用期限的
C#示例:

    public static void stepTwo(string code)
    {
        string url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
        var parameters = new Dictionary<string, string>
        {
            { "client_id", "" },
            { "scope", "XboxLive.signin offline_access" },
            { "code", code},
            { "redirect_uri", "http://127.0.0.1:40935"},
            { "grant_type", "authorization_code"}
        };
        string context = PostWithParameters(parameters, url, "application/json", "application/x-www-form-urlencoded");
        string accesstoken = GetValueFromJson(context, "access_token");
        string refreshtoken = GetValueFromJson(context, "refresh_token");
        Console.WriteLine($"MSL step 3");
        stepThree(accesstoken);
    }

刷新access_token

POST https://login.microsoftonline.com/consumers/oauth2/v2.0/token,参数:

        var params = new Dictionary<string, string>
        {
            { "client_id", "Azure应用程序的client_id"},
            { "scope", "XboxLive.signin offline_access" },
            { "refresh_token", 上一步的refresh_token},
            { "grant_type", "refresh_token"}
        };

返回和上一步相同

XBL身份验证

    POST https://login.microsoftonline.com/consumers/oauth2/v2.0/token

data:

    {
        "Properties": {
            "AuthMethod": "RPS",
            "SiteName": "user.auth.xboxlive.com",
            "RpsTicket": "d=token" // token=刚刚的accesstoken,bad request可以删掉d=
        },
        "RelyingParty": "http://auth.xboxlive.com",
        "TokenType": "JWT"
    }

记得ContentTypeAccept设置为application/json
返回:

   {
      "IssueInstant":"2020-12-07T19:52:08.4463796Z",
      "NotAfter":"2020-12-21T19:52:08.4463796Z",
      "Token":"token", // XBL令牌
      "DisplayClaims":{
         "xui":[
            {
               "uhs":"uhs" //用户哈希值
            }
         ]
      }
   }

你需要xbl令牌和用户哈希值(user hash uhs)以进行下一步

XSTS身份验证

   POST https://xsts.auth.xboxlive.com/xsts/authorize

data:

   {
       "Properties": {
           "SandboxId": "RETAIL",
           "UserTokens": [
               "xbl" // 上面的XBL令牌
           ]
       },
       "RelyingParty": "rp://api.minecraftservices.com/",
       "TokenType": "JWT"
   }

返回:

   {
      "IssueInstant":"2020-12-07T19:52:09.2345095Z",
      "NotAfter":"2020-12-08T11:52:09.2345095Z",
      "Token":"token", // xsts令牌
      "DisplayClaims":{
         "xui":[
            {
               "uhs":"" // 相同
            }
         ]
      }
   }

你需要xsts令牌以进行下一步的操作

MC访问令牌

   POST https://api.minecraftservices.com/authentication/login_with_xbox
   {
       "identityToken": "XBL3.0 x=<uhs>;<xsts_token>"
   }

返回:

   {
     "username" : "没用",
     "roles" : [ ],
     "access_token" : "mc access token", // mc令牌
     "token_type" : "Bearer",
     "expires_in" : 86400
   }

检查是否购买游戏

全是枯燥的HTTP请求,如下转自mc wiki(https://zh.minecraft.wiki/w/Tutorial:编写启动器):

那么现在让我们使用Minecraft的访问令牌来检查该账号是否包含产品许可。
GET https://api.minecraftservices.com/entitlements/mcstore
访问令牌在验证文件头中:Authorization: Bearer token。你需要保留Bearer,并在Bearer后添加上一步的访问令牌。

如果用户拥有游戏,那么响应会看起来像这样:

{
  "items" : [ {
    "name" : "product_minecraft",
    "signature" : "jwt sig"
  }, {
    "name" : "game_minecraft",
    "signature" : "jwt sig"
  } ],
  "signature" : "jwt sig",
  "keyId" : "1"
}
第一个jwts会包含值:
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "1"
}.{
  "signerId": "2535416586892404",
  "name": "product_minecraft"
}.[Signature]
最后一个jwt看起来是这样解码的:
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "1"
}.{
  "entitlements": [
    {
      "name": "product_minecraft"
    },
    {
      "name": "game_minecraft"
    }
  ],
  "signerId": "2535416586892404"
}.[Signature]
如果该账号没有拥有游戏,那么项目为空。
获取玩家 UUID
启动游戏还需要玩家的UUID,我们目前也没有获得。不过只要玩家拥有游戏,就一定有办法获取其UUID:
现在我们知道了该账号拥有游戏,那么可以获取他的档案来得到UUID:
GET https://api.minecraftservices.com/minecraft/profile
同样,访问令牌在验证文件头中:Authorization: Bearer token
如果账号拥有游戏,响应看起来会像这样:
{
  "id" : "986dec87b7ec47ff89ff033fdb95c4b5", // 账号的真实UUID
  "name" : "HowDoesAuthWork", // 该账号的Minecraft用户名
  "skins" : [ {
    "id" : "6a6e65e5-76dd-4c3c-a625-162924514568",
    "state" : "ACTIVE",
    "url" : "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b",
    "variant" : "CLASSIC",
    "alias" : "STEVE"
  } ],
  "capes" : [ ]
}
否则会看起来像这样:
{
  "path" : "/minecraft/profile",
  "errorType" : "NOT_FOUND",
  "error" : "NOT_FOUND",
  "errorMessage" : "The server has not found anything matching the request URI",
  "developerMessage" : "The server has not found anything matching the request URI"
}

完整C#示例代码:

    public static void stepOne()
    {
        Console.WriteLine("MSL step 1");
        string url = "http://127.0.0.1:40935/";
        string loginurl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=&response_type=code&redirect_uri=http://127.0.0.1:40935&response_mode=query&scope=XboxLive.signin+offline_access";
        System.Diagnostics.Process.Start("explorer.exe", $"\"{loginurl}\"");

        HttpListener listener = new HttpListener();
        listener.Prefixes.Add(url);
        listener.Start();
        Console.WriteLine("Listening...");

        HttpListenerContext context = listener.GetContext();
        HttpListenerRequest request = context.Request;
        HttpListenerResponse response = context.Response;

        if (request.QueryString["code"] != null)
        {
            string code = request.QueryString["code"];
            string responseString = $"<html><body><center><h1>您已登录,现在可以关闭此页面。</h1></center></body></html>";
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            response.ContentType = "text/html; charset=UTF-8";
            response.ContentEncoding = Encoding.UTF8;
            System.IO.Stream output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();
            listener.Stop();
            Console.WriteLine("Server stopped.");
            Console.WriteLine("MSL step 2");
            stepTwo(code);

        }
        else
        {
            string responseString = "<html><body><center><h1>登录失败,请重试!</h1><center></body></html>";
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            response.ContentType = "text/html; charset=UTF-8";
            response.ContentEncoding = Encoding.UTF8;
            System.IO.Stream output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();
        }
    }
    public static void stepTwo(string code)
    {
        string url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
        var parameters = new Dictionary<string, string>
        {
            { "client_id", "1c" },
            { "scope", "XboxLive.signin offline_access" },
            { "code", code},
            { "redirect_uri", "http://127.0.0.1:40935"},
            { "grant_type", "authorization_code"}
        };
        string context = PostWithParameters(parameters, url, "application/json", "application/x-www-form-urlencoded");
        string accesstoken = GetValueFromJson(context, "access_token");
        string refreshtoken = GetValueFromJson(context, "refresh_token");
        Console.WriteLine($"MSL step 3");
        stepThree(accesstoken);
        Console.WriteLine("MSL refreshtoken test");
        rttest(refreshtoken);
    }
    public static void rttest(string rtoken)
    {
        string url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
        var parameters = new Dictionary<string, string>
        {
            { "client_id", "1" },
            { "scope", "XboxLive.signin offline_access" },
            { "refresh_token", rtoken},
            { "grant_type", "refresh_token"}
        };
        string context = PostWithParameters(parameters, url, "application/json", "application/x-www-form-urlencoded");
        Console.WriteLine(context);
    }
    public static void stepThree(string token)
    {
        string json = "{" +
            "\"Properties\": {" +
                "\"AuthMethod\": \"RPS\"," +
                "\"SiteName\": \"user.auth.xboxlive.com\"," +
                "\"RpsTicket\": \"d=" + token + "\"}," +
            "\"RelyingParty\": \"http://auth.xboxlive.com\"," +
            "\"TokenType\": \"JWT\"}";
        string contenttype = "application/json";
        string url = "https://user.auth.xboxlive.com/user/authenticate";
        string xblres = PostWithJson(json, url, contenttype, contenttype);
        string t = GetValueFromJson(xblres, "Token");
        Console.WriteLine("MSL step 4");
        stepFour(t);
    }

    public static void stepFour(string tokenth){
        string json = "{" +
            "\"Properties\": {" +
                "\"SandboxId\": \"RETAIL\"," +
                "\"UserTokens\": [\"" + tokenth + "\"]}," +
            "\"RelyingParty\": \"rp://api.minecraftservices.com/\"," +
            "\"TokenType\": \"JWT\"}";
        string url = "https://xsts.auth.xboxlive.com/xsts/authorize";
        string contenttype = "application/json";
        string xstsres = PostWithJson(json,url,contenttype,contenttype);
        string token = GetValueFromJson(xstsres, "Token");
        string uhs = GetValueFromJson(xstsres, "DisplayClaims.xui[0].uhs");
        Console.WriteLine("MSL step 5");
        stepFive(token, uhs);
    }
    public static void stepFive(string tokenf, string uhs) {
        string json = "{ \"identityToken\": \"XBL3.0 x=" + uhs + $";{tokenf}\"" + "}";
        string url = "https://api.minecraftservices.com/authentication/login_with_xbox";
        string contenttype = "application/json";
        string mjapires = PostWithJson(json,url, contenttype, contenttype);
        string token = GetValueFromJson(mjapires, "access_token");
        Console.WriteLine("MSL step 6");
        stepSix(token);
    }
    public static void stepSix(string tokenf)
    {
        string url = "https://api.minecraftservices.com/entitlements/mcstore";
        string accept = "application/json";
        string checkres = GetWithAuth($"Bearer {tokenf}", url, accept);
        var jsonObject = JObject.Parse(checkres);
        var items = jsonObject.SelectToken("items") as JArray;
        url = "https://api.minecraftservices.com/minecraft/profile";
        string profileres = GetWithAuth($"Bearer {tokenf}", url, accept);
        bool haveMc = !(items == null || items.Count == 0 || profileres.Contains("NOT_FOUND"));
        Console.WriteLine("Does Minecraft have : " + haveMc.ToString());
        if (haveMc)
        {
            string uuid = GetValueFromJson(profileres, "id");
            string name = GetValueFromJson(profileres, "name");
            Console.WriteLine($"uuid={uuid}\nname={name}");
        }
    }

参考资料

Minecraft Wiki:https://zh.minecraft.wiki
Wiki.vg:https://wiki.vg/


最后修改:2024-08-06 13:09
本文链接:https://blog.huangyu.win/index.php/archives/29/
版权声明:本文 如何编写一个Minecraft Java版启动器 | Part 2 微软登录 | 2-2 为 皇鱼 原创。著作权归作者所有,如无特殊声明,本文将依据CC BY-NC-SA 3.0 CN发布,请注意版权。
转载说明:请依据CC BY-NC-SA 3.0 CN进行转载。
最后修改:2024 年 08 月 06 日
如果觉得我的文章对你有用,请留言/点赞