添加了自省端点允许通过配置修改

This commit is contained in:
huiyiruciduojiao
2025-11-05 20:57:24 +08:00
parent 3a41e0e7f5
commit e1c7f42b8d
6 changed files with 199 additions and 119 deletions

View File

@@ -43,8 +43,12 @@ public class QRLoginConfig extends IdentityProviderModel {
public SignatureConfig.SignatureAlgorithm getAlgorithm(){
return getAlgorithm("algorithm", SignatureConfig.SignatureAlgorithm.HMAC_SHA256);
}
public String getSecret() {
return get("clientSecret", null);
}
public String getClientId() {
return get("clientId", null);
}
private String get(String k, String def) {
String v = model.getConfig() == null ? null : model.getConfig().get(k);

View File

@@ -1,82 +1,82 @@
package top.ysit.qrlogin.core.store;
import com.webauthn4j.util.exception.NotImplementedException;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import top.ysit.qrlogin.core.QRSession;
import top.ysit.qrlogin.core.QRSessionStatus;
import top.ysit.qrlogin.core.SessionStore;
import top.ysit.qrlogin.core.util.JsonUtil;
public class RedisSessionStore implements SessionStore, AutoCloseable {
private final RedisClient client;
private final StatefulRedisConnection<String, String> conn;
private final String ns;
private final int ttlSeconds;
public RedisSessionStore(String uri, String namespace, int ttlSeconds) {
this.client = RedisClient.create(uri);
this.conn = client.connect();
this.ns = namespace;
this.ttlSeconds = ttlSeconds;
}
private String key(String id) {
return ns + id;
}
@Override
public void put(QRSession s) {
RedisCommands<String, String> cmd = conn.sync();
cmd.setex(key(s.getSessionId()), ttlSeconds, JsonUtil.toJson(s));
}
@Override
public QRSession get(String id) {
String json = conn.sync().get(key(id));
return json == null ? null : JsonUtil.fromJson(json, QRSession.class);
}
@Override
public void setScanned(String id) {
QRSession s = get(id);
if (s == null) return;
s.setStatus(QRSessionStatus.SCANNED);
put(s);
}
@Override
public void setConfirmed(String id, String userId) {
QRSession s = get(id);
if (s == null) return;
s.setStatus(QRSessionStatus.CONFIRMED);
s.setUserId(userId);
put(s);
}
@Override
public void setResponseUrl(String sessionId, String url) {
throw new NotImplementedException("Not Impl");
}
@Override
public void expire(String id) {
QRSession s = get(id);
if (s == null) return;
s.setStatus(QRSessionStatus.EXPIRED);
put(s);
}
@Override
public void delete(String id) {
conn.sync().del(key(id));
}
@Override
public void close() {
conn.close();
client.shutdown();
}
}
//package top.ysit.qrlogin.core.store;
//
//import com.webauthn4j.util.exception.NotImplementedException;
//import io.lettuce.core.RedisClient;
//import io.lettuce.core.api.StatefulRedisConnection;
//import io.lettuce.core.api.sync.RedisCommands;
//import top.ysit.qrlogin.core.QRSession;
//import top.ysit.qrlogin.core.QRSessionStatus;
//import top.ysit.qrlogin.core.SessionStore;
//import top.ysit.qrlogin.core.util.JsonUtil;
//
//public class RedisSessionStore implements SessionStore, AutoCloseable {
// private final RedisClient client;
// private final StatefulRedisConnection<String, String> conn;
// private final String ns;
// private final int ttlSeconds;
//
// public RedisSessionStore(String uri, String namespace, int ttlSeconds) {
// this.client = RedisClient.create(uri);
// this.conn = client.connect();
// this.ns = namespace;
// this.ttlSeconds = ttlSeconds;
// }
//
// private String key(String id) {
// return ns + id;
// }
//
// @Override
// public void put(QRSession s) {
// RedisCommands<String, String> cmd = conn.sync();
// cmd.setex(key(s.getSessionId()), ttlSeconds, JsonUtil.toJson(s));
// }
//
// @Override
// public QRSession get(String id) {
// String json = conn.sync().get(key(id));
// return json == null ? null : JsonUtil.fromJson(json, QRSession.class);
// }
//
// @Override
// public void setScanned(String id) {
// QRSession s = get(id);
// if (s == null) return;
// s.setStatus(QRSessionStatus.SCANNED);
// put(s);
// }
//
// @Override
// public void setConfirmed(String id, String userId) {
// QRSession s = get(id);
// if (s == null) return;
// s.setStatus(QRSessionStatus.CONFIRMED);
// s.setUserId(userId);
// put(s);
// }
//
// @Override
// public void setResponseUrl(String sessionId, String url) {
// throw new NotImplementedException("Not Impl");
// }
//
//
// @Override
// public void expire(String id) {
// QRSession s = get(id);
// if (s == null) return;
// s.setStatus(QRSessionStatus.EXPIRED);
// put(s);
// }
//
// @Override
// public void delete(String id) {
// conn.sync().del(key(id));
// }
//
// @Override
// public void close() {
// conn.close();
// client.shutdown();
// }
//}

View File

@@ -7,7 +7,6 @@ import org.keycloak.models.RealmModel;
import top.ysit.qrlogin.config.QRLoginConfig;
import top.ysit.qrlogin.core.SessionStore;
import top.ysit.qrlogin.core.store.InMemorySessionStore;
import top.ysit.qrlogin.core.store.RedisSessionStore;
import top.ysit.qrlogin.idp.QRLoginIdentityProviderFactory;
public class QRLoginStoreUtil {
@@ -44,11 +43,11 @@ public class QRLoginStoreUtil {
* 根据配置创建 SessionStore 实例
*/
public static SessionStore createStore(QRLoginConfig cfg) {
System.out.println("cfg.getStoreType() = " + cfg.getStoreType());
if ("memory".equalsIgnoreCase(cfg.getStoreType())) {
return new InMemorySessionStore(cfg);
}
return new RedisSessionStore(cfg.getRedisUri(), cfg.getRedisNamespace(), cfg.getSessionTtlSeconds());
// return new RedisSessionStore(cfg.getRedisUri(), cfg.getRedisNamespace(), cfg.getSessionTtlSeconds());
return null;
}

View File

@@ -1,5 +1,6 @@
package top.ysit.qrlogin.core.util;
import jakarta.annotation.Nonnull;
import jakarta.json.Json;
import jakarta.json.JsonObject;
@@ -13,19 +14,17 @@ import java.util.Collections;
import java.util.List;
public class TokenUtil {
private static final String KEYCLOAK_URL = "http://localhost:8080";
private static final String REALM = "master";
private static final String CLIENT_ID = "test001";
private static final String CLIENT_SECRET = "JEKhtMyFAic68UdA8CRXChE1OtDOwHzh";
public static TokenValidationResult verifyToken(String token) {
public static TokenValidationResult verifyToken(String token, @Nonnull IntrospectConfig config) {
if (token == null || token.isEmpty()) {
return TokenValidationResult.invalid("token 为空或无效");
}
if (config.baseUrl == null || config.realm == null || config.clientId == null || config.clientSecret == null){
return TokenValidationResult.invalid("配置错误");
}
try {
// 构造 introspect URL
String urlString = KEYCLOAK_URL + "/realms/" + REALM + "/protocol/openid-connect/token/introspect";
String urlString = config.baseUrl + "/realms/" + config.realm + "/protocol/openid-connect/token/introspect";
URL url = new URL(urlString);
// 设置请求
@@ -34,7 +33,7 @@ public class TokenUtil {
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
String body = String.format("client_id=%s&client_secret=%s&token=%s", CLIENT_ID, CLIENT_SECRET, token);
String body = String.format("client_id=%s&client_secret=%s&token=%s", config.clientId, config.clientSecret, token);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
@@ -121,8 +120,6 @@ public class TokenUtil {
public static TokenValidationResult valid(String username, String clientId, String email, String sub, List<String> scope, Long expiration) {
System.out.println("valid" + username + " " + clientId + " " + email + " " + scope + " " + expiration);
return new TokenValidationResult(true, null, username, clientId, email, sub, scope, expiration);
}
@@ -134,4 +131,55 @@ public class TokenUtil {
return new TokenValidationResult(false, error, null, null, null, null, null, null);
}
}
public static class IntrospectConfig {
private String baseUrl;
private String realm;
private String clientId;
private String clientSecret;
public IntrospectConfig(String baseUrl, String realm, String clientId, String clientSecret) {
this.baseUrl = baseUrl;
this.realm = realm;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
public IntrospectConfig() {
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getRealm() {
return realm;
}
public void setRealm(String realm) {
this.realm = realm;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
}
}

View File

@@ -13,6 +13,7 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.MediaType;
import top.ysit.qrlogin.config.QRLoginConfig;
import top.ysit.qrlogin.core.QRSession;
@@ -76,24 +77,37 @@ public class QRLoginIdentityProvider extends AbstractIdentityProvider<IdentityPr
String baseUrl = session.getContext().getUri().getBaseUri().toString();
// 构造请求地址
String confirmUrl = baseUrl + "realms/" + request.getRealm().getName() + "/" + QRLoginEndpointProviderFactory.ID + "/" + "qr/confirm?qr_session=" + s.getSessionId() + "&kc_session=" + authSession.getParentSession().getId();
String checkUrl = baseUrl + "realms/" + request.getRealm().getName() + "/" + QRLoginEndpointProviderFactory.ID + "/" + "qr/status" + "?qr_session=" + s.getSessionId() + "&kc_session=" + authSession.getParentSession().getId();
// 构造二维码中的JSON数据
Map<String, Object> qrData = new HashMap<>();
qrData.put("type", "qr_login");
qrData.put("baseUrl", baseUrl + "realms/" + request.getRealm().getName() + "/" + QRLoginEndpointProviderFactory.ID + "/" + "qr/");
qrData.put("qr_session", s.getSessionId());
qrData.put("kc_session", authSession.getParentSession().getId());
qrData.put("algorithm", cfg.getAlgorithm());
qrData.put("token", ""); // 客户端需要填充
qrData.put("sign", ""); // 客户端需要填充
qrData.put("ttl", cfg.getSessionTtlSeconds());
qrData.put("timestamp", "");
qrData.put("expiredAt", s.getExpireAt().toEpochMilli());
String imageData;
String qrJsonData;
try {
imageData = QRCodeUtil.toDataUrl(confirmUrl, 512);
qrJsonData = JsonSerialization.writeValueAsPrettyString(qrData);
String imageData = QRCodeUtil.toDataUrl(qrJsonData, 512);
return Response.ok(Map.of("qr_session", s.getSessionId(),
"kc_session", authSession.getParentSession().getId(),
"qr_image_data", imageData,
"statusUrl", checkUrl,
"ttl", cfg.getSessionTtlSeconds(),
"interval", cfg.getPollIntervalMs()
))
.type(MediaType.APPLICATION_JSON).build();
} catch (Exception e) {
throw new RuntimeException(e);
}
return Response.ok(Map.of("qr_session", s.getSessionId(),
"kc_session", authSession.getParentSession().getId(),
"qr_image_data", imageData,
"confirm", confirmUrl,
"statusUrl", checkUrl,
"ttl", cfg.getSessionTtlSeconds(),
"interval", cfg.getPollIntervalMs()
))
.type(MediaType.APPLICATION_JSON).build();
}
@@ -135,12 +149,21 @@ public class QRLoginIdentityProvider extends AbstractIdentityProvider<IdentityPr
return Response.status(Response.Status.NOT_FOUND).build();
}
QRSession qrs = qrIdp.store.get(qrSessionId);
if (qrs == null || !qrs.getKcSessionId().equals(kcSessionId) || qrs.getStatus() != QRSessionStatus.CONFIRMED) {;
if (qrs == null || !qrs.getKcSessionId().equals(kcSessionId) || qrs.getStatus() != QRSessionStatus.CONFIRMED) {
return Response.status(Response.Status.NOT_FOUND).build();
}
TokenUtil.TokenValidationResult tokenValidationResult = TokenUtil.verifyToken(token);
if (!tokenValidationResult.valid()) {;
TokenUtil.IntrospectConfig config = new TokenUtil.IntrospectConfig();
config.setClientId(qrIdp.cfg.getClientId());
config.setClientSecret(qrIdp.cfg.getSecret());
String baseUrl = qrIdp.session.getContext().getUri().getBaseUri().toString();
String realm = qrIdp.session.getContext().getRealm().getName();
config.setBaseUrl(baseUrl);
config.setRealm(realm);
TokenUtil.TokenValidationResult tokenValidationResult = TokenUtil.verifyToken(token,config);
if (!tokenValidationResult.valid()) {
return Response.status(Response.Status.NOT_FOUND).build();
}

View File

@@ -160,7 +160,6 @@ public class QRLoginEndpoint extends RealmsResource implements RealmResourceProv
}
// 在 QRLoginEndpoint 类中添加以下私有方法
private ValidationResult validateRequest(Map<String, String> body) {
if (body == null || body.get("timestamp") == null) {
@@ -168,23 +167,29 @@ public class QRLoginEndpoint extends RealmsResource implements RealmResourceProv
}
String secret = qrLoginConfig.getSecret();
String clientId = qrLoginConfig.getClientId();
if (secret == null || clientId == null) {
return ValidationResult.error("Invalid configuration");
}
String kcSession = body.get("kc_session");
String qrSession = body.get("qr_session");
Long timestamp = Long.parseLong(body.get("timestamp"));
String sign = body.get("sign");
String token = body.get("token");
if (kcSession == null || qrSession == null || sign == null || token == null) {
return ValidationResult.error("Invalid request body");
}
Map<String, String> params = new HashMap<>();
params.put("qr_session", qrSession);
params.put("kc_session", kcSession);
params.put("token", token);
params.put("timestamp", String.valueOf(timestamp));
params.put("sign", sign);
try {
if (!signatureUtil.verify(params)) {
return ValidationResult.error("Invalid signature");
@@ -192,18 +197,19 @@ public class QRLoginEndpoint extends RealmsResource implements RealmResourceProv
} catch (SignatureException e) {
throw new RuntimeException(e);
}
QRSession qrs = this.store.get(qrSession);
if (qrs == null) {
return ValidationResult.error("QR session not found");
}
if (!kcSession.equals(qrs.getKcSessionId())) {
return ValidationResult.error("Invalid kcSession");
}
URI baseUri = session.getContext().getUri().getBaseUri();
String realm = session.getContext().getRealm().getName();
TokenUtil.IntrospectConfig introspectConfig = new TokenUtil.IntrospectConfig(baseUri.toString(), realm, clientId, secret);
TokenUtil.TokenValidationResult tokenValidationResult = TokenUtil.verifyToken(token);
TokenUtil.TokenValidationResult tokenValidationResult = TokenUtil.verifyToken(token, introspectConfig);
if (!tokenValidationResult.valid()) {
return ValidationResult.error("Invalid token");
}