diff --git a/.env.release.example b/.env.release.example index a6eadf21c..0ad0843f2 100644 --- a/.env.release.example +++ b/.env.release.example @@ -79,6 +79,18 @@ SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_SCOPE=openid,profile,email SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_NAME=OIDC SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI= +# Optional: CAS-based SSO login (private deployments). +# After enabling, redirect users to /api/v1/auth/sso/login to initiate login. +# Register the application on the SSO admin console to obtain client-url and client-token. +# The response fields default to "account", "id", "name" — override if your SSO uses different JSON keys. +SKILLHUB_AUTH_SSO_ENABLED=false +SKILLHUB_AUTH_SSO_BASE_URL= +SKILLHUB_AUTH_SSO_VALIDATE_PATH= +SKILLHUB_AUTH_SSO_CLIENT_URL= +SKILLHUB_AUTH_SSO_CLIENT_TOKEN= +# Frontend runtime config: shows the "Enterprise SSO Login" button when enabled. +SKILLHUB_WEB_AUTH_SSO_ENABLED=false + # SMTP configuration for password reset verification emails. SPRING_MAIL_HOST= SPRING_MAIL_PORT=587 diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/SsoLoginController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/SsoLoginController.java new file mode 100644 index 000000000..733f59247 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/SsoLoginController.java @@ -0,0 +1,94 @@ +package com.iflytek.skillhub.controller; + +import java.io.IOException; + +import com.iflytek.skillhub.auth.config.SsoProperties; +import com.iflytek.skillhub.auth.session.PlatformSessionService; +import com.iflytek.skillhub.auth.sso.SsoClient; +import com.iflytek.skillhub.auth.sso.SsoIdentityService; +import com.iflytek.skillhub.auth.sso.SsoUser; +import com.iflytek.skillhub.auth.sso.TicketValidationException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * CAS-based SSO login controller. + * + *
Handles the redirect to the SSO server and the ticket-callback exchange
+ * that establishes a platform session on success.
+ */
+@Controller
+@RequestMapping("/api/v1/auth/sso")
+public class SsoLoginController {
+
+ private static final Logger log = LoggerFactory.getLogger(SsoLoginController.class);
+
+ private final SsoProperties properties;
+ private final SsoClient ssoClient;
+ private final SsoIdentityService ssoIdentityService;
+ private final PlatformSessionService platformSessionService;
+
+ public SsoLoginController(SsoProperties properties,
+ SsoClient ssoClient,
+ SsoIdentityService ssoIdentityService,
+ PlatformSessionService platformSessionService) {
+ this.properties = properties;
+ this.ssoClient = ssoClient;
+ this.ssoIdentityService = ssoIdentityService;
+ this.platformSessionService = platformSessionService;
+ }
+
+ /**
+ * Initiates SSO login by redirecting the browser to the SSO login page.
+ */
+ @GetMapping("/login")
+ public void ssoLogin(HttpServletResponse response) throws IOException {
+ if (!properties.isEnabled()) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "SSO login is disabled");
+ return;
+ }
+ String ssoLoginUrl = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl())
+ .path("/login")
+ .queryParam("clientUrl", properties.getClientUrl())
+ .build()
+ .toUriString();
+ response.sendRedirect(ssoLoginUrl);
+ }
+
+ /**
+ * Receives the CAS ticket callback from the SSO server, validates the
+ * ticket, establishes a platform session, and redirects the browser to the
+ * frontend home page.
+ */
+ @GetMapping("/callback")
+ public void ssoCallback(@RequestParam("ticket") String ticket,
+ HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+ if (!properties.isEnabled()) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "SSO login is disabled");
+ return;
+ }
+
+ try {
+ SsoUser ssoUser = ssoClient.validateTicket(ticket);
+ var principal = ssoIdentityService.resolveOrCreate(ssoUser);
+ platformSessionService.establishSession(principal, request);
+ response.sendRedirect("/");
+ } catch (TicketValidationException e) {
+ log.warn("SSO ticket validation failed: {}", e.getMessage());
+ response.sendRedirect("/login?error=sso_auth_failed");
+ } catch (Exception e) {
+ log.error("SSO callback error", e);
+ response.sendRedirect("/login?error=sso_error");
+ }
+ }
+}
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SsoProperties.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SsoProperties.java
new file mode 100644
index 000000000..2ccef5cb0
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SsoProperties.java
@@ -0,0 +1,60 @@
+package com.iflytek.skillhub.auth.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "skillhub.auth.sso")
+public class SsoProperties {
+
+ /** Kept disabled in OSS by default. */
+ private boolean enabled = false;
+
+ /** SSO server base URL. */
+ private String baseUrl;
+
+ /** Ticket validation endpoint path on SSO server. */
+ private String validatePath;
+
+ /** Client URL registered in SSO (used as callback base). */
+ private String clientUrl;
+
+ /** Client token registered in SSO, used for logout API calls. */
+ private String clientToken;
+
+ /** Response field mapping for SSO user info JSON. */
+ private ResponseFields response = new ResponseFields();
+
+ public boolean isEnabled() { return enabled; }
+ public void setEnabled(boolean enabled) { this.enabled = enabled; }
+
+ public String getBaseUrl() { return baseUrl; }
+ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
+
+ public String getValidatePath() { return validatePath; }
+ public void setValidatePath(String validatePath) { this.validatePath = validatePath; }
+
+ public String getClientUrl() { return clientUrl; }
+ public void setClientUrl(String clientUrl) { this.clientUrl = clientUrl; }
+
+ public String getClientToken() { return clientToken; }
+ public void setClientToken(String clientToken) { this.clientToken = clientToken; }
+
+ public ResponseFields getResponse() { return response; }
+ public void setResponse(ResponseFields response) { this.response = response; }
+
+ public static class ResponseFields {
+ private String accountField = "account";
+ private String idField = "id";
+ private String nameField = "name";
+
+ public String getAccountField() { return accountField; }
+ public void setAccountField(String accountField) { this.accountField = accountField; }
+
+ public String getIdField() { return idField; }
+ public void setIdField(String idField) { this.idField = idField; }
+
+ public String getNameField() { return nameField; }
+ public void setNameField(String nameField) { this.nameField = nameField; }
+ }
+}
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java
index 10a14e085..43df8d5a9 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java
@@ -27,6 +27,7 @@ public class RouteSecurityPolicyRegistry {
RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/direct/login"),
RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/local/**"),
RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/device/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/sso/**"),
RouteAuthorizationPolicy.permitAll(null, "/api/v1/check"),
RouteAuthorizationPolicy.permitAll(null, "/actuator/health"),
RouteAuthorizationPolicy.permitAll(null, "/v3/api-docs/**"),
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/sso/SsoClient.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/sso/SsoClient.java
new file mode 100644
index 000000000..afa770d4a
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/sso/SsoClient.java
@@ -0,0 +1,64 @@
+package com.iflytek.skillhub.auth.sso;
+
+import java.util.Map;
+
+import com.iflytek.skillhub.auth.config.SsoProperties;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Client that validates a CAS service-ticket against the SSO server and
+ * returns the associated user identity.
+ */
+@Service
+public class SsoClient {
+
+ private final SsoProperties properties;
+ private final RestTemplate restTemplate;
+
+ public SsoClient(SsoProperties properties, RestTemplateBuilder restTemplateBuilder) {
+ this.properties = properties;
+ this.restTemplate = restTemplateBuilder.build();
+ }
+
+ /**
+ * Validates a CAS service ticket and returns the resolved user identity,
+ * or throws {@link TicketValidationException} when the ticket is invalid
+ * or the SSO server is unreachable.
+ */
+ public SsoUser validateTicket(String ticket) {
+ var request = Map.of(
+ "Ticket", ticket,
+ "Url", properties.getClientUrl()
+ );
+ var validateUrl = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl())
+ .path(properties.getValidatePath())
+ .build()
+ .toUriString();
+
+ @SuppressWarnings("unchecked")
+ Map