mirror of
https://github.com/huiyiruciduojiao/KeycloakQRLogin.git
synced 2026-01-28 03:24:37 +08:00
添加了自省端点允许通过配置修改
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
// }
|
||||
//}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user