本章教大家如何使用微软登录

切记,本章使用的是浏览器回显登录,如果你的启动器不愿意支持这个,请转到下一章OAuth登录。

首先哈,正如文章标题,本章使用的是浏览器回显登录。那么什么是浏览器回显呢?

首先,让我们看看Minecraft官方Wiki上是怎么说的吧:点我

首先,Minecraft的客户端ID被硬编码为00000000402b5328,这是众所周知的。我们可以在浏览器中访问下面那一串HTTP。紧接着,也许会弹出一个让你登录Minecraft的登录窗口,也许不会弹。

那么,有些时候会弹出一个登录窗口是怎么回事呢?此时可能是因为你使用的Windows浏览器不是Edge,并且从未登录过任一微软账号的原因。如果没有弹出登录窗口,而是直接改变了网址栏,变成了【https://login.live.com/oauth20_desktop.srf?code=<登录代码> 】类似于这样的网址。那多半是你的浏览器已经保存了你有关Minecraft微软登录的Cookie,请删掉该浏览器中的所有Cookie后再使用。

好了,然后我再来给大家介绍一下在程序中使用WebBrowsers进行登录而不是使用外置浏览器进行登录的方式:其实此时与外置浏览器登录没有任何区别,只是我们使用的是IE内核的浏览器,与Chromium内核的浏览器的Cookie保存位置不一样,因此即使我们在程序中删掉有关登录信息的任何Cookie也不会有任何问题。

我们所需要实现的目的是:在程序中或者在外置浏览器中,登录网址后对其返回的所有Json进行解析,最终得到Minecraft的登录令牌。

这里切记几点问题:

  1. 由于Minecraft的登录令牌时限为24个小时,因此,在这24个小时内,你不能将你的accessToken暴露给你不熟悉的任何一个人。
  2. Minecraft的刷新令牌实现为一个月,因此,在这一个月内,你不能将你的refreshToken暴露给你不熟悉的任何一个人。
  3. 微软账号的安全性,你不可以将你的微软账号以及密码暴露给你不熟悉的任何一个人。
  4. 由于很多启动器在导出启动参数之时,误将AccessToken给保存到本地目录下,因此,你不可以将AccessToken以明文的形式保存在本地。【这里的明文不仅限于使用Base64加密后的Token。】,否则在MCBBS上很难审核通过。

我们可以来看看,使用登录网址所返回的代码有哪些:

首先是这个:【https://login.live.com/oauth20_desktop.srf?code=************】

我们需要做的,就是将这串返回网址code=后面的所有字符给提取出来。然后对此进行解析,最终获得Minecraft登录令牌了!

还是老样子,先来拽窗口控件吧,像我这样修改你的窗口:

微软登录窗体样式

由于目前我们暂时未学OAuth登录,因此这种方式将暂时搁置一下。

我们双击浏览器登录,然后在浏览器登录的按钮点击事件里,写上这么一串字:

procedure TForm2.Button7Click(Sender: TObject);
begin
  ShellExecute(Application.Handle, nil, pchar(Concat('https://login.live.com/oauth20_authorize.srf',
                                                     '?client_id=00000000402b5328',
                                                     '&response_type=code',
                                                     '&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL',
                                                     '&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf')), nil, nil, SW_SHOWNORMAL);
end;

是的,没错!我们要做的就是调用一个执行cmd指令的程序:ShellExecute,如果你的编程语言里有直接调用系统默认浏览器打开网址的函数,你当然也可以使用哦!

然后,我们会获得一个回调链接,让玩家将回调链接输入进我们的输入框中,然后点击微软登录的单选框,最后,我们只需要点击一次添加按钮即可开始添加我们的微软账号了!

要添加微软账号,首先在我们的添加账号按钮点击事件里面,写上这样几行代码:

procedure TForm2.Button2Click(Sender: TObject);
begin
  if RadioButton1.Checked then begin
    ... //上期离线登录的代码
  end else if RadioButton2.Checked then begin
    if messagebox(Handle, '你选择的是微软正版登录,登录过程可能会卡挺久,需要有耐心!有可能会失败,是否要继续呢?。', '有点久提醒', MB_YESNO+MB_ICONINFORMATION) = 7 then exit; //弹出信息框提示玩家是否使用微软登录,如果选否,则退出。
    var ed: String := Edit3.Text; //将Edit3的输入框转换成String类型的。Edit3就是回调链接的那个框。
    //下面开始判断,如果回调连接里面出现了&lc=,即证明回调链接输入正确。
    if ed.IndexOf('&lc=') <> -1 then begin
      //然后此处开始将code后面的字符串切开。
      if ed.IndexOf('code=') <> -1 then begin
        ed := ed.Substring(ed.IndexOf('code=') + 5, ed.Length); 
      end; //判断回调链接是否合理,共计后面所有的字符都属于需要用到的。
      var cpro: TProc := procedure begin //定义一个线程【这里用到了TTask,这个是我们还没学到的,不过我们也即将要学习了!】
        try
          accm := Account.InitializeAccount(ed, 'post'); //给初始化方法设定一个post值,此处使用了自定义类中的构造函数。
          at := accm.GetAccessToken; //获取AccessToken登录秘钥
          if at = 'noneaccount' then exit; //如果at没有账号,则为返回方法。
          rt := accm.GetRefreshToken;  //获取RefreshToken刷新秘钥
          un := accm.GetUserName;   //获取玩家名字
          ud := accm.GetUUID;       //获取UUID
        except  //读取不出则抛出报错。然后执行。这里为网络连接超时所产生的报错。
          messagebox(Handle, pchar(Concat('你的网络连接超时了,请连接之后再进行网络请求。或者如果你连接了,重试一次即可。')), '连接超时引发的报错', MB_ICONERROR);
          exit; //弹出信息框后退出方法。
        end; //将所有目标添加到配置文件。
        //这里需要配置5个值。type、name、uuid、accessToken、refreshToken,
        //当然,如果你想的话,你可以为accessToken和refreshToken进行加密处理。
        (AccountJson.GetValue('account') as TJsonArray).Add(TJsonObject.Create
          .AddPair('type', 'microsoft')
          .AddPair('name', un)
          .AddPair('uuid', ud)
          .AddPair('access_token', at)
          .AddPair('refresh_token', rt)
        );
        ComboBox1.ItemIndex := ComboBox1.Items.Add(Concat(un, '(微软)')); //为下拉框新建一个元素,为【用户名+(微软)】图示。
        Edit3.Text := ''; //给回调链接输入框重新设为空。
        messagebox(Handle, '添加成功!', '添加成功', MB_OK+MB_ICONINFORMATION);
      end; //添加成功!
      TTask.Run(cpro); //使用TTask执行上述过程。
    end else messagebox(Handle, '你的回调链接输入错误了,请重试', '回调链接错误', MB_OK+MB_ICONERROR); //如果回调链接不满足规范,则弹出信息框。
  end;
end;

上述部分,我们用到了TTask,那么什么是TTask呢?众所周知,C#中有一个Task类,这个与C#那个其实差不多,都是新生成一个线程执行里面的函数罢了!在此之前,我们需要引用一个类:

uses
  Threading;

引用上述Threading类即可!

其中,我们需要接触到新类的概念了。什么是新类?新类就是在type中声明一个别的类,不在原本的类里面的,就叫新类【类比于Java语言,新定义一个public static class的一致。】

看了上面的代码,大家可能会对我们添加账号看得比较懂了吧。

但是,我们要新建的类,可不是public static class哦!而是在外部定义一个class。就相当于Java中的以下代码:

public class Hello{
    public static void main(String[] args){
        Hello2 hello = new Hello2();
        hello.cateat();
    }
}
class Hello2{
    public void cateat(){
        System.out.println("The Cat Like Eat Fish!!");
    }
}

上述代码会输出一个The Cat Like Eat Fish!!,我们要声明的类,就在Hello2这一部分啦!

我们首先往上翻,找到type TForm2这个类,然后像我这么写:

type
  TForm2 = class(TForm) //这里的括号内,表示继承。
  //如果继承的是个类,则只能填一个,反之,interface接口可以填入多个哦!
    ...//Some Control
  end;
  Account = class //此处定义一个新类。不用多写一个type了。
  private
    un, at, uu, rt: String;
    tun, tat, tuu, tct, tbs: String;
  public
    constructor InitializeAccount(key, rr: String); overload; //构造函数
    // constructor InitializeAccount(sn, un, pwd, ct, id, rr: String); overload; 
    // constructor InitializeOAuth(key, token, rr: String); overload; 
    function GetAccessToken: String;
    function GetRefreshToken: String;
    function GetUserName: String;
    function GetUUID: String;
    // function GetThirdAccessToken: String;
    // function GetThirdClientToken: String;
    // function GetThirdUserName: String;
    // function GetThirdUUID: String;
    // function GetThirdBaseCode: string;
    class function GetHttpf(key, web: String): String;
    class function GetHttpy(key, web: String): String;
    class function GetHttph(key, web: String): String;
  end;

我们定义和上面一样多的函数就可以了!

此时,我们可以看到,怎么会这么多函数呢?虽然有些被注释了哈。。

原因很简单,我只是把下一章才教的外置登录和下下一章教到的微软OAuth登录提前将方法定义好了。如果我们去掉下两章的代码,则就是将上方注释的样子。如果去掉注释,则就是下两章说道的内容了!

oh,我发现了一个新的问题,大家应该还没见过constructor这个关键字吧,其实这个关键字是构造函数的意思,也就是类似于java中如下的写法:

public class Hello{
    private final int i;
    private Hello(int i){
        this.i = i;
    }
    public static void main(String[] args){
        Hello hel = new Hello(10);
        System.out.println(hel.i);
    }
}

这么写,上面的private Hello就相当于Delphi中的constructor了!但只不过我们在Java中的写法是private的,而在Delphi里面的写法是public下面的啦!

而且,我们这里不仅声明了一个相同名称的构造函数,这里给大家普及一个知识点,在Delphi中,我们使用构造函数或者别的方法时,如果使用到同一个函数名,但是参数不一样。直接写会报错,此时,我们应该在该函数的末尾声明为overload即可。,这个的意思是多态。

还有一个是关于继承的,在Java或者Kotlin中,我们都有override关键字,或者是它的注解,用于声明这个方法是继承得来的方法,而Delphi中也有override关键字,我们只需要在继承的方法后面写上override就可以了!

因此,就是这么简单了!

然后,我们还要来说说看静态方法,在Delphi中,是不存在static关键字的,但是我们可以用另一种方式来声明一个静态方法,就是在方法前面加上一个class关键字,这样,我们就可以直接通过【类名.方法名】来获取到这个方法了。

好了,接着说,我们先把下面的GetHTTPf、GetHTTPy、GetHTTPh写好之后,我们再去写构造函数啦。

首先,键入以下代码:

implementation //在implementation下方写。

class function Account.GetHTTPf(key: string; web: string): string;
begin
  var ss := TStringStream.Create('', TEncoding.UTF8, False); //首先定义一个流。用于为Post写入请求头。
  var http := TNetHTTPClient.Create(nil); //定义一个TNetHTTPClient,用于进行网络Get、Post指令。
  try
    ss.WriteString(key); //写入流
    ss.Position := 0; //让流的位置返回0
    with http do begin //with关键字是可以无需使用http.某个函数,直接使用就好。
      AcceptCharSet := 'utf-8'; //设置网络请求编码,默认都是utf-8
      AcceptEncoding := '65001'; //设置编码代号,此为默认编码
      AcceptLanguage := 'en-US'; //设置网络请求语言,此为英文。
      ResponseTimeout := 200000; //设置请求时长
      ConnectionTimeout := 200000; //设置连接时长
      SendTimeout := 200000; //设置发送时长【完美的英文理解】,以下为设置请求协议。
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13]; //请求协议,这里调用了SSL3、TLS12和TLS13。
      HandleRedirects := True;  //可以网址重定向,也就是说假如Post、Get的网址有重定向,这样也可以获取到。
      ContentType := 'application/x-www-form-urlencoded;charset=utf-8'; //设置请求类型,这里是x-www-form-urlencoded。
      var res := Post(web, ss); //现在开始Post请求。将写入的头定义的流填入。
      result := res.ContentAsString; //返回网址的请求值,将其转换成String类型。
    end;
  finally
    http.Free; //释放资源
    ss.Free; //释放资源
  end;
end;
class function Account.GetHTTPy(key, web: String): String;
begin
  var ss := TStringStream.Create('', TEncoding.UTF8, False);
  var http := TNetHTTPClient.Create(nil);
  try
    ss.WriteString(key); //该方法与上面的一致
    ss.Position := 0;
    with http do begin
      AcceptCharSet := 'utf-8';
      AcceptEncoding := '65001';
      AcceptLanguage := 'en-US';
      ResponseTimeout := 200000;
      ConnectionTimeout := 200000;
      SendTimeout := 200000;
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13];
      HandleRedirects := True;
      ContentType := 'application/json;charset=utf-8'; //唯独这里不一样,这里使用了json作为请求头类型。
      Accept := 'application/json'; //这里将Accept类型也换成json,只是少了charset=utf-8的。
      var res := Post(web, ss); //然后依旧是一致的哦!
      result := res.ContentAsString;
    end;
  finally
    http.Free;
    ss.Free;
  end;
end;
class function Account.GetHTTPh(key, web: String): String;
begin
  var ss := TStringStream.Create('', TEncoding.UTF8, False);
  var http := TNetHTTPClient.Create(nil);
  try
    with http do begin //这里的h方法,是Get的方法,与上面有些许不一致。
      AcceptCharSet := 'utf-8'; //这里没有将key写入流,因为此时,这个流是用于接收Get请求的返回值的。
      AcceptEncoding := '65001';
      AcceptLanguage := 'en-US';
      ResponseTimeout := 200000;
      ConnectionTimeout := 200000;
      SendTimeout := 200000;
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13];
      HandleRedirects := True; //此处与上面的均一致。
      CustomHeaders['Authorization'] := Concat('Bearer ', key); //这里使用自定义头,使用了Authorization作为键,然后Bearer key作为请求的密钥。
      Get(web, ss); //这里对web进行get请求,然后将一个已经初始化的空流当作第二个参数填入进去。此处不需要使用一个变量接收Get值。
      result := ss.DataString; //返回流的字符串数据。
    end;
  finally
    http.Free;
    ss.Free;
  end;
end;

好了,注释都写好了!我们需要注意的是,在Delphi中,对于Get、Post的请求均不一致,如Post请求需要提前在第二个参数里写上key值,而Get请求,则是直接在CustomHeaders里面,写上键,和值即可。

然后,我们需要注意的是:在别的语言中,可能并没有Delphi这么详细的设置,如设置AcceptEncoding := '65501'、AcceptCharSet := 'utf-8'等的设置,大多数语言都会有一个默认值。大家当然也可以完全按照所需要的语言教程中的HTTP请求的默认值来写哦!

然后嘛,将这一串代码复制进Delphi中,可能会发生大报错。原因是我们还没有引用任何一个头文件。这个TNetHTTPClient是需要引用好几个头文件才能够正常使用的。

我们需要引用下方几个头【如果你所编写的语言需要import什么类,你就照常引用就好了】

uses
  System.Net.URLClient, System.Net.HttpClient, System.Net.HttpClientComponent;

我们需要引用整整三个头文件,是的,TNetHTTPClient就是这样!

当然,如果你想自己找头文件引用的话,你可以进入窗口视图,在右下角的Palette中,搜索TNetHTTPClient,然后将其中的某个控件拖到窗体上,再进入一次代码视图,然后再回到窗体视图,右键TNetHTTPClient删掉这个控件即可。

自然,照我上面一说,大家肯定认为这个TNetHTTPClient是一个控件。那为什么我们不在Delphi里面直接拖入一个TNetHTTPClient控件,而非要在程序中自己Create一个呢?原因很简单,拖入的控件无法free资源因为一旦free后,其他时候再想调用就会很麻烦,而且无法做到每个函数执行完一次后直接free资源,从而达到内存不会泄漏,同时内存可以及时的释放资源。综上所述,我们需要在程序里自己Create一个TNetHTTPClient控件。

然后嘛,自然就是我们的

function GetAccessToken: String;
function GetRefreshToken: String;
function GetUserName: String;
function GetUUID: String;

这几个函数啦。这几个函数尤为简单,我们为什么会这么写呢?原因不得而知,我们有几个全局变量是private类型的,因此我们需要写上这么几个函数啦!

然后,还有,我不是很清楚Java里面的private函数作用域是在本文件里到处可以使用,还是在本类中到处可以使用。我只知道Delphi里面,private内的变量或函数的作用域是在本单元文件中的到处都可以使用,无关于类。

但是,我们还是要写上这么四个函数【后期做第三方Authlib登录的时候也需要】,因为后面我们的启动游戏过程中,会启动过程中调用一次微软的验证服务器来验证我们的accessToken是否合法。

我们直接开始写吧:

function Account.GetAccessToken: string;
begin
  result := at;
end;
function Account.GetRefreshToken: string;
begin
  result := rt;
end;
function Account.GetUserName: string;
begin
  result := un;
end;
function Account.GetUUID: string;
begin
  result := uu;
end;

那么,这里就是我们的函数了!是不是很简单呢?自然,我们也可以对其进行判空,例如判断at、rt、un、uu全局变量是否为空,如果为空,则raise一个Exception,都是可以的!

这几个变量我在之前已经说过了哦!大家可以回去看看我的Account这个类里面的private代码块下面的全局变量哦!

好了好了,接下来才是重头戏,我们需要写微软登录的构造函数InitializeAccount了!这个的中文意思是初始化账号哦!


//初始化微软登录
constructor Account.InitializeAccount(key, rr: String); //构造函数实现
const  //设置4个请求网址
  micro = 'https://login.live.com/oauth20_token.srf';
  xbox = 'https://user.auth.xboxlive.com/user/authenticate';
  xsts = 'https://xsts.auth.xboxlive.com/xsts/authorize';
  mccc = 'https://api.minecraftservices.com/authentication/login_with_xbox';
  ishas = 'https://api.minecraftservices.com/minecraft/profile';
begin
  var k1: String;
  if rr = 'refresh' then
    k1 := Concat('client_id=00000000402b5328',
      '&refresh_token=', key,
      '&grant_type=refresh_token',
      '&redirect_uri=https://login.live.com/oauth20_desktop.srf',
      '&scope=service::user.auth.xboxlive.com::MBI_SSL')
  else
    k1 := Concat('client_id=00000000402b5328',
      '&code=', key,
      '&grant_type=authorization_code',
      '&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf',
      '&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL');
    //这里是请求microsoft的。
  Form3.Label8.Caption := '正在请求microsoft中……';
  var t1 := Account.GetHttpf(k1, micro);  //传值进方法并将返回值设置。
  var j1 := TJsonObject.ParseJSONValue(t1) as TJsonObject; //设置json解析
  var w1 := j1.GetValue('access_token').Value; //获取assets_token,下面继续设置请求参数。
  //这里是请求xbox的
  var k2 := Concat('{"Properties":{"AuthMethod":"RPS","SiteName":"user.auth.xboxlive.com","RpsTicket":"d=', w1, '"},"RelyingParty":"http://auth.xboxlive.com","TokenType":"JWT"}'); //设定xbox的请求头。
  var t2 := Account.GetHttpy(k2, xbox);
  var j2 := TJsonObject.ParseJSONValue(t2) as TJsonObject;
  var w2 := j2.GetValue('Token').Value;
  //这里将获取到uhs的值。
  var r1 := j2.GetValue('DisplayClaims') as TJsonObject;
  var ur := r1.GetValue('xui') as TJsonArray;
  var uhs := ur[0].GetValue<String>('uhs');
  //这里是请求xsts的。
  var k3 := Concat('{"Properties":{"SandboxId":"RETAIL","UserTokens":["', w2, '"]},"RelyingParty":"rp://api.minecraftservices.com/","TokenType":"JWT"}');//设定xsts的请求头。
  var t3 := Account.GetHttpy(k3, xsts);
  var j3 := TJsonObject.ParseJSONValue(t3) as TJsonObject;
  var w3 := j3.GetValue('Token').Value;
  //这里将判断uhs是否一致【基本上都会一致的,没有存在不一致的情况。。。】
  var rr1 := j3.GetValue('DisplayClaims') as TJsonObject; //对uhs进行json解析。
  var uur := rr1.GetValue('xui') as TJsonArray;
  var uhhs := uur[0].GetValue<String>('uhs');
  if uhhs <> uhs then raise Exception.Create('Microsoft uhs is not equal');
  //这一步是请求mc的。
  var k4 := Concat('{"identityToken":"XBL3.0 x=', uhs, ';', w3, '"}');
  var t4 := Account.GetHttpy(k4, mccc);
  var j4 := TJsonObject.ParseJSONValue(t4) as TJsonObject;
  var w4 := j4.GetValue('access_token').Value; //获取到accesstoken。
  //PS:直到这一步,我们才正式的获取到accessToken。很复杂,但也很耐人寻味。
  //获取是否为购买了mc的样子。
  var t5 := Account.GetHttph(w4, ishas);
  var j5 := TJsonObject.ParseJSONValue(t5) as TJsonObject; //将通过accessToken的json值,解析出来,并判断里面是否有以下键值。
  try //判断里面是否有name、id值,如果有,则将rt、at一并赋值为refresh_token、access_token。
    un := j5.GetValue('name').Value;
    uu := j5.GetValue('id').Value;
    rt := j1.GetValue('refresh_token').Value;
    at := j4.GetValue('access_token').Value;
  except //如果里面没有值,则抛出报错。
    if messagebox(Form2.Handle, '不好意思,您的Microsoft账户并没有购买Minecraft,请问是否立即前往官网购买?', '暂未购买,是否前往商店', MB_YESNO+MB_ICONERROR) = 6 then //暂未购买。
    begin //打开一个网址,请用户购买。
      ShellExecute(Application.Handle, nil,
        'https://www.minecraft.net/zh-hans/store/minecraft-java-edition',
        nil, nil, SW_SHOWNORMAL)
    end;
    at := 'noneaccount';
  end;
end;

好了好了,这就是我们的构造函数了哦!

至于为什么要这么写呢?我们来看看各个请求的返回json数据是怎么样的吧!

首先是第一个:请求Microsoft的步骤!

POST https://login.live.com/oauth20_token.srf
Content-Type: application/x-www-form-urlencoded

&code=key&grant_type=authorization_code&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL

其返回的数据是:

参数描述
token_type总是Bearer。
expires_in有效时间:以秒为单位,这里是24小时。
scope请求数据,这里请求的是xbox.com。
access_token你的Access Token,但是这个无法用于启动游戏,这个是用来请求下一步操作的。保存它。
refresh_token你的Refresh Token,这个可以用来重置账号。
user_id你登录时用的账号id,每个用户只有一份。
foci暂不清楚用途,一直都是1。

如果你是刷新账号的话,只需要把以上的请求参数修改成refresh_token即可!

下面我们再来看看请求xbox的吧,这里我们需要用到上一次的access_token。

POST https://user.auth.xboxlive.com/user/authenticate
Content-Type: application/json
Accept: application/json

{
  "Properties": {
    "AuthMethod": "RPS",
    "SiteName": "user.auth.xboxlive.com",
    "RpsTicket": "d=<access_token>" 
  },
  "RelyingParty": "http://auth.xboxlive.com",
  "TokenType": "JWT"
}

其中,RpsTicket里面,我们在填入access_token的时候,我们需要在前面填入一个d=,这点必须。

其返回的数据为:

参数描述
IssueInstant你的请求时间
NotAfter你的请求时间【与上者相同】
Token你的Xbox Access Token,保存它。
DisplayClaims请求显示要求,对于每个微软软件登录操作各不相同,例如我的世界地下城。
xui显示你的要求之一,这里用xui写。
uhs这里是你的uhs代码,保存它。

在这里,我们需要保存两个东西,第一个是uhs,第二个是Token。

再来看看xsts的请求数据吧,这里用到我们上一步用到的Token值。

POST https://xsts.auth.xboxlive.com/xsts/authorize
Content-Type: application/json
Accept: application/json

{
  "Properties": {
    "SandboxId": "RETAIL",
    "UserTokens": [
      "xbox_token"
    ]
  },
  "RelyingParty": "rp://api.minecraftservices.com/",
  "TokenType": "JWT"
}

其返回的数据与上方的一致,并且uhs也是一致的,我们只需要保存内部的Token即可!

下面我们就可以直接获取Minecraft的最终Access Token了!!我们的xsts获取的Token刚好可以用于验证我们的Minecraft。

POST https://api.minecraftservices.com/authentication/login_with_xbox
Content-Type: application/json
Accept: application/json

{
  "identityToken": "XBL3.0 x=<uhs>;<xsts_token>"
}

这里的参数拼接可能有些奇怪,但也根本不影响我们书写!

其返回的数据是:

参数描述
username用户的验证名称,这里并不是用户的最终UUID……
roles这里应该是空数组,不清楚用途,应该是用户属性之类的。
access_token你的老朋友,Access Token,该Token需要保存,后启动游戏。
token_type持有者,一直都是Bearer
expires_in有效期,这里是24小时,也就是一天。

现在我们需要验证用户的账号里是否购买了Minecraft,如果购买了,则获取它的用户名和UUID。

我们看到wiki上写了一个获取mcstore的,但是着实想了想,根本没必要啊……我们可以直接获取用户数据,最后获得其uuid和名称,如果获取不到的话,就抛出报错,将错误展现给我们的用户即可!

来看验证Minecraft有效性:

GET https://api.minecraftservices.com/minecraft/profile
Header[Authorization]: Bearer <your access token>

上面的your access token写上你的登录用的AccessToken。然后前面填入的是Header哦!

其返回的数据是:

参数描述
id你账号的真实UUID
name你账号的真实用户名
skins账号皮肤
capes账号披风

对于皮肤和披风的解析请自行参照官方结构,这里暂不赘述。

如果用户未拥有Minecraft,则解析看起来如下:

参数描述
path用户请求错误的路径
errorType错误类型:这里应该是NOT_FOUND
error错误标志
errorMessage错误信息,这里会描述你为什么会遇到错误【英文】
developerMessage开发者信息

好了,那么我们的请求链接就写完了,大概就是这样的啦!各位可以看看啦!

这么写完了以后,我们再次回到我们的按钮,我们惊讶的发现了,我们的按钮已经设置完毕了!那么,此时此刻,我们就可以回到我们的主窗口,点击我们的启动游戏按钮,然后键入以下代码:


procedure TForm1.Button1Click(Sender: TJsonObject);
begin
  ... //前面省略
    if typetemp = 'offline' then begin //如果type键等于offline,则执行。
      PlayerName := chooseAbout.GetValue('name').Value; //设置为name
      PlayerUUID := chooseAbout.GetValue('uuid').Value; //设置为uuid
      PlayerAcTo := PlayerUUID; //accessToken等于uuid
      PlayerType := 'Legacy'; //type为Legacy。
      PlayerAuth := ''; //为空
    end //将末尾分号去掉。
    else if typetemp = 'microsoft' then begin
      PlayerName := chooseAbout.GetValue('name').Value; //设置为name
      PlayerUUID := chooseAbout.GetValue('uuid').Value; //设置为uuid
      PlayerAcTo := chooseAbout.GetValue('access_token').Value; //设置为accessToken
      PlayerType := 'msa'; //type为msa。这样才能在多人联机中发言。
      PlayerAuth := ''; //为空
    end;
  ... //后面省略
end;

然后,此时此刻我们再尝试一下启动游戏!成功了!微软登录成功了!