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>
|
<properties>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<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>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
@@ -86,4 +86,6 @@ public class QRSession {
|
|||||||
public void setResponseUrl(String responseUrl) {
|
public void setResponseUrl(String responseUrl) {
|
||||||
this.responseUrl = responseUrl;
|
this.responseUrl = responseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ public class TokenUtil {
|
|||||||
if (token == null || token.isEmpty()) {
|
if (token == null || token.isEmpty()) {
|
||||||
return TokenValidationResult.invalid("token 为空或无效");
|
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("配置错误");
|
return TokenValidationResult.invalid("配置错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +62,6 @@ public class TokenUtil {
|
|||||||
JsonObject payload = tokenToJson(token);
|
JsonObject payload = tokenToJson(token);
|
||||||
String email = payload.getString("email", null);
|
String email = payload.getString("email", null);
|
||||||
String sub = json.getString("sub", null);
|
String sub = json.getString("sub", null);
|
||||||
|
|
||||||
|
|
||||||
return TokenValidationResult.valid(username, clientId, email, sub, scope, exp);
|
return TokenValidationResult.valid(username, clientId, email, sub, scope, exp);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -82,7 +80,6 @@ public class TokenUtil {
|
|||||||
if (token == null || token.isEmpty()) {
|
if (token == null || token.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// JWT token 由三部分组成,用点分隔:header.payload.signature
|
// JWT token 由三部分组成,用点分隔:header.payload.signature
|
||||||
String[] parts = token.split("\\.");
|
String[] parts = token.split("\\.");
|
||||||
@@ -103,7 +100,8 @@ public class TokenUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Base64 解码
|
// 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);
|
String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// 解析为 JsonObject
|
// 解析为 JsonObject
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import org.keycloak.models.FederatedIdentityModel;
|
|||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
import org.keycloak.utils.MediaType;
|
import org.keycloak.utils.MediaType;
|
||||||
import top.ysit.qrlogin.config.QRLoginConfig;
|
import top.ysit.qrlogin.config.QRLoginConfig;
|
||||||
@@ -71,6 +73,7 @@ public class QRLoginIdentityProvider extends AbstractIdentityProvider<IdentityPr
|
|||||||
s.setCreatedAt(Instant.now());
|
s.setCreatedAt(Instant.now());
|
||||||
s.setKcSessionId(authSession.getParentSession().getId());
|
s.setKcSessionId(authSession.getParentSession().getId());
|
||||||
s.setAuthSession(authSession);
|
s.setAuthSession(authSession);
|
||||||
|
|
||||||
store.put(s);
|
store.put(s);
|
||||||
|
|
||||||
|
|
||||||
@@ -166,13 +169,16 @@ public class QRLoginIdentityProvider extends AbstractIdentityProvider<IdentityPr
|
|||||||
if (!tokenValidationResult.valid()) {
|
if (!tokenValidationResult.valid()) {
|
||||||
return Response.status(Response.Status.NOT_FOUND).build();
|
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());
|
BrokeredIdentityContext federatedIdentity = new BrokeredIdentityContext(tokenValidationResult.sub(), qrIdp.getConfig());
|
||||||
federatedIdentity.setIdp(qrIdp);
|
federatedIdentity.setIdp(qrIdp);
|
||||||
|
|
||||||
federatedIdentity.setUsername(tokenValidationResult.username());
|
federatedIdentity.setUsername(tokenValidationResult.username());
|
||||||
federatedIdentity.setEmail(tokenValidationResult.email());
|
federatedIdentity.setEmail(tokenValidationResult.email());
|
||||||
federatedIdentity.setAuthenticationSession(qrs.getAuthSession());
|
federatedIdentity.setAuthenticationSession(authSession);
|
||||||
|
|
||||||
|
|
||||||
return callback.authenticated(federatedIdentity);
|
return callback.authenticated(federatedIdentity);
|
||||||
|
|||||||
@@ -61,8 +61,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (expired) return;
|
if (expired) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${statusUrl}${statusUrl.includes('?') ? '&' : '?'}timestamp=${Math.floor(Date.now() / 1000)}`);
|
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();
|
const data = await resp.json();
|
||||||
|
|
||||||
|
|
||||||
switch (data.status) {
|
switch (data.status) {
|
||||||
case "CONFIRMED":
|
case "CONFIRMED":
|
||||||
const url = data.url;
|
const url = data.url;
|
||||||
@@ -121,5 +133,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
poll();
|
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