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: