Skip to content

Commit e49fec6

Browse files
authored
feat: Enhance JWT handling and update dependencies (#38)
- Add note to README about jjwt 0.12.x exclusions in Maven/Gradle - Update API and example pom files from version 0.2.4 to 0.2.5 - Introduce JWTBuilder, DefaultJWTBuilder, and ExampleJWTBuilder for JWT generation - Refactor JWTOAuthClient to delegate JWT creation - Implement RxJava for asynchronous chat event processing in example
1 parent d1e1296 commit e49fec6

14 files changed

Lines changed: 392 additions & 124 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,41 @@ public void getAccessToken() {
531531

532532
#### JWT OAuth App
533533

534+
**Note**: The SDK uses jjwt version 0.11.5. If you are using jjwt version 0.12.x or above:
535+
536+
1. You need to exclude jjwt dependencies when importing the SDK:
537+
538+
for Maven:
539+
```xml
540+
<dependency>
541+
<groupId>com.coze</groupId>
542+
<artifactId>coze-api</artifactId>
543+
<version>0.1.0</version>
544+
<exclusions>
545+
<exclusion>
546+
<groupId>io.jsonwebtoken</groupId>
547+
<artifactId>jjwt-api</artifactId>
548+
</exclusion>
549+
<exclusion>
550+
<groupId>io.jsonwebtoken</groupId>
551+
<artifactId>jjwt-impl</artifactId>
552+
</exclusion>
553+
<exclusion>
554+
<groupId>io.jsonwebtoken</groupId>
555+
<artifactId>jjwt-jackson</artifactId>
556+
</exclusion>
557+
</exclusions>
558+
</dependency>
559+
```
560+
561+
for Gradle:
562+
```
563+
implementation('com.coze:coze-api:0.1.0') {
564+
exclude group: 'io.jsonwebtoken'
565+
}
566+
```
567+
2. Please refer to [ExampleJWTBuilder.java](example/src/main/java/example/auth/ExampleJWTBuilder.java) to implement your own JWT builder.
568+
534569
Firstly, users need to access https://www.coze.com/open/oauth/apps. For the cn environment,
535570
users need to access https://www.coze.cn/open/oauth/apps to create an OAuth App of the type
536571
of Service application.

api/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
</parent>
4444

4545
<artifactId>coze-api</artifactId>
46-
<version>0.2.4</version>
46+
<version>0.2.5</version>
4747

4848
<scm>
4949
<connection>scm:git:git://github.com/coze-dev/coze-java.git</connection>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.coze.openapi.service.auth;
2+
3+
import java.security.PrivateKey;
4+
import java.util.Map;
5+
6+
import io.jsonwebtoken.JwtBuilder;
7+
import io.jsonwebtoken.Jwts;
8+
import io.jsonwebtoken.SignatureAlgorithm;
9+
import lombok.NoArgsConstructor;
10+
11+
@NoArgsConstructor
12+
public class DefaultJWTBuilder implements JWTBuilder {
13+
14+
@Override
15+
public String generateJWT(PrivateKey privateKey, Map<String, Object> header, JWTPayload payload) {
16+
try {
17+
JwtBuilder jwtBuilder =
18+
Jwts.builder()
19+
.setHeader(header)
20+
.setIssuer(payload.getIss())
21+
.setAudience(payload.getAud())
22+
.setIssuedAt(payload.getIat())
23+
.setExpiration(payload.getExp())
24+
.setId(payload.getJti())
25+
.signWith(privateKey, SignatureAlgorithm.RS256);
26+
if (payload.getSessionName() != null) {
27+
jwtBuilder.claim("session_name", payload.getSessionName());
28+
}
29+
return jwtBuilder.compact();
30+
31+
} catch (Exception e) {
32+
throw new RuntimeException("Failed to generate JWT", e);
33+
}
34+
}
35+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.coze.openapi.service.auth;
2+
3+
import java.security.PrivateKey;
4+
import java.util.Map;
5+
6+
public interface JWTBuilder {
7+
String generateJWT(PrivateKey privateKey, Map<String, Object> header, JWTPayload payload);
8+
}

api/src/main/java/com/coze/openapi/service/auth/JWTOAuthClient.java

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@
1212
import com.coze.openapi.client.auth.scope.Scope;
1313
import com.coze.openapi.service.utils.Utils;
1414

15-
import io.jsonwebtoken.JwtBuilder;
16-
import io.jsonwebtoken.Jwts;
17-
import io.jsonwebtoken.SignatureAlgorithm;
1815
import lombok.Getter;
1916

2017
public class JWTOAuthClient extends OAuthClient {
2118
@Getter private final Integer ttl;
2219
private final PrivateKey privateKey;
2320
private final String publicKey;
21+
private final JWTBuilder jwtBuilder;
2422

2523
protected JWTOAuthClient(JWTOAuthBuilder builder) throws Exception {
2624
super(builder);
25+
2726
this.privateKey = parsePrivateKey(builder.privateKey);
2827
this.publicKey = builder.publicKey;
28+
if (builder.jwtBuilder != null) {
29+
this.jwtBuilder = builder.jwtBuilder;
30+
} else {
31+
this.jwtBuilder = new DefaultJWTBuilder();
32+
}
2933
this.ttl = builder.ttl;
3034
}
3135

@@ -79,37 +83,23 @@ public OAuthToken getAccessToken(Integer ttl, Scope scope, String sessionName) {
7983
private OAuthToken doGetAccessToken(Integer ttl, Scope scope, String sessionName) {
8084
GetAccessTokenReq.GetAccessTokenReqBuilder builder = GetAccessTokenReq.builder();
8185
builder.grantType(GrantType.JWT_CODE.getValue()).durationSeconds(ttl).scope(scope);
82-
83-
return getAccessToken(this.generateJWT(ttl, sessionName), builder.build());
84-
}
85-
86-
private String generateJWT(int ttl, String sessionName) {
87-
try {
88-
long now = System.currentTimeMillis() / 1000;
89-
90-
// 构建 JWT header
91-
Map<String, Object> header = new HashMap<>();
92-
header.put("alg", "RS256");
93-
header.put("typ", "JWT");
94-
header.put("kid", this.publicKey);
95-
96-
JwtBuilder jwtBuilder =
97-
Jwts.builder()
98-
.setHeader(header)
99-
.setIssuer(this.clientID)
100-
.setAudience(this.hostName)
101-
.setIssuedAt(new Date(now * 1000))
102-
.setExpiration(new Date((now + ttl) * 1000))
103-
.setId(Utils.genRandomSign(16))
104-
.signWith(privateKey, SignatureAlgorithm.RS256);
105-
if (sessionName != null) {
106-
jwtBuilder.claim("session_name", sessionName);
107-
}
108-
return jwtBuilder.compact();
109-
110-
} catch (Exception e) {
111-
throw new RuntimeException("Failed to generate JWT", e);
112-
}
86+
Map<String, Object> header = new HashMap<>();
87+
header.put("alg", "RS256");
88+
header.put("typ", "JWT");
89+
header.put("kid", this.publicKey);
90+
long now = System.currentTimeMillis() / 1000;
91+
92+
JWTPayload payload =
93+
JWTPayload.builder()
94+
.iss(this.clientID)
95+
.aud(this.hostName)
96+
.exp(new Date((now + this.ttl) * 1000))
97+
.iat(new Date(now * 1000))
98+
.sessionName(sessionName)
99+
.jti(Utils.genRandomSign(16))
100+
.build();
101+
return getAccessToken(
102+
this.jwtBuilder.generateJWT(privateKey, header, payload), builder.build());
113103
}
114104

115105
private PrivateKey parsePrivateKey(String privateKeyPEM) throws Exception {
@@ -129,6 +119,7 @@ public static class JWTOAuthBuilder extends OAuthBuilder<JWTOAuthBuilder> {
129119
private Integer ttl;
130120
private String publicKey;
131121
private String privateKey;
122+
private JWTBuilder jwtBuilder;
132123

133124
public JWTOAuthBuilder publicKey(String publicKey) {
134125
this.publicKey = publicKey;
@@ -145,6 +136,12 @@ public JWTOAuthBuilder privateKey(String privateKey) {
145136
return this;
146137
}
147138

139+
// If you are using jjwt version 0.12.x or above, you need to handle JWT parsing by yourself
140+
public JWTOAuthBuilder jwtBuilder(JWTBuilder jwtBuilder) {
141+
this.jwtBuilder = jwtBuilder;
142+
return this;
143+
}
144+
148145
@Override
149146
protected JWTOAuthBuilder self() {
150147
return this;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.coze.openapi.service.auth;
2+
3+
import java.util.Date;
4+
5+
import lombok.*;
6+
7+
@Data
8+
@Builder
9+
@AllArgsConstructor
10+
@NoArgsConstructor
11+
public class JWTPayload {
12+
@NonNull private String iss;
13+
@NonNull private String aud;
14+
@NonNull private Date iat;
15+
@NonNull private Date exp;
16+
@NonNull private String jti;
17+
private String sessionName;
18+
}

api/src/main/java/com/coze/openapi/service/service/common/AbstractEventCallback.java

Lines changed: 81 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.io.InputStream;
66
import java.io.InputStreamReader;
77
import java.nio.charset.StandardCharsets;
8+
import java.util.concurrent.ExecutorService;
9+
import java.util.concurrent.Executors;
810

911
import org.slf4j.Logger;
1012

@@ -24,71 +26,99 @@ public abstract class AbstractEventCallback<T> implements Callback<ResponseBody>
2426
private static final ObjectMapper mapper = Utils.defaultObjectMapper();
2527
private static final Logger logger = CozeLoggerFactory.getLogger();
2628

29+
private final ExecutorService backgroundExecutor;
2730
protected FlowableEmitter<T> emitter;
2831

2932
public AbstractEventCallback(FlowableEmitter<T> emitter) {
3033
this.emitter = emitter;
34+
this.backgroundExecutor = Executors.newSingleThreadExecutor();
35+
36+
emitter.setCancellable(backgroundExecutor::shutdownNow);
3137
}
3238

3339
@Override
3440
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
35-
BufferedReader reader = null;
36-
37-
try {
38-
String logID = Utils.getLogID(response);
39-
if (!response.isSuccessful()) {
40-
logger.warn("HTTP error: " + response.code() + " " + response.message());
41-
String errStr = response.errorBody().string();
42-
CozeError error = mapper.readValue(errStr, CozeError.class);
43-
throw new CozeApiException(
44-
Integer.valueOf(response.code()), error.getErrorMessage(), logID);
45-
}
46-
47-
// 检查 response body 是否为 BaseResponse 格式
48-
String contentType = response.headers().get("Content-Type");
49-
if (contentType != null && contentType.contains("application/json")) {
50-
String respStr = response.body().string();
51-
BaseResponse<?> baseResp = mapper.readValue(respStr, BaseResponse.class);
52-
if (baseResp.getCode() != 0) {
53-
logger.warn("API error: {} {}", baseResp.getCode(), baseResp.getMsg());
54-
throw new CozeApiException(baseResp.getCode(), baseResp.getMsg(), logID);
55-
}
56-
return;
57-
}
58-
59-
InputStream in = response.body().byteStream();
60-
reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
61-
String line;
62-
63-
while (!emitter.isCancelled() && (line = reader.readLine()) != null) {
64-
if (processLine(line, reader, logID)) {
65-
break;
66-
}
67-
}
68-
69-
emitter.onComplete();
70-
71-
} catch (Throwable t) {
72-
onFailure(call, t);
73-
} finally {
74-
if (reader != null) {
75-
try {
76-
reader.close();
77-
} catch (IOException e) {
78-
// do nothing
79-
}
80-
if (response.body() != null) {
81-
response.body().close();
82-
}
83-
}
84-
}
41+
// 将整个处理过程移到后台线程
42+
backgroundExecutor.execute(
43+
() -> {
44+
BufferedReader reader = null;
45+
46+
try {
47+
String logID = Utils.getLogID(response);
48+
if (!response.isSuccessful()) {
49+
logger.warn("HTTP error: " + response.code() + " " + response.message());
50+
String errStr = response.errorBody().string();
51+
CozeError error = mapper.readValue(errStr, CozeError.class);
52+
CozeApiException exception =
53+
new CozeApiException(
54+
Integer.valueOf(response.code()), error.getErrorMessage(), logID);
55+
emitter.onError(exception);
56+
return;
57+
}
58+
59+
// 检查 response body 是否为 BaseResponse 格式
60+
String contentType = response.headers().get("Content-Type");
61+
if (contentType != null && contentType.contains("application/json")) {
62+
String respStr = response.body().string();
63+
try {
64+
BaseResponse<?> baseResp = mapper.readValue(respStr, BaseResponse.class);
65+
if (baseResp.getCode() != 0) {
66+
logger.warn("API error: {} {}", baseResp.getCode(), baseResp.getMsg());
67+
CozeApiException exception =
68+
new CozeApiException(baseResp.getCode(), baseResp.getMsg(), logID);
69+
emitter.onError(exception);
70+
return;
71+
}
72+
emitter.onComplete();
73+
return;
74+
} catch (Exception e) {
75+
logger.error("Failed to parse JSON response: {}", respStr, e);
76+
CozeApiException exception =
77+
new CozeApiException(
78+
-1, "Failed to parse JSON response: " + e.getMessage(), logID);
79+
emitter.onError(exception);
80+
return;
81+
}
82+
}
83+
84+
InputStream in = response.body().byteStream();
85+
reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
86+
String line;
87+
88+
while (!emitter.isCancelled() && (line = reader.readLine()) != null) {
89+
if (processLine(line, reader, logID)) {
90+
break;
91+
}
92+
}
93+
94+
emitter.onComplete();
95+
96+
} catch (Throwable t) {
97+
onFailure(call, t);
98+
} finally {
99+
if (reader != null) {
100+
try {
101+
reader.close();
102+
} catch (IOException e) {
103+
// do nothing
104+
}
105+
if (response.body() != null) {
106+
response.body().close();
107+
}
108+
}
109+
}
110+
});
85111
}
86112

87113
protected abstract boolean processLine(String line, BufferedReader reader, String logID)
88114
throws IOException;
89115

90116
@Override
91117
public void onFailure(Call<ResponseBody> call, Throwable t) {
92-
emitter.onError(t);
118+
try {
119+
emitter.onError(t);
120+
} finally {
121+
backgroundExecutor.shutdown();
122+
}
93123
}
94124
}

api/src/main/java/com/coze/openapi/service/utils/UserAgentInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public Response intercept(Chain chain) throws IOException {
2424
return chain.proceed(request);
2525
}
2626

27-
public static final String VERSION = "0.2.4";
27+
public static final String VERSION = "0.2.5";
2828
private static final ObjectMapper objectMapper = new ObjectMapper();
2929

3030
/** 获取操作系统版本 */

api/src/test/java/com/coze/openapi/service/auth/JWTOAuthClientTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ void setUp() throws Exception {
6464
.publicKey("test_public_key")
6565
.privateKey(TEST_PRIVATE_KEY)
6666
.ttl(900)
67+
.jwtBuilder(new DefaultJWTBuilder())
6768
.build();
6869
TestUtils.setField(client, "api", mockApi);
6970
}

0 commit comments

Comments
 (0)