Compare commits

...

1 Commits
master ... view

Author SHA1 Message Date
鱼星 a6b2c86076 fix: 新增可视化页面 2 weeks ago

@ -257,6 +257,11 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</build>

@ -0,0 +1,52 @@
package com.ngskcloud.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "sa-token.sso-server")
public class SsoServerClientsProperties {
private Map<String, Client> clients = new LinkedHashMap<>();
public Map<String, Client> getClients() {
return clients;
}
public void setClients(Map<String, Client> clients) {
this.clients = clients == null ? new LinkedHashMap<>() : clients;
}
public static class Client {
private String client;
private String allowUrl;
private String secretKey;
public String getClient() {
return client;
}
public void setClient(String client) {
this.client = client;
}
public String getAllowUrl() {
return allowUrl;
}
public void setAllowUrl(String allowUrl) {
this.allowUrl = allowUrl;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
}
}

@ -0,0 +1,76 @@
package com.ngskcloud.controller;
import com.ngskcloud.config.SsoServerClientsProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/sso/dashboard")
public class SsoDashboardController {
private final SsoServerClientsProperties properties;
public SsoDashboardController(SsoServerClientsProperties properties) {
this.properties = properties;
}
@GetMapping("/clients")
public DashboardClientsResponse clients() {
List<SsoClientView> clients = properties.getClients().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> toView(entry.getKey(), entry.getValue()))
.toList();
return new DashboardClientsResponse(clients.size(), Instant.now().toString(), clients);
}
private SsoClientView toView(String id, SsoServerClientsProperties.Client client) {
String name = valueOrFallback(client.getClient(), id);
String allowUrl = valueOrFallback(client.getAllowUrl(), "-");
boolean secretConfigured = hasText(client.getSecretKey());
String status = hasText(client.getClient()) && secretConfigured ? "CONFIGURED" : "INCOMPLETE";
return new SsoClientView(
id,
name,
allowUrl,
secretConfigured,
maskSecret(client.getSecretKey()),
status);
}
private String maskSecret(String secretKey) {
if (!hasText(secretKey)) {
return "";
}
if (secretKey.length() <= 10) {
return "****";
}
return secretKey.substring(0, 7) + "****" + secretKey.substring(secretKey.length() - 3);
}
private String valueOrFallback(String value, String fallback) {
return hasText(value) ? value : fallback;
}
private boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
public record DashboardClientsResponse(int total, String generatedAt, List<SsoClientView> clients) {
}
public record SsoClientView(
String id,
String name,
String allowUrl,
boolean secretConfigured,
String secretPreview,
String status) {
}
}

@ -1,6 +1,323 @@
<html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SSO 接入平台看板</title>
<style>
:root {
color-scheme: light;
--bg: #f5f7fb;
--panel: #ffffff;
--text: #172033;
--muted: #667085;
--line: #d9e0ea;
--accent: #2563eb;
--ok: #138a56;
--warn: #b45309;
--shadow: 0 18px 40px rgba(23, 32, 51, 0.08);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.shell {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0;
}
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
margin-bottom: 24px;
}
h1 {
margin: 0 0 8px;
font-size: 30px;
line-height: 1.2;
letter-spacing: 0;
}
.subtitle {
margin: 0;
color: var(--muted);
font-size: 14px;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
button {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
color: var(--text);
min-height: 38px;
padding: 0 14px;
cursor: pointer;
font-weight: 600;
}
button:hover {
border-color: var(--accent);
color: var(--accent);
}
.summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.metric {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 18px;
box-shadow: var(--shadow);
}
.metric span {
display: block;
color: var(--muted);
font-size: 13px;
margin-bottom: 10px;
}
.metric strong {
display: block;
font-size: 28px;
line-height: 1;
}
.client-list {
display: grid;
gap: 14px;
}
.client-card {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr auto;
gap: 16px;
align-items: center;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 18px;
box-shadow: var(--shadow);
}
.label {
color: var(--muted);
font-size: 12px;
margin-bottom: 6px;
}
.value {
font-size: 14px;
overflow-wrap: anywhere;
}
.platform-name {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
}
.client-id {
color: var(--muted);
font-size: 13px;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 86px;
height: 30px;
border-radius: 999px;
padding: 0 12px;
font-size: 13px;
font-weight: 700;
}
.badge.ok {
background: #e7f8ef;
color: var(--ok);
}
.badge.warn {
background: #fff4df;
color: var(--warn);
}
.empty,
.error {
background: var(--panel);
border: 1px dashed var(--line);
border-radius: 8px;
padding: 28px;
color: var(--muted);
text-align: center;
}
@media (max-width: 760px) {
.topbar {
align-items: flex-start;
flex-direction: column;
}
.summary {
grid-template-columns: 1fr;
}
.client-card {
grid-template-columns: 1fr;
}
.badge {
width: fit-content;
}
}
</style>
</head>
<body>
<h1>hello word!!!</h1>
<p>this is a html page</p>
<main class="shell">
<section class="topbar">
<div>
<h1>SSO 接入平台看板</h1>
<p class="subtitle">查看当前认证中心已配置的平台接入状态</p>
</div>
<div class="actions">
<button id="refreshButton" type="button" aria-label="刷新接入平台">刷新</button>
</div>
</section>
<section class="summary" aria-label="接入概览">
<div class="metric">
<span>已配置平台</span>
<strong id="totalCount">0</strong>
</div>
<div class="metric">
<span>密钥完整</span>
<strong id="secretCount">0</strong>
</div>
<div class="metric">
<span>最后刷新</span>
<strong id="refreshTime">--</strong>
</div>
</section>
<section id="clientList" class="client-list" aria-live="polite"></section>
</main>
<script>
const clientList = document.querySelector("#clientList");
const totalCount = document.querySelector("#totalCount");
const secretCount = document.querySelector("#secretCount");
const refreshTime = document.querySelector("#refreshTime");
const refreshButton = document.querySelector("#refreshButton");
async function loadClients() {
refreshButton.disabled = true;
refreshButton.textContent = "刷新中";
try {
const response = await fetch("/sso/dashboard/clients", {headers: {"Accept": "application/json"}});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
renderDashboard(data);
} catch (error) {
clientList.innerHTML = `<div class="error">接入平台数据加载失败:${escapeHtml(error.message)}</div>`;
} finally {
refreshButton.disabled = false;
refreshButton.textContent = "刷新";
}
}
function renderDashboard(data) {
const clients = Array.isArray(data.clients) ? data.clients : [];
const configuredSecrets = clients.filter((client) => client.secretConfigured).length;
totalCount.textContent = String(data.total ?? clients.length);
secretCount.textContent = String(configuredSecrets);
refreshTime.textContent = formatTime(data.generatedAt);
if (clients.length === 0) {
clientList.innerHTML = `<div class="empty">当前没有配置 SSO 接入平台</div>`;
return;
}
clientList.innerHTML = clients.map(renderClient).join("");
}
function renderClient(client) {
const ready = client.status === "CONFIGURED";
const statusText = ready ? "已配置" : "待完善";
return `
<article class="client-card">
<div>
<div class="platform-name">${escapeHtml(client.name)}</div>
<div class="client-id">${escapeHtml(client.id)}</div>
</div>
<div>
<div class="label">允许地址</div>
<div class="value">${escapeHtml(client.allowUrl)}</div>
</div>
<div>
<div class="label">密钥状态</div>
<div class="value">${client.secretConfigured ? escapeHtml(client.secretPreview) : "未配置"}</div>
</div>
<div>
<span class="badge ${ready ? "ok" : "warn"}">${statusText}</span>
</div>
</article>
`;
}
function formatTime(value) {
if (!value) {
return "--";
}
return new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}).format(new Date(value));
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
loadClients();
refreshButton.addEventListener("click", loadClients);
</script>
</body>
</html>
</html>

@ -0,0 +1,45 @@
package com.ngskcloud.controller;
import com.ngskcloud.config.SsoServerClientsProperties;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class SsoDashboardControllerTest {
@Test
void listsConfiguredClientsWithMaskedSecrets() throws Exception {
SsoServerClientsProperties.Client playedu = new SsoServerClientsProperties.Client();
playedu.setClient("playedu-client");
playedu.setAllowUrl("*");
playedu.setSecretKey("SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor");
SsoServerClientsProperties properties = new SsoServerClientsProperties();
properties.setClients(new LinkedHashMap<>(Map.of("sso-client2", playedu)));
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new SsoDashboardController(properties))
.build();
mockMvc.perform(get("/sso/dashboard/clients"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.total").value(1))
.andExpect(jsonPath("$.clients[0].id").value("sso-client2"))
.andExpect(jsonPath("$.clients[0].name").value("playedu-client"))
.andExpect(jsonPath("$.clients[0].allowUrl").value("*"))
.andExpect(jsonPath("$.clients[0].secretConfigured").value(true))
.andExpect(jsonPath("$.clients[0].secretPreview").value("SSO-C2-****Kor"))
.andExpect(jsonPath("$.clients[0].status").value("CONFIGURED"))
.andExpect(content().string(not(containsString("kQwIOrYvnXmSDkwEiFngrKidMcdrg"))));
}
}
Loading…
Cancel
Save