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