mirror of
https://github.com/huiyiruciduojiao/KeycloakQRLogin.git
synced 2026-01-28 03:24:37 +08:00
fix(qr-login): restore AuthenticationSession on external QR callback flow
解决扫码登录在跨设备回调场景下丢失 AuthenticationSession 的问题。 在 QR 登录流程中,浏览器端发起登录请求时会创建 AuthenticationSession, 但移动端扫码确认回调是独立请求,不携带浏览器 Cookie 和会话上下文, 导致直接持有的 AuthenticationSessionModel 引用无效并返回 null。 修复内容: - 移除直接缓存 AuthenticationSessionModel 引用的方式 - 使用 AuthenticationSessionManager + kc_session_id + tabId 恢复浏览器会话 - 确保扫码回调能够正确续接原始认证流程并完成 Broker 登录 该修复提高了跨设备扫码登录的稳定性与兼容性,避免因会话断链导致登录失败。
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -11,7 +11,7 @@
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<keycloak.version>26.2.4</keycloak.version>
|
||||
<keycloak.version>26.4.2</keycloak.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -86,4 +86,6 @@ public class QRSession {
|
||||
public void setResponseUrl(String responseUrl) {
|
||||
this.responseUrl = responseUrl;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class TokenUtil {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return TokenValidationResult.invalid("token 为空或无效");
|
||||
}
|
||||
if (config.baseUrl == null || config.realm == null || config.clientId == null || config.clientSecret == null){
|
||||
if (config.baseUrl == null || config.realm == null || config.clientId == null || config.clientSecret == null) {
|
||||
return TokenValidationResult.invalid("配置错误");
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ public class TokenUtil {
|
||||
JsonObject payload = tokenToJson(token);
|
||||
String email = payload.getString("email", null);
|
||||
String sub = json.getString("sub", null);
|
||||
|
||||
|
||||
return TokenValidationResult.valid(username, clientId, email, sub, scope, exp);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -82,7 +80,6 @@ public class TokenUtil {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// JWT token 由三部分组成,用点分隔:header.payload.signature
|
||||
String[] parts = token.split("\\.");
|
||||
@@ -103,7 +100,8 @@ public class TokenUtil {
|
||||
}
|
||||
|
||||
// Base64 解码
|
||||
byte[] decodedBytes = java.util.Base64.getDecoder().decode(payload);
|
||||
// 使用URL安全的Base64解码器
|
||||
byte[] decodedBytes = java.util.Base64.getUrlDecoder().decode(payload);
|
||||
String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
|
||||
|
||||
// 解析为 JsonObject
|
||||
|
||||
@@ -12,7 +12,9 @@ import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
import top.ysit.qrlogin.config.QRLoginConfig;
|
||||
@@ -71,6 +73,7 @@ public class QRLoginIdentityProvider extends AbstractIdentityProvider<IdentityPr
|
||||
s.setCreatedAt(Instant.now());
|
||||
s.setKcSessionId(authSession.getParentSession().getId());
|
||||
s.setAuthSession(authSession);
|
||||
|
||||
store.put(s);
|
||||
|
||||
|
||||
@@ -166,13 +169,16 @@ public class QRLoginIdentityProvider extends AbstractIdentityProvider<IdentityPr
|
||||
if (!tokenValidationResult.valid()) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(qrIdp.session);
|
||||
RootAuthenticationSessionModel root = asm.getCurrentRootAuthenticationSession(qrIdp.session.getContext().getRealm());
|
||||
AuthenticationSessionModel authSession = root.getAuthenticationSession(qrs.getAuthSession().getClient(), qrs.getAuthSession().getTabId());
|
||||
|
||||
BrokeredIdentityContext federatedIdentity = new BrokeredIdentityContext(tokenValidationResult.sub(), qrIdp.getConfig());
|
||||
federatedIdentity.setIdp(qrIdp);
|
||||
|
||||
federatedIdentity.setUsername(tokenValidationResult.username());
|
||||
federatedIdentity.setEmail(tokenValidationResult.email());
|
||||
federatedIdentity.setAuthenticationSession(qrs.getAuthSession());
|
||||
federatedIdentity.setAuthenticationSession(authSession);
|
||||
|
||||
|
||||
return callback.authenticated(federatedIdentity);
|
||||
|
||||
@@ -61,8 +61,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (expired) return;
|
||||
try {
|
||||
const resp = await fetch(`${statusUrl}${statusUrl.includes('?') ? '&' : '?'}timestamp=${Math.floor(Date.now() / 1000)}`);
|
||||
// 检查HTTP状态码,处理404等情况
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 404) {
|
||||
// 处理404错误,视为二维码失效
|
||||
handleQRCodeExpired();
|
||||
return;
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${resp.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
|
||||
switch (data.status) {
|
||||
case "CONFIRMED":
|
||||
const url = data.url;
|
||||
@@ -121,5 +133,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
};
|
||||
poll();
|
||||
const handleQRCodeExpired = () => {
|
||||
expired = true;
|
||||
tip.innerText = "二维码已失效,请重新开始登录流程";
|
||||
tip.style.color = "#dc3545";
|
||||
count.style.display = "none";
|
||||
|
||||
// 模糊二维码图像
|
||||
const qrImage = box.querySelector('img');
|
||||
if (qrImage) {
|
||||
qrImage.style.filter = "blur(4px)";
|
||||
}
|
||||
|
||||
btn.innerText = "重新开始";
|
||||
btn.style.background = "#007bff";
|
||||
btn.onclick = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user