feat(core): 集成Spring AI与Apple登录并新增聊天接口
This commit is contained in:
27
pom.xml
27
pom.xml
@@ -60,6 +60,33 @@
|
|||||||
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
||||||
<version>1.0.0.4</version>
|
<version>1.0.0.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
<version>2.0.58</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||||
|
<version>${spring-ai.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-aop</artifactId>
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
|||||||
28
src/main/java/com/yolo/keyborad/config/LLMConfig.java
Normal file
28
src/main/java/com/yolo/keyborad/config/LLMConfig.java
Normal file
@@ -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();
|
||||||
|
//// }
|
||||||
|
//}
|
||||||
@@ -34,7 +34,9 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
// 你的其他放行路径,例如登录接口
|
// 你的其他放行路径,例如登录接口
|
||||||
"/demo/test",
|
"/demo/test",
|
||||||
"/error"
|
"/error",
|
||||||
|
"/demo/talk",
|
||||||
|
"/user/appleLogin"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ import com.yolo.keyborad.common.ResultUtils;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -22,9 +31,40 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@Tag(name = "测试控制器", description = "测试控制器")
|
@Tag(name = "测试控制器", description = "测试控制器")
|
||||||
public class DemoController {
|
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")
|
@GetMapping("/test")
|
||||||
@Operation(summary = "测试接口", description = "测试接口")
|
@Operation(summary = "测试接口", description = "测试接口")
|
||||||
public BaseResponse<String> testDemo(){
|
public BaseResponse<String> testDemo(){
|
||||||
return ResultUtils.success("hello world");
|
return ResultUtils.success("hello world");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/talk")
|
||||||
|
@Operation(summary = "测试聊天接口", description = "测试接口")
|
||||||
|
public Flux<String> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Claims> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
spring:
|
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:
|
application:
|
||||||
name: keyborad-backend
|
name: keyborad-backend
|
||||||
profiles:
|
profiles:
|
||||||
@@ -16,14 +25,20 @@ spring:
|
|||||||
timeout: 86400
|
timeout: 86400
|
||||||
store-type: redis
|
store-type: redis
|
||||||
# redis 配置
|
# redis 配置
|
||||||
redis:
|
data:
|
||||||
port: 6379
|
redis:
|
||||||
host: localhost
|
port: 6379
|
||||||
database: 0
|
host: localhost
|
||||||
|
database: 0
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 7529
|
port: 7529
|
||||||
servlet:
|
servlet:
|
||||||
context-path: /api
|
context-path: /api
|
||||||
|
encoding:
|
||||||
|
charset: UTF-8
|
||||||
|
force: true
|
||||||
|
enabled: true
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: false
|
map-underscore-to-camel-case: false
|
||||||
|
|||||||
Reference in New Issue
Block a user