Skip to content

支持通过 OAuth 登录 Littleskin #3491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val littleSkinClientId = System.getenv("LITTLT_SKIN_CLIENT_ID") ?: "866" // TODO

val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")

@@ -137,6 +138,7 @@ tasks.shadowJar {
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"LittleSkin-Client-Id" to littleSkinClientId,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
34 changes: 24 additions & 10 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/OAuthServer.java
Original file line number Diff line number Diff line change
@@ -25,7 +25,6 @@
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;

import java.io.IOException;
@@ -123,28 +122,39 @@ public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();

private final String clientId;
private final String clientSecret;

public Factory(String clientId, String clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
}

@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
if (StringUtils.isBlank(getClientId())) {
throw new MicrosoftAuthenticationNotSupportedException();
}

IOException exception = null;
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
for (int port = 29111; port < 29116; port++) {
try {
OAuthServer server = new OAuthServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
return server;
} catch (IOException e) {
exception = e;
if (exception == null) {
exception = new IOException();
}
exception.addSuppressed(e);
}
}
throw exception;
}

@Override
public void grantDeviceCode(String userCode, String verificationURI) {
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
public void grantDeviceCode(String userCode, String verificationURI, String verificationUriComplete) {
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI, verificationUriComplete));
}

@Override
@@ -157,14 +167,12 @@ public void openBrowser(String url) throws IOException {

@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
return clientId;
}

@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
return clientSecret;
}

@Override
@@ -176,11 +184,13 @@ public boolean isPublicClient() {
public static class GrantDeviceCodeEvent extends Event {
private final String userCode;
private final String verificationUri;
private final String verificationUriComplete;

public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri, String verificationUriComplete) {
super(source);
this.userCode = userCode;
this.verificationUri = verificationUri;
this.verificationUriComplete = verificationUriComplete;
}

public String getUserCode() {
@@ -190,6 +200,10 @@ public String getUserCode() {
public String getVerificationUri() {
return verificationUri;
}

public String getVerificationUriComplete() {
return verificationUriComplete;
}
}

public static class OpenBrowserEvent extends Event {
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.littleskin.LittleSkinAccount;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
@@ -355,7 +356,7 @@ public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid
}

public static void bindAvatar(Canvas canvas, Account account) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof OfflineAccount)
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof LittleSkinAccount || account instanceof OfflineAccount)
fxAvatarBinding(canvas, skinBinding(account));
else {
unbindAvatar(canvas);
40 changes: 25 additions & 15 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java
Original file line number Diff line number Diff line change
@@ -27,6 +27,9 @@
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.*;
import org.jackhuang.hmcl.auth.littleskin.LittleSkinAccount;
import org.jackhuang.hmcl.auth.littleskin.LittleSkinAccountFactory;
import org.jackhuang.hmcl.auth.littleskin.LittleSkinService;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
@@ -35,6 +38,7 @@
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;

@@ -80,12 +84,23 @@ private static void triggerAuthlibInjectorUpdateCheck() {
}
}

public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OAuthServer.Factory MICROSOFT_OAUTH_CALLBACK = new OAuthServer.Factory(
System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getManifestAttribute("Microsoft-Auth-Id", "")),
System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""))
);

public static final OAuthServer.Factory LITTLE_SKIN_CALLBACK = new OAuthServer.Factory(
JarUtils.getManifestAttribute("LittleSkin-Client-Id", ""),
""
);

public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MICROSOFT_OAUTH_CALLBACK));
public static final LittleSkinAccountFactory FACTORY_LITTLE_SKIN = new LittleSkinAccountFactory(new LittleSkinService(LITTLE_SKIN_CALLBACK));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_LITTLE_SKIN, FACTORY_AUTHLIB_INJECTOR);

// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -95,6 +110,7 @@ private static void triggerAuthlibInjectorUpdateCheck() {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.put("microsoft", FACTORY_MICROSOFT);
type2factory.put("littleskin", FACTORY_LITTLE_SKIN);

type2factory.forEach((type, factory) -> factory2type.put(factory, type));
}
@@ -127,6 +143,8 @@ else if (account instanceof AuthlibInjectorAccount)
return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof MicrosoftAccount)
return FACTORY_MICROSOFT;
else if (account instanceof LittleSkinAccount)
return FACTORY_LITTLE_SKIN;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
@@ -208,16 +226,6 @@ static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");

if (!config().isAddedLittleSkin()) {
AuthlibInjectorServer littleSkin = new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/");

if (config().getAuthlibInjectorServers().stream().noneMatch(it -> littleSkin.getUrl().equals(it.getUrl()))) {
config().getAuthlibInjectorServers().add(0, littleSkin);
}

config().setAddedLittleSkin(true);
}

loadGlobalAccountStorages();

// load accounts
@@ -420,7 +428,9 @@ private static void removeDanglingAuthlibInjectorAccounts() {
private static final Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"),
pair(Accounts.FACTORY_LITTLE_SKIN, "account.methods.littleskin")
);

public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
@@ -486,7 +496,7 @@ public static String localizeErrorMessage(Exception exception) {
} else if (exception instanceof MicrosoftService.NoXuiException) {
return i18n("account.methods.microsoft.error.add_family_probably");
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
return i18n("account.methods.microsoft.snapshot");
return i18n("account.methods.snapshot");
} else if (exception instanceof OAuthAccount.WrongAccountException) {
return i18n("account.failed.wrong_account");
} else if (exception.getClass() == AuthenticationException.class) {
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.littleskin.LittleSkinService;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
@@ -168,12 +169,23 @@ public AccountListPageSkin(AccountListPage skinnable) {
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));

AdvancedListItem littleSkinItem = new AdvancedListItem();
littleSkinItem.getStyleClass().add("navigation-drawer-item");
littleSkinItem.setActionButtonVisible(false);
littleSkinItem.setTitle("LittleSkin");
littleSkinItem.setLeftGraphic(wrap(SVG.DRESSER));
littleSkinItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_LITTLE_SKIN)));
boxMethods.getChildren().add(littleSkinItem);

VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG.DRESSER));
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
if (LittleSkinService.API_ROOT.equals(server.getUrl())) {
item.setVisible(false);
}

JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
Loading
Oops, something went wrong.