diff --git a/pom.xml b/pom.xml
index 4493263..f1ea591 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,6 +60,33 @@
spring-ai-alibaba-starter-dashscope
1.0.0.4
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ com.alibaba
+ fastjson
+ 2.0.58
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
+ org.springframework.ai
+ spring-ai-starter-model-openai
+ ${spring-ai.version}
+
org.springframework.boot
spring-boot-starter-aop
diff --git a/src/main/java/com/yolo/keyborad/config/LLMConfig.java b/src/main/java/com/yolo/keyborad/config/LLMConfig.java
new file mode 100644
index 0000000..2d4fc0c
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/config/LLMConfig.java
@@ -0,0 +1,28 @@
+//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();
+//// }
+//}
diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java
index 5157edf..13f730b 100644
--- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java
+++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java
@@ -34,7 +34,9 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/favicon.ico",
// 你的其他放行路径,例如登录接口
"/demo/test",
- "/error"
+ "/error",
+ "/demo/talk",
+ "/user/appleLogin"
};
}
@Bean
diff --git a/src/main/java/com/yolo/keyborad/controller/DemoController.java b/src/main/java/com/yolo/keyborad/controller/DemoController.java
index 09e9e86..a7a9d46 100644
--- a/src/main/java/com/yolo/keyborad/controller/DemoController.java
+++ b/src/main/java/com/yolo/keyborad/controller/DemoController.java
@@ -5,11 +5,20 @@ import com.yolo.keyborad.common.ResultUtils;
import io.swagger.v3.oas.annotations.Operation;
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 reactor.core.publisher.Flux;
/*
* @author: ziin
@@ -22,9 +31,40 @@ import org.springframework.web.bind.annotation.RestController;
@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();
+ }
+
+
+
@GetMapping("/test")
@Operation(summary = "测试接口", description = "测试接口")
public BaseResponse testDemo(){
return ResultUtils.success("hello world");
}
+
+
+
+ @GetMapping("/talk")
+ @Operation(summary = "测试聊天接口", description = "测试接口")
+ public Flux testTalk(String userInput){
+ return openAiChatClient
+ .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();
+ }
}
diff --git a/src/main/java/com/yolo/keyborad/controller/UserController.java b/src/main/java/com/yolo/keyborad/controller/UserController.java
new file mode 100644
index 0000000..e1c47bd
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/controller/UserController.java
@@ -0,0 +1,40 @@
+package com.yolo.keyborad.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.yolo.keyborad.service.impl.IAppleService;
+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.web.bind.annotation.*;
+
+import java.util.Objects;
+
+/**
+ * 用户前端控制器
+ *
+ * @author Salmon
+ * @since 2024-07-20
+ */
+@RestController
+@Slf4j
+@RequestMapping("/user")
+@Tag(name = "用户")
+@AllArgsConstructor
+public class UserController {
+
+ @Resource
+ private IAppleService appleService;
+
+ /**
+ * 苹果登录
+ *
+ * @param code JWT
+ */
+ @PostMapping("/appleLogin")
+ public String appleLogin(String code) throws Exception {
+ String subjectId = appleService.login(code);
+ return JSON.toJSONString(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
new file mode 100644
index 0000000..41b32be
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/service/impl/AppleServiceImpl.java
@@ -0,0 +1,128 @@
+package com.yolo.keyborad.service.impl;
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONArray;
+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;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Objects;
+
+/**
+ * Apple相关API实现
+ *
+ * @author Salmon
+ * @since 2024-07-20
+ */
+@Service
+@Slf4j
+@AllArgsConstructor
+public class AppleServiceImpl implements IAppleService {
+
+
+
+ /**
+ * 登录
+ *
+ * @param identityToken JWT身份令牌
+ */
+ @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授权登录的数据异常");
+ }
+
+ // 验证Apple登录的JWT令牌
+ boolean result = this.verifyAppleLoginCode(publicKey, identityToken, aud, sub);
+
+ // 返回用户标识符
+ if (result) {
+ return sub;
+ }
+ return null;
+ }
+
+ /**
+ * 验证Apple登录的JWT令牌
+ *
+ * @param publicKey 公钥
+ * @param identityToken JWT身份令牌
+ * @param aud 受众
+ * @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 {
+ Jws claim = jwtParser.parseClaimsJws(identityToken);
+ if (claim != null && claim.getBody().containsKey("auth_time")) {
+ result = true;
+ }
+ } catch (ExpiredJwtException e) {
+ log.error("error identity expire time out", e);
+ throw new RuntimeException("apple登录授权identityToken过期", e);
+ } catch (SecurityException e) {
+ log.error("error identity illegal", e);
+ throw new RuntimeException("apple登录授权identityToken非法", e);
+ }
+ return result;
+ }
+
+ /**
+ * 获取公钥
+ *
+ * @param kid 标识符密钥
+ * @return 公钥
+ */
+ private PublicKey getPublicKey(String kid) throws Exception {
+ String appleAuthUrl = "https://appleid.apple.com/auth/keys";
+
+ String result = HttpUtil.get(appleAuthUrl);
+ JSONObject content = JSONObject.parseObject(result);
+ String keys = content.getString("keys");
+ JSONArray keysArray = JSONObject.parseArray(keys);
+ if (keysArray.isEmpty()) {
+ return null;
+ }
+ for (Object key : keysArray) {
+ JSONObject keyJsonObject = (JSONObject) key;
+ 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));
+ RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ return kf.generatePublic(spec);
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/yolo/keyborad/service/impl/IAppleService.java b/src/main/java/com/yolo/keyborad/service/impl/IAppleService.java
new file mode 100644
index 0000000..2263812
--- /dev/null
+++ b/src/main/java/com/yolo/keyborad/service/impl/IAppleService.java
@@ -0,0 +1,17 @@
+package com.yolo.keyborad.service.impl;
+
+/**
+ * Apple相关API
+ *
+ * @author Salmon
+ * @since 2024-07-20
+ */
+public interface IAppleService {
+
+ /**
+ * 登录
+ *
+ * @param identityToken JWT身份令牌
+ */
+ String login(String identityToken) throws Exception;
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 2b2b487..06ea808 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,4 +1,13 @@
spring:
+ ai:
+ openai:
+ api-key: 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
+ base-url: https://openrouter.ai/api/
+ chat:
+ options:
+ model: google/gemini-2.5-flash-lite
+ dashscope:
+ api-key: 11
application:
name: keyborad-backend
profiles:
@@ -16,14 +25,20 @@ spring:
timeout: 86400
store-type: redis
# redis 配置
- redis:
- port: 6379
- host: localhost
- database: 0
+ data:
+ redis:
+ port: 6379
+ host: localhost
+ database: 0
+
server:
port: 7529
servlet:
context-path: /api
+ encoding:
+ charset: UTF-8
+ force: true
+ enabled: true
mybatis-plus:
configuration:
map-underscore-to-camel-case: false