diff --git a/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java b/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java
index 07ccad95..1bf490e3 100644
--- a/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java
+++ b/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java
@@ -50,7 +50,8 @@ public class AuthService {
.eq(UserEntity::getUsername, request.getUsername())
.eq(UserEntity::getEnabled, true));
- if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
+ if (user == null || user.getPassword() == null
+ || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new MateClawException("err.auth.invalid_credentials", 401, "用户名或密码错误");
}
@@ -212,7 +213,10 @@ public class AuthService {
return userMapper.selectById(userId);
}
- private String generateToken(UserEntity user) {
+ /**
+ * 生成 JWT token。SSO 登录路径复用此方法签发格式一致的 token。
+ */
+ public String generateToken(UserEntity user) {
return Jwts.builder()
.subject(user.getUsername())
.claim("userId", user.getId())
diff --git a/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoAutoConfiguration.java b/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoAutoConfiguration.java
new file mode 100644
index 00000000..e361201a
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoAutoConfiguration.java
@@ -0,0 +1,33 @@
+package vip.mate.auth.sso;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import vip.mate.auth.sso.provider.FeishuSsoProvider;
+
+/**
+ * SSO 配置。启用 {@link SsoProperties} 绑定 + 按需注册飞书 Provider。
+ *
+ * 仅当 {@code mateclaw.sso.enabled=true} 时此配置生效。飞书 Provider 进一步要求
+ * {@code mateclaw.sso.feishu.enabled=true}。
+ *
+ * @author MateClaw Team
+ */
+@Configuration
+@EnableScheduling
+@EnableConfigurationProperties(SsoProperties.class)
+@ConditionalOnProperty(name = "mateclaw.sso.enabled", havingValue = "true")
+public class SsoAutoConfiguration {
+
+ /**
+ * 飞书 SSO Provider。仅当飞书 SSO 启用时注册。
+ */
+ @Bean
+ @ConditionalOnProperty(name = "mateclaw.sso.feishu.enabled", havingValue = "true")
+ public FeishuSsoProvider feishuSsoProvider(SsoProperties ssoProperties, ObjectMapper objectMapper) {
+ return new FeishuSsoProvider(ssoProperties.getFeishu(), objectMapper);
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoCallbackResponse.java b/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoCallbackResponse.java
new file mode 100644
index 00000000..ca81a91a
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoCallbackResponse.java
@@ -0,0 +1,43 @@
+package vip.mate.auth.sso;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import vip.mate.auth.model.LoginResponse;
+
+/**
+ * SSO 回调响应。两种互斥形态由 {@code bindRequired} 区分:
+ *
+ * - {@code bindRequired=false}: 登录成功, {@code loginResponse} 携带 JWT
+ * - {@code bindRequired=true}: link-only 模式未绑定, {@code bindToken} 供前端引导绑定
+ *
+ *
+ * 替代了原先用 {@code R.fail(200, Map.toString())} 传递绑定信号的 hack。
+ *
+ * @author MateClaw Team
+ */
+@Data
+@AllArgsConstructor
+public class SsoCallbackResponse {
+
+ /** link-only 模式下未绑定时为 true */
+ private boolean bindRequired;
+
+ /** 登录成功时非空 */
+ private LoginResponse loginResponse;
+
+ /** bindRequired=true 时非空, 供前端调 /sso/bind */
+ private String bindToken;
+
+ private String provider;
+ private String displayName;
+
+ /** 登录成功响应工厂 */
+ public static SsoCallbackResponse of(LoginResponse loginResponse) {
+ return new SsoCallbackResponse(false, loginResponse, null, null, null);
+ }
+
+ /** 需绑定响应工厂 (link-only 模式) */
+ public static SsoCallbackResponse bindRequired(String bindToken, String provider, String displayName) {
+ return new SsoCallbackResponse(true, null, bindToken, provider, displayName);
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoController.java b/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoController.java
new file mode 100644
index 00000000..ee4bc3cd
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/auth/sso/SsoController.java
@@ -0,0 +1,70 @@
+package vip.mate.auth.sso;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import vip.mate.auth.model.LoginResponse;
+import vip.mate.auth.sso.provider.SsoProviderRegistry;
+import vip.mate.common.result.R;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * SSO 单点登录 HTTP 端点。全部 permitAll (与 /auth/login 同级)。
+ *
+ * @author MateClaw Team
+ */
+@Tag(name = "SSO 单点登录")
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/auth/sso")
+@RequiredArgsConstructor
+public class SsoController {
+
+ private final SsoProviderRegistry registry;
+ private final SsoService ssoService;
+
+ @Operation(summary = "列出已启用的 SSO Provider")
+ @GetMapping("/providers")
+ public R>> providers() {
+ List