技术选型
spring-ai更新太快,api不统一,截止到目前为止,不适合企业开发
目前企业开发中,还是使用 dashscope-sdk-java 的 2.16.x 版本更多
依赖
排除 slf4j-simple 依赖,否则启动有警告或报错
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.16.10</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
大模型配置
配置 api-key 和 模型名称
# DashScope AI配置
dashscope:
api-key: your-api-key-here
model: qwen-turbo
授权过滤
如果使用 流式回答,必须放开授权,否则会被拦截
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF 保护,因为我们使用 JWT 认证
.csrf(AbstractHttpConfigurer::disable)
// 配置请求授权
.authorizeHttpRequests(authorize -> authorize
// 允许登录接口匿名访问
.requestMatchers("/api/users/login").permitAll()
.requestMatchers("/app/login").permitAll()
// 放行 ai 的url
.requestMatchers("/api/ai/stream").permitAll()
// 其他所有请求都需要认证
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Controller实现
package com.smartagriculture.controller;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.common.Role;
import com.smartagriculture.utils.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.ArrayList;
import java.util.Collections;
/**
* AI助手控制器
*/
@RestController
@RequestMapping("/api/ai")
public class AIController {
private static final Logger logger = LoggerFactory.getLogger(AIController.class);
private static final String PROMPT_SUFFIX = "不要用markdown格式";
@Autowired
private JwtUtil jwtUtil;
@Value("${dashscope.api-key}")
private String configApiKey;
@Value("${dashscope.model}")
private String model;
@GetMapping("/stream")
public SseEmitter streamAIResponse(@RequestParam String prompt,
@RequestHeader(value = "Authorization", required = false) String tokenHeader) {
logger.info("收到AI请求,prompt: {}", prompt);
SseEmitter emitter = new SseEmitter();
// ======================
// 手动鉴权(安全核心)
// ======================
int check = check(tokenHeader);
if (check == -1) {
emitter.completeWithError(new RuntimeException("无权限访问"));
return emitter;
}else if (check == -2) {
emitter.completeWithError(new RuntimeException("token 无效"));
return emitter;
}
// 异步线程处理
new Thread(() -> {
try {
GenerationParam param = GenerationParam.builder()
.apiKey(configApiKey)
.model(model)
.messages(Collections.singletonList(
Message.builder()
.role(Role.USER.getValue())
.content(prompt + PROMPT_SUFFIX)
.build()
))
.incrementalOutput(true)
.build();
// 🔥 使用独立的回调类(无Lambda)
Generation generation = new Generation();
generation.streamCall(param, new AiStreamResultCallback(emitter));
} catch (Exception e) {
logger.error("AI接口调用失败", e);
emitter.completeWithError(e);
}
}).start();
return emitter;
}
private int check(String token){
logger.debug("JWT Filter: Authorization header = " + token);
// 检查 token 是否存在且格式正确
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 去除 "Bearer " 前缀
logger.debug("JWT Filter: Token = " + token);
// 验证 token
if (jwtUtil.validateToken(token)) {
// 从 token 中获取用户名
String username = jwtUtil.getUsernameFromToken(token);
logger.debug("JWT Filter: Username from token = " + username);
// 创建用户详情对象
UserDetails userDetails = new User(username, "", new ArrayList<>());
// 创建认证对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// 设置认证信息到 SecurityContext
return 0;
} else {
logger.info("JWT Filter: Invalid token");
return -2;
}
} else {
return -1;
}
}
public static class AiStreamResultCallback extends ResultCallback<GenerationResult> {
private final SseEmitter emitter;
public AiStreamResultCallback(SseEmitter emitter) {
this.emitter = emitter;
}
/**
* 流式接收消息 🔥 2.16 标准写法
*/
@Override
public void onEvent(GenerationResult result) {
try {
if (result == null || result.getOutput() == null) {
return;
}
// 企业标准写法,永不null
String content = result.getOutput().getText();
if (content != null && !content.isEmpty()) {
logger.warn(content);
emitter.send(content);
}
} catch (Exception e) {
emitter.completeWithError(e);
}
}
/**
* 流式完成
*/
@Override
public void onComplete() {
try {
emitter.send("[DONE]");
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}
/**
* 异常处理
*/
@Override
public void onError(Exception e) {
logger.error("AI流式回调异常", e);
emitter.completeWithError(e);
}
}
}