diff --git a/pom.xml b/pom.xml
index f1ea591..53f7084 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,11 +55,11 @@
org.springframework.boot
spring-boot-starter-web
-
- com.alibaba.cloud.ai
- spring-ai-alibaba-starter-dashscope
- 1.0.0.4
-
+
+
+
+
+
io.jsonwebtoken
jjwt-api
diff --git a/src/main/java/com/yolo/keyborad/config/LLMConfig.java b/src/main/java/com/yolo/keyborad/config/LLMConfig.java
index 2d4fc0c..bf82e90 100644
--- a/src/main/java/com/yolo/keyborad/config/LLMConfig.java
+++ b/src/main/java/com/yolo/keyborad/config/LLMConfig.java
@@ -1,28 +1,42 @@
-//package com.yolo.keyborad.config;
-//
-//import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
-//import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
-//import org.springframework.beans.factory.annotation.Value;
-//import org.springframework.context.annotation.Bean;
-//import org.springframework.context.annotation.Configuration;
-//
-///*
-// * @author: ziin
-// * @date: 2025/11/11 20:37
-// */
-//@Configuration
-//public class LLMConfig {
-////
-//// @Value("${spring.ai.dashscope.api-key}")
-//// private String apiKey;
-//// @Value("${spring.ai.dashscope.chat.base-url}")
-//// private String baseUrl;
-////
-//// @Bean
-//// public DashScopeApi dashScopeApi() {
-//// return DashScopeApi.builder()
-//// .apiKey(apiKey)
-//// .baseUrl(baseUrl)
-//// .build();
-//// }
-//}
+package com.yolo.keyborad.config;
+
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/*
+ * @author: ziin
+ * @date: 2025/11/11 20:37
+ */
+@Configuration
+public class LLMConfig {
+
+ @Value("${spring.ai.openai.api-key}")
+ private String apiKey;
+ @Value("${spring.ai.openai.base-url}")
+ private String baseUrl;
+ @Value("${spring.ai.openai.chat.options.model}")
+ private String openRouterChatModel;
+
+ @Bean
+ public OpenAiApi openAiApi() {
+ return OpenAiApi.builder()
+ .apiKey(apiKey)
+ .baseUrl(baseUrl)
+ .build();
+ }
+
+ @Bean
+ public ChatClient chatClient(ChatModel geminiModel) {
+ return ChatClient.builder(geminiModel)
+ .defaultOptions(OpenAiChatOptions.builder()
+ .model(openRouterChatModel)
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/com/yolo/keyborad/controller/DemoController.java b/src/main/java/com/yolo/keyborad/controller/DemoController.java
index a7a9d46..af7a24e 100644
--- a/src/main/java/com/yolo/keyborad/controller/DemoController.java
+++ b/src/main/java/com/yolo/keyborad/controller/DemoController.java
@@ -1,23 +1,21 @@
package com.yolo.keyborad.controller;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yolo.keyborad.common.BaseResponse;
+import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.common.ResultUtils;
+import com.yolo.keyborad.exception.BusinessException;
+import com.yolo.keyborad.model.dto.IosPayVerifyReq;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
-import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
-import org.springframework.ai.chat.memory.MessageWindowChatMemory;
-import org.springframework.ai.chat.model.ChatModel;
-import org.springframework.ai.openai.OpenAiChatOptions;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.boot.context.properties.bind.DefaultValue;
+import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
/*
@@ -31,24 +29,8 @@ import reactor.core.publisher.Flux;
@Tag(name = "测试控制器", description = "测试控制器")
public class DemoController {
- private final ChatClient openAiChatClient;
-
- private final ChatModel chatModel;
-
- public DemoController(@Qualifier("openAiChatModel") ChatModel chatModel) {
-
- this.chatModel = chatModel;
-
- // 构造时,可以设置 ChatClient 的参数
- // {@link org.springframework.ai.chat.client.ChatClient};
- this.openAiChatClient = ChatClient.builder(chatModel)
- .defaultOptions(OpenAiChatOptions.builder()
- .build())
- // 实现 Chat Memory 的 Advisor
- .build();
- }
-
-
+ @Resource
+ private ChatClient client;
@GetMapping("/test")
@Operation(summary = "测试接口", description = "测试接口")
@@ -60,11 +42,20 @@ public class DemoController {
@GetMapping("/talk")
@Operation(summary = "测试聊天接口", description = "测试接口")
- public Flux testTalk(String userInput){
- return openAiChatClient
+ @Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something")
+ public Flux testTalk(@DefaultValue("you are so cute!") String userInput){
+ return client
.prompt("You're a 25-year-old guy—witty and laid-back, always replying in English.\n" +
"Read the user's last message, then write three short and funny replies that sound like something a guy would say. Go easy on the emojis.\n" +
- "Keep each under 20 words. Use '/t' to separate them—no numbers or extra labels.").user("you are so cute!")
- .stream().content();
+ "Keep each under 20 words. Use '/t' to separate them—no numbers or extra labels.")
+ .user(userInput)
+ .stream()
+ .content();
+ }
+
+ @Operation(summary = "IOS内购凭证校验", description = "IOS内购凭证校验")
+ public BaseResponse iosPay(@RequestBody IosPayVerifyReq req) {
+
+ return null;
}
}
diff --git a/src/main/java/com/yolo/keyborad/controller/UserController.java b/src/main/java/com/yolo/keyborad/controller/UserController.java
index e1c47bd..c418a93 100644
--- a/src/main/java/com/yolo/keyborad/controller/UserController.java
+++ b/src/main/java/com/yolo/keyborad/controller/UserController.java
@@ -1,12 +1,18 @@
package com.yolo.keyborad.controller;
import com.alibaba.fastjson.JSON;
+import com.yolo.keyborad.common.BaseResponse;
+import com.yolo.keyborad.common.ResultUtils;
+import com.yolo.keyborad.model.dto.AppleLoginReq;
import com.yolo.keyborad.service.impl.IAppleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
+import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
@@ -30,11 +36,13 @@ public class UserController {
/**
* 苹果登录
*
- * @param code JWT
+ * @param appleLoginReq identityToken
*/
@PostMapping("/appleLogin")
- public String appleLogin(String code) throws Exception {
- String subjectId = appleService.login(code);
- return JSON.toJSONString(subjectId);
+ @Operation(summary = "苹果登录", description = "苹果登录接口")
+ @Parameter(name = "code",required = true,description = "苹果登录凭证",example = "123456")
+ public BaseResponse appleLogin(@RequestBody AppleLoginReq appleLoginReq) throws Exception {
+ String subjectId = appleService.login(appleLoginReq.getIdentityToken());
+ return ResultUtils.success(subjectId);
}
}
diff --git a/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java
index 41b32be..5b8b59a 100644
--- a/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java
+++ b/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java
@@ -6,7 +6,6 @@ import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
@@ -14,6 +13,7 @@ import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
+import java.util.Base64;
import java.util.Objects;
/**
@@ -27,8 +27,6 @@ import java.util.Objects;
@AllArgsConstructor
public class AppleServiceImpl implements IAppleService {
-
-
/**
* 登录
*
@@ -36,30 +34,46 @@ public class AppleServiceImpl implements IAppleService {
*/
@Override
public String login(String identityToken) throws Exception {
- // 从identityToken中获取头部和载荷
- String firstDate;
- String claim;
- firstDate = new String(Base64.decodeBase64(identityToken.split("\\.")[0]), StandardCharsets.UTF_8);
- claim = new String(Base64.decodeBase64(identityToken.split("\\.")[1]), StandardCharsets.UTF_8);
- // 开发者帐户中获取的 10 个字符的标识符密钥
- String kid = JSONObject.parseObject(firstDate).get("kid").toString();
- String aud = JSONObject.parseObject(claim).get("aud").toString();
- String sub = JSONObject.parseObject(claim).get("sub").toString();
-
- // 获取公钥
- PublicKey publicKey = this.getPublicKey(kid);
- if (Objects.isNull(publicKey)) {
- throw new RuntimeException("apple授权登录的数据异常");
+ // 1. 清理一下 token,防止前后多了引号/空格
+ identityToken = identityToken.trim();
+ if (identityToken.startsWith("\"") && identityToken.endsWith("\"")) {
+ identityToken = identityToken.substring(1, identityToken.length() - 1);
}
- // 验证Apple登录的JWT令牌
+ // 2. 拆分三段 & 使用 Base64URL 解码
+ String[] parts = identityToken.split("\\.");
+ if (parts.length != 3) {
+ throw new RuntimeException("非法的 identityToken,JWT 结构不正确");
+ }
+
+ Base64.Decoder urlDecoder = Base64.getUrlDecoder();
+
+ String headerJson = new String(urlDecoder.decode(parts[0]), StandardCharsets.UTF_8);
+ String payloadJson = new String(urlDecoder.decode(parts[1]), StandardCharsets.UTF_8);
+
+ JSONObject header = JSONObject.parseObject(headerJson);
+ JSONObject payload = JSONObject.parseObject(payloadJson);
+
+ // 开发者帐户中获取的 10 个字符的标识符密钥
+ String kid = header.getString("kid");
+ String aud = payload.getString("aud");
+ String sub = payload.getString("sub");
+
+ // 3. 获取公钥
+ PublicKey publicKey = this.getPublicKey(kid);
+ if (Objects.isNull(publicKey)) {
+ throw new RuntimeException("apple授权登录的公钥获取失败,kid=" + kid);
+ }
+
+ // 4. 验证Apple登录的JWT令牌
boolean result = this.verifyAppleLoginCode(publicKey, identityToken, aud, sub);
// 返回用户标识符
if (result) {
return sub;
}
+
return null;
}
@@ -68,28 +82,45 @@ public class AppleServiceImpl implements IAppleService {
*
* @param publicKey 公钥
* @param identityToken JWT身份令牌
- * @param aud 受众
+ * @param aud 受众(目前你从 token 里拿出来的)
* @param sub 用户标识符
* @return 验证结果
*/
private boolean verifyAppleLoginCode(PublicKey publicKey, String identityToken, String aud, String sub) {
boolean result = false;
String appleIssUrl = "https://appleid.apple.com";
- JwtParser jwtParser = Jwts.parserBuilder()
- .setSigningKey(publicKey)
- .requireIssuer(aud)
- .requireSubject(sub)
- .requireIssuer(appleIssUrl).build();
+
try {
+ JwtParser jwtParser = Jwts.parserBuilder()
+ .setSigningKey(publicKey)
+ .requireIssuer(appleIssUrl) // ✅ iss 固定校验 apple
+ .requireSubject(sub) // ✅ 校验 sub
+ // .requireAudience(expectedAud) // 更严谨做法:用你服务端配置的 client_id 校验
+ .build();
+
Jws claim = jwtParser.parseClaimsJws(identityToken);
- if (claim != null && claim.getBody().containsKey("auth_time")) {
+
+ Claims body = claim.getBody();
+
+ // 可选:再手动对 aud 做一层检查(现在 aud 是从 payload 里读出来的)
+ String tokenAud = body.getAudience();
+ if (!Objects.equals(tokenAud, aud)) {
+ log.warn("apple identityToken aud 不一致, tokenAud={}, inputAud={}", tokenAud, aud);
+ // 这里看你要不要直接抛异常
+ }
+
+ if (body.containsKey("auth_time")) {
result = true;
}
} catch (ExpiredJwtException e) {
log.error("error identity expire time out", e);
throw new RuntimeException("apple登录授权identityToken过期", e);
+ } catch (SignatureException e) {
+ // ✅ 专门打一下签名错误日志,便于你确认是不是 key 对不上
+ log.error("apple identityToken 签名校验失败", e);
+ throw new RuntimeException("apple登录授权identityToken签名非法", e);
} catch (SecurityException e) {
- log.error("error identity illegal", e);
+ log.error("apple identityToken 安全校验失败", e);
throw new RuntimeException("apple登录授权identityToken非法", e);
}
return result;
@@ -106,23 +137,30 @@ public class AppleServiceImpl implements IAppleService {
String result = HttpUtil.get(appleAuthUrl);
JSONObject content = JSONObject.parseObject(result);
- String keys = content.getString("keys");
- JSONArray keysArray = JSONObject.parseArray(keys);
- if (keysArray.isEmpty()) {
+
+ // ✅ 直接拿 JSONArray,之前先 getString 再 parse 容易搞错类型
+ JSONArray keysArray = content.getJSONArray("keys");
+ if (keysArray == null || keysArray.isEmpty()) {
return null;
}
- for (Object key : keysArray) {
- JSONObject keyJsonObject = (JSONObject) key;
+
+ Base64.Decoder urlDecoder = Base64.getUrlDecoder();
+
+ for (int i = 0; i < keysArray.size(); i++) {
+ JSONObject keyJsonObject = keysArray.getJSONObject(i);
if (kid.equals(keyJsonObject.getString("kid"))) {
String n = keyJsonObject.getString("n");
String e = keyJsonObject.getString("e");
- BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n));
- BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e));
+
+ BigInteger modulus = new BigInteger(1, urlDecoder.decode(n)); // ✅ Base64URL
+ BigInteger publicExponent = new BigInteger(1, urlDecoder.decode(e));
+
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
- KeyFactory kf = KeyFactory.getInstance("RSA");
+ KeyFactory kf = KeyFactory.getInstance("RSA"); // ✅ 和 kty=RSA / alg=RS256 对应
return kf.generatePublic(spec);
}
}
+ log.warn("未在 Apple JWK 中找到匹配 kid 的公钥,kid={}", kid);
return null;
}
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 06ea808..5c04d24 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -6,6 +6,9 @@ spring:
chat:
options:
model: google/gemini-2.5-flash-lite
+ embedding:
+ options:
+ model: qwen/qwen3-embedding-8b
dashscope:
api-key: 11
application:
@@ -14,7 +17,7 @@ spring:
active: dev
datasource:
driver-class-name: org.postgresql.Driver
- url: jdbc:postgresql://localhost:5432/postgres
+ url: jdbc:postgresql://localhost:5432/keyborad_db
username: root
password: 123asd
mvc: