java实现coze平台鉴权+工作流调用(踩坑记录)
问题偏多建议大家看完文章后再开始实现
OAuth鉴权
https://www.coze.cn/open/docs/developer_guides/preparation
https://www.coze.cn/open/docs/developer_guides/oauth_apps
OAuth 授权码鉴权
https://www.coze.cn/open/docs/developer_guides/oauth_code
创建OAuth应用
https://www.coze.cn/open/oauth/apps
获取访问令牌
https://www.coze.cn/api/permission/oauth2/authorize?response_type=code&client_id=22***替换为自己的.app.coze&redirect_uri=http://localhost:48080/admin-api/ai/coze/oauth&state=
最后的state=
无论使用与否都需要携带
这里如果本地的接口写好的话,他会直接请求接口做后续逻辑,我这里测试就一切从简
存好url中的code= 后面的授权码
获取 OAuth Access Token
postman访问
https://api.coze.cn/api/permission/oauth2/token
Authorization Bearer
{
"grant_type":"authorization_code",
"code":"code_",
"redirect_uri":"http://localhost:48080/admin-api/ai/coze/oauth"
,"client_id":""}
以上报错是code有问题,重新授权拿新的授权码再试就成功了
coze配置文件
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* @author YXY
* @date 2025-03-24
* @description coze配置文件
*/
@Data
@RefreshScope
public class CozeProperties {
@Value("${coze.clientSecret}")
private String clientSecret;
@Value("${coze.grantType}")
private String grantType;
@Value("${coze.code}")
private String code;
@Value("${coze.clientId}")
private String clientId;
@Value("${coze.redirectUri}")
private String redirectUri;
@Value("${coze.oAuthAccessTokenUri}")
private String oAuthAccessTokenUri;
}
配置文件
coze:
clientSecret: 创建 OAuth 应用时获取的客户端密钥
grantType: authorization_code
code: 授权码
clientId: 创建 OAuth 应用时获取的客户端 ID。
redirectUri: 创建 OAuth 应用时指定的重定向 URL。
oAuthAccessTokenUri: https://api.coze.cn/api/permission/oauth2/token
import com.alibaba.fastjson.JSONObject;
import com.goodsoft.shrk.module.ai.controller.admin.coze.vo.CozeAuthRespVo;
import com.goodsoft.shrk.module.ai.enums.coze.CozeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @author YXY
* @date 2025-03-24
* @description coze API
*
*/
@Component
@Slf4j
public class CozeApiClient {
private static final String MEDIA_TYPE_JSON = "application/json; charset=utf-8";
private final static OkHttpClient client = new OkHttpClient().newBuilder()
.connectionPool(new ConnectionPool(100, 5, TimeUnit.MINUTES))
.readTimeout(60 * 10, TimeUnit.SECONDS)
.build();
private static final String ERROR_MESSAGE = "Unexpected code: ";
@Resource
private CozeProperties cozeProperties;
/**
* 获取token
* @return
*/
public CozeAuthRespVo getAccessToken() {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put(CozeEnum.GRANT_TYPE.getName(), cozeProperties.getGrantType());
jsonObject.put(CozeEnum.CODE.getName(), cozeProperties.getCode());
jsonObject.put(CozeEnum.CLIENT_ID.getName(), cozeProperties.getClientId());
jsonObject.put(CozeEnum.REDIRECT_URI.getName(), cozeProperties.getRedirectUri());
RequestBody body = RequestBody.create(jsonObject.toString(), MediaType.get(MEDIA_TYPE_JSON));
Request request = buildAuthRequest(cozeProperties.getOAuthAccessTokenUri(), body);
String res = executeRequest(request);
CozeAuthRespVo cozeAuthRespVo = JSONObject.parseObject(res, CozeAuthRespVo.class);
return cozeAuthRespVo;
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 构建获取鉴权请求
* @param url
* @param body
* @return
*/
private Request buildAuthRequest(String url, RequestBody body) {
return new Request.Builder()
.url(url)
.addHeader(CozeEnum.AUTHORIZATION.getName(), CozeEnum.BEARER+ cozeProperties.getClientSecret())
.post(body)
.build();
}
/**
* 发送请求
* @param request
* @return
* @throws IOException
*/
private String executeRequest(Request request) throws IOException {
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.out.println(response.body().string());
throw new IOException(ERROR_MESSAGE + response);
}
String res = response.body().string();
return res;
}
}
}
以上获取 Access Token 成功了,但是根据官网调用 刷新OAuth Access Token 一直失败
所以打算换种方式实现
采用官网提供的SDK进行实现
官方github的demo地址:https://github.com/coze-dev/coze-java/tree/main/example/src/main/java/example
pom中引入
因为我的项目中有使用 okhttp3 版本冲突了,所以这里处理了下
<dependency>
<groupId>com.coze</groupId>
<artifactId>coze-api</artifactId>
<version>LATEST</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
CozeProperties 配置文件
package com.goodsoft.shrk.module.ai.config.coze;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* @author YXY
* @date 2025-03-24
* @description coze鉴权配置文件
*/
@Data
@RefreshScope
public class CozeProperties {
@Value("${coze.clientSecret}")
private String clientSecret;
@Value("${coze.code}")
private String code;
@Value("${coze.clientId}")
private String clientId;
@Value("${coze.redirectUri}")
private String redirectUri;
}
import com.coze.openapi.client.auth.OAuthToken;
import com.coze.openapi.service.auth.WebOAuthClient;
import com.coze.openapi.service.config.Consts;
import com.goodsoft.shrk.framework.redis.config.CommonCache;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author YXY
* @date 2025-03-24
* @description coze API
*/
@Component
@Slf4j
public class CozeApiClient {
@Resource
private CozeProperties cozeProperties;
@Resource
private CommonCache commonCache;
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY = "COZE_ACCESS_TOKEN_LOCK";
private static final String COZE_ACCESS_TOKEN= "coze_access_token:";
/**
* 获取token
* @return
*/
public OAuthToken getAccessToken() {
if (commonCache.hasKey(COZE_ACCESS_TOKEN)){
return commonCache.getVal(COZE_ACCESS_TOKEN);
}
boolean lockAcquired = false;
int maxRetries = 5; // 最大重试次数
int retries = 0; // 当前重试次数
RLock lock = redissonClient.getLock(LOCK_KEY);
if (!commonCache.hasKey(COZE_ACCESS_TOKEN)){
while (!lockAcquired && retries < maxRetries) {
try {
// 尝试获取锁,最多等待 5 秒钟
lockAcquired = lock.tryLock(5, TimeUnit.SECONDS);
if (lockAcquired) {
try {
WebOAuthClient oauth =
new WebOAuthClient.WebOAuthBuilder()
.clientID(cozeProperties.getClientId())
.clientSecret(cozeProperties.getClientSecret())
.baseURL(Consts.COZE_CN_BASE_URL)
.build();
OAuthToken resp = oauth.getAccessToken(cozeProperties.getCode(), cozeProperties.getRedirectUri());
log.info( "获取CozeApi-token成功:{}", resp);
resp = oauth.refreshToken(resp.getRefreshToken());
log.info( "刷新CozeApi-token成功:{}", resp);
commonCache.set(COZE_ACCESS_TOKEN,resp,resp.getExpiresIn()-60,TimeUnit.SECONDS);
} finally {
lock.unlock(); // 释放锁
}
} else {
// 未能获取锁,重试
retries++;
Thread.sleep(100); // 重试间隔
}
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
// 尝试重新标记中断状态,并继续重试
Thread.currentThread().interrupt();
}
}
}
return commonCache.getVal(COZE_ACCESS_TOKEN);
}
}
测试鉴权成功,进行下一步
执行工作流
找到工作流的 workflow_id
demo
public void reqWorkflows(){
OAuthToken accessToken = getAccessToken();
TokenAuth authCli = new TokenAuth(accessToken.getAccessToken());
CozeAPI coze =
new CozeAPI.Builder()
.baseURL(Consts.COZE_CN_BASE_URL)
.auth(authCli)
.readTimeout(10000)
.build();
HashMap<String, Object> map = new HashMap<>();
map.put("input", "生成一个设计方案");
map.put("fileUrl", "https://***.pdf");
map.put("title", "标题");
RunWorkflowResp workflowResp = coze.workflows().runs().create(
RunWorkflowReq.builder()
.workflowID("******工作流ID")
.parameters(map)
.build()
);
System.out.println(workflowResp);
}
做到这里我发现这个授权码code用一次就失效了,每次都得点击页面授权,这个需要前端打开授权页,然后用户点击授权成功回调接口执行后续逻辑,而我们需要的业务没有页面授权这一操作,
应该采用OAuth JWT 授权
OAuth JWT 授权
https://www.coze.cn/open/docs/developer_guides/oauth_jwt
重新创建JWT应用
点击创建Key 自动下载私钥 复制公钥备用
将私钥 private_key.pem
文件放到项目的resources
下
CozeProperties配置文件
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* @author YXY
* @date 2025-03-24
* @description coze鉴权配置文件
*/
@Data
@RefreshScope
public class CozeProperties {
@Value("${coze.clientId}")
private String clientId;
@Value("${coze.jwt.publicKey}")
private String publicKey;
public String getPrivateKey() {
try (InputStream inputStream = CozeApiClient.class.getClassLoader()
.getResourceAsStream("private_key.pem")) {
if (inputStream == null) {
throw new RuntimeException("私钥文件未找到");
}
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("读取私钥文件失败", e);
}
}
}
根据官网提供方式进行测试:
public static void main(String[] args) {
String jwtOauthClientID = "应用ID";
String jwtOauthPrivateKey = getPrivateKey();
String jwtOauthPublicKeyID = "公钥";
JWTOAuthClient oauth = null;
try {
oauth =
new JWTOAuthClient.JWTOAuthBuilder()
.clientID(jwtOauthClientID)
.privateKey(jwtOauthPrivateKey)
.publicKey(jwtOauthPublicKeyID)
.baseURL(Consts.COZE_CN_BASE_URL)
.build();
} catch (Exception e) {
e.printStackTrace();
return;
}
try {
OAuthToken resp = oauth.getAccessToken();
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String getPrivateKey() {
try (InputStream inputStream = CozeApiClient.class.getClassLoader()
.getResourceAsStream("private_key.pem")) {
if (inputStream == null) {
throw new RuntimeException("私钥文件未找到");
}
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("读取私钥文件失败", e);
}
}
运行报错
找到原因:https://github.com/coze-dev/coze-java?tab=readme-ov-file#jwt-oauth-app
需要自己实现com.coze.openapi.service.auth.JWTBuilder这个接口
https://github.com/coze-dev/coze-java/blob/main/example/src/main/java/example/auth/ExampleJWTBuilder.java
需要实现自己的 JWTBuilder
import com.coze.openapi.service.auth.JWTBuilder;
import com.coze.openapi.service.auth.JWTPayload;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.NoArgsConstructor;
import java.security.PrivateKey;
import java.util.Map;
/**
* @author YXY
* @date 2025-03-24
* @description
*/
@NoArgsConstructor
public class ExampleJWTBuilder implements JWTBuilder {
@Override
public String generateJWT(PrivateKey privateKey, Map<String, Object> header, JWTPayload payload) {
try {
JwtBuilder jwtBuilder =
Jwts.builder()
.setHeader(header)
.setIssuer(payload.getIss())
.setAudience(payload.getAud())
.setIssuedAt(payload.getIat())
.setExpiration(payload.getExp())
.setId(payload.getJti())
.signWith(privateKey, SignatureAlgorithm.RS256);
if (payload.getSessionName() != null) {
jwtBuilder.claim("session_name", payload.getSessionName());
}
return jwtBuilder.compact();
} catch (Exception e) {
throw new RuntimeException("Failed to generate JWT", e);
}
}
}
实现后进行使用
成功
package com.goodsoft.shrk.module.ai.config.coze;
import com.coze.openapi.client.auth.OAuthToken;
import com.coze.openapi.service.auth.JWTOAuthClient;
import com.coze.openapi.service.config.Consts;
import com.goodsoft.shrk.framework.redis.config.CommonCache;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import static com.goodsoft.shrk.module.system.dal.redis.RedisKeyConstants.COZE_ACCESS_TOKEN;
/**
* @author YXY
* @date 2025-03-24
* @description coze鉴权配置文件
*/
@Component
@Slf4j
public class CozeApiClient {
@Resource
private CozeProperties cozeProperties;
@Resource
private CommonCache commonCache;
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY = "COZE_ACCESS_TOKEN_LOCK";
/**
* 获取token
* @return
*/
public OAuthToken getAccessToken() {
if (commonCache.hasKey(COZE_ACCESS_TOKEN)){
return commonCache.getVal(COZE_ACCESS_TOKEN);
}
boolean lockAcquired = false;
int maxRetries = 5; // 最大重试次数
int retries = 0; // 当前重试次数
RLock lock = redissonClient.getLock(LOCK_KEY);
if (!commonCache.hasKey(COZE_ACCESS_TOKEN)){
while (!lockAcquired && retries < maxRetries) {
try {
// 尝试获取锁,最多等待 5 秒钟
lockAcquired = lock.tryLock(5, TimeUnit.SECONDS);
if (lockAcquired) {
try {
OAuthToken oAuthToken = getOAuthToken();
commonCache.set(COZE_ACCESS_TOKEN,oAuthToken,oAuthToken.getExpiresIn()-60,TimeUnit.SECONDS);
} finally {
lock.unlock(); // 释放锁
}
} else {
// 未能获取锁,重试
retries++;
Thread.sleep(100); // 重试间隔
}
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
// 尝试重新标记中断状态,并继续重试
Thread.currentThread().interrupt();
}
}
}
return commonCache.getVal(COZE_ACCESS_TOKEN);
}
public OAuthToken getOAuthToken() {
String jwtOauthClientID = cozeProperties.getClientId();
String jwtOauthPrivateKey = cozeProperties.getPrivateKey();
String jwtOauthPublicKeyID = cozeProperties.getPublicKey();
OAuthToken resp = null;
try {
JWTOAuthClient oauth =
new JWTOAuthClient.JWTOAuthBuilder()
.clientID(jwtOauthClientID)
.privateKey(jwtOauthPrivateKey)
.publicKey(jwtOauthPublicKeyID)
.baseURL(Consts.COZE_CN_BASE_URL)
.jwtBuilder(new ExampleJWTBuilder())
.build();
resp = oauth.getAccessToken();
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
return resp;
}
}
鉴权搞定之后进行调用工作流
工作流入参实体,工作流id 和 工作流入参数据
package com.goodsoft.shrk.module.ai.controller.admin.coze.vo;
import lombok.Data;
import java.util.HashMap;
/**
* @author YXY
* @date 2025-03-24
* @description
*/
@Data
public class WorkflowsReqVo {
private String workflowID;
private HashMap<String, Object> parameters;
}
CozeApiClient
这里的 COZE_ACCESS_TOKEN = "coze_access_token:";
提取到redis常量池中了
import com.coze.openapi.client.auth.OAuthToken;
import com.coze.openapi.client.workflows.run.RunWorkflowReq;
import com.coze.openapi.client.workflows.run.RunWorkflowResp;
import com.coze.openapi.service.auth.JWTOAuthClient;
import com.coze.openapi.service.auth.TokenAuth;
import com.coze.openapi.service.config.Consts;
import com.coze.openapi.service.service.CozeAPI;
import com.goodsoft.shrk.framework.redis.config.CommonCache;
import com.goodsoft.shrk.module.ai.controller.admin.coze.vo.WorkflowsReqVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import static com.goodsoft.shrk.module.system.dal.redis.RedisKeyConstants.COZE_ACCESS_TOKEN;
/**
* @author YXY
* @date 2025-03-24
* @description coze鉴权配置文件
*/
@Component
@Slf4j
public class CozeApiClient {
@Resource
private CozeProperties cozeProperties;
@Resource
private CommonCache commonCache;
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY = "COZE_ACCESS_TOKEN_LOCK";
/**
* 获取token
* @return
*/
public OAuthToken getAccessToken() {
if (commonCache.hasKey(COZE_ACCESS_TOKEN)){
return commonCache.getVal(COZE_ACCESS_TOKEN);
}
boolean lockAcquired = false;
int maxRetries = 5; // 最大重试次数
int retries = 0; // 当前重试次数
RLock lock = redissonClient.getLock(LOCK_KEY);
if (!commonCache.hasKey(COZE_ACCESS_TOKEN)){
while (!lockAcquired && retries < maxRetries) {
try {
// 尝试获取锁,最多等待 5 秒钟
lockAcquired = lock.tryLock(5, TimeUnit.SECONDS);
if (lockAcquired) {
try {
OAuthToken oAuthToken = getOAuthToken();
commonCache.set(COZE_ACCESS_TOKEN,oAuthToken,oAuthToken.getExpiresIn()-60,TimeUnit.SECONDS);
} finally {
lock.unlock(); // 释放锁
}
} else {
// 未能获取锁,重试
retries++;
Thread.sleep(100); // 重试间隔
}
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
// 尝试重新标记中断状态,并继续重试
Thread.currentThread().interrupt();
}
}
}
return commonCache.getVal(COZE_ACCESS_TOKEN);
}
public OAuthToken getOAuthToken() {
String jwtOauthClientID = cozeProperties.getClientId();
String jwtOauthPrivateKey = cozeProperties.getPrivateKey();
String jwtOauthPublicKeyID = cozeProperties.getPublicKey();
OAuthToken resp = null;
try {
JWTOAuthClient oauth =
new JWTOAuthClient.JWTOAuthBuilder()
.clientID(jwtOauthClientID)
.privateKey(jwtOauthPrivateKey)
.publicKey(jwtOauthPublicKeyID)
.baseURL(Consts.COZE_CN_BASE_URL)
.jwtBuilder(new ExampleJWTBuilder())
.build();
resp = oauth.getAccessToken();
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
return resp;
}
/**
* 调用工作流
*/
public void reqWorkflows(WorkflowsReqVo req){
OAuthToken accessToken = getAccessToken();
TokenAuth authCli = new TokenAuth(accessToken.getAccessToken());
CozeAPI coze =
new CozeAPI.Builder()
.baseURL(Consts.COZE_CN_BASE_URL)
.auth(authCli)
.readTimeout(10000)
.build();
RunWorkflowResp workflowResp = coze.workflows().runs().create(
RunWorkflowReq.builder()
.workflowID(req.getWorkflowID())
.parameters(req.getParameters())
.isAsync(true)
.build()
);
System.out.println(workflowResp);
}
}
因为工作流执行时间较长,我的工作流配置了执行完成后 http请求我的接口进行后续逻辑处理,所以这里开启了异步执行
isAsync(true)
代表异步调用工作流
coze工作流发送http请求:https://blog.csdn.net/YXWik/article/details/146398637
成功