Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
- name: Checkout the code
uses: actions/checkout@v4

- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'

- name: Setup Gradle
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dev-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
uses: actions/checkout@v4

# --- Java, Gradle 설정 ---
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/prod-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ jobs:
uses: actions/checkout@v4

# --- Java, Gradle 설정 ---
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# JDK 버전 설정
FROM eclipse-temurin:17-jdk
FROM eclipse-temurin:21-jdk

# JAR_FILE 변수 정의
ARG JAR_FILE=./build/libs/solid-connection-0.0.1-SNAPSHOT.jar
Expand Down
14 changes: 8 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.flywaydb.flyway' version '9.16.3'
id 'org.springframework.boot' version '3.5.11'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.flywaydb.flyway' version '10.15.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '17'
sourceCompatibility = '21'
}

configurations {
Expand Down Expand Up @@ -43,8 +43,9 @@ dependencies {
// Security
implementation 'org.springframework.security:spring-security-config'
implementation 'org.springframework.security:spring-security-web'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
runtimeOnly 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' // for jjwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
Comment on lines +46 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 이전 JJWT API를 사용하는 파일 검색
echo "=== SignatureAlgorithm 사용 파일 ==="
rg -n "SignatureAlgorithm" --type java

echo ""
echo "=== setSubject/setIssuedAt/setExpiration 사용 파일 ==="
rg -n "setSubject|setIssuedAt|setExpiration" --type java -g '!build/**'

Repository: solid-connection/solid-connect-server

Length of output: 3760


🏁 Script executed:

# Verify the actual migration path for JJWT 0.12.6 API
rg -A 2 "signWith\(" src/test/java/com/example/solidconnection/security/ --type java | head -20

Repository: solid-connection/solid-connect-server

Length of output: 2106


JJWT 0.12.6 마이그레이션이 세 개의 테스트 파일에서 미완료되었습니다.

build.gradle 의존성 업그레이드는 올바르게 수행되었으나, 다음 세 개의 테스트 파일이 여전히 이전 JJWT API를 사용하고 있어 컴파일 오류가 발생할 것입니다:

  1. src/test/java/com/example/solidconnection/security/authentication/TokenAuthenticationProviderTest.java
  2. src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java
  3. src/test/java/com/example/solidconnection/security/filter/SignOutCheckFilterTest.java

각 파일마다 다음과 같은 API 변경이 필요합니다:

- `signWith(SignatureAlgorithm.HS256, key)` → `signWith(secretKey)`
- `setSubject()` → `subject()`
- `setIssuedAt()` → `issuedAt()`
- `setExpiration()` → `expiration()`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build.gradle` around lines 46 - 48, Three test files still use the old JJWT
builder/signing API and must be updated to the 0.12.6 API: in
TokenAuthenticationProviderTest, TokenAuthenticationFilterTest, and
SignOutCheckFilterTest replace uses of signWith(SignatureAlgorithm.HS256, key)
with signWith(secretKey) (ensure the test creates a SecretKey instance as used
in production), and rename JWT builder setters setSubject(...),
setIssuedAt(...), setExpiration(...) to the new fluent names subject(...),
issuedAt(...), expiration(...); update imports/usages accordingly so the tests
compile against the jjwt-api 0.12.6 methods.


// Monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Expand All @@ -70,6 +71,7 @@ dependencies {
implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store:3.0.4'
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.apache.commons:commons-lang3'

// Database Proxy
implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0'
Expand Down
4 changes: 2 additions & 2 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

Solid Connect Server는 교환학생 준비생을 위해 대학 정보, 멘토 매칭, 모의지원 기능 등을 제공하는 교환학생 지원 통합 플랫폼입니다.

- **언어**: Java 17
- **프레임워크**: Spring Boot 3.1.5
- **언어**: Java 21
- **프레임워크**: Spring Boot 3.5.11
- **빌드 도구**: Gradle
- **데이터베이스**: MySQL (주), Redis (캐싱)
- **마이그레이션**: Flyway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ private MultiValueMap<String, String> buildFormData(String code) {
private String parseEmailFromToken(PublicKey applePublicKey, String idToken) {
try {
return Jwts.parser()
.setSigningKey(applePublicKey)
.parseClaimsJws(idToken)
.getBody()
.verifyWith(applePublicKey)
.build()
.parseSignedClaims(idToken)
.getPayload()
.get("email", String.class);
} catch (Exception e) {
throw new CustomException(INVALID_APPLE_ID_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
import com.example.solidconnection.auth.client.config.AppleOAuthClientProperties;
import com.example.solidconnection.common.exception.CustomException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.annotation.PostConstruct;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Component;

/*
Expand Down Expand Up @@ -42,20 +41,19 @@ public String generateClientSecret() {
Date expiration = new Date(now.getTime() + TOKEN_DURATION);

return Jwts.builder()
.setHeaderParam("alg", "ES256")
.setHeaderParam(KEY_ID_HEADER, appleOAuthClientProperties.keyId())
.setSubject(appleOAuthClientProperties.clientId())
.setIssuer(appleOAuthClientProperties.teamId())
.setAudience(appleOAuthClientProperties.clientSecretAudienceUrl())
.setExpiration(expiration)
.signWith(SignatureAlgorithm.ES256, privateKey)
.header().add(KEY_ID_HEADER, appleOAuthClientProperties.keyId()).and()
.subject(appleOAuthClientProperties.clientId())
.issuer(appleOAuthClientProperties.teamId())
.audience().add(appleOAuthClientProperties.clientSecretAudienceUrl()).and()
.expiration(expiration)
.signWith(privateKey, Jwts.SIG.ES256)
.compact();
}

private PrivateKey loadPrivateKey() {
try {
String secretKey = appleOAuthClientProperties.secretKey();
byte[] encoded = Base64.decodeBase64(secretKey);
byte[] encoded = Base64.getMimeDecoder().decode(secretKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return keyFactory.generatePrivate(keySpec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
import com.example.solidconnection.auth.token.config.JwtProperties;
import com.example.solidconnection.common.exception.CustomException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.Map;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

Expand All @@ -32,16 +35,18 @@ public String generateToken(Subject subject, Map<String, String> customClaims, D
}

private String generateJwtTokenValue(String subject, Map<String, String> claims, Duration expireTime) {
Claims jwtClaims = Jwts.claims().setSubject(subject);
jwtClaims.putAll(claims);
Date now = new Date();
Date expiredDate = new Date(now.getTime() + expireTime.toMillis());
return Jwts.builder()
.setClaims(jwtClaims)
.setIssuedAt(now)
.setExpiration(expiredDate)
.signWith(SignatureAlgorithm.HS512, jwtProperties.secret())
.compact();
JwtBuilder builder = Jwts.builder()
.subject(subject)
.issuedAt(now)
.expiration(expiredDate);
claims.forEach(builder::claim);
return builder.signWith(getSigningKey()).compact();
}

private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.secret().getBytes(StandardCharsets.UTF_8));
}

@Override
Expand All @@ -61,9 +66,10 @@ public <T> T parseClaims(String token, String claimName, Class<T> claimType) {
private Claims parseJwtClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtProperties.secret())
.parseClaimsJws(token)
.getBody();
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
} catch (Exception e) {
throw new CustomException(INVALID_TOKEN);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;

Expand All @@ -55,7 +55,7 @@ class AdminHostUniversityServiceTest {
@Autowired
private UnivApplyInfoFixtureBuilder univApplyInfoFixtureBuilder;

@SpyBean
@MockitoSpyBean
private CustomCacheManager cacheManager;

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
Expand All @@ -35,7 +35,7 @@ class RefreshTokenCookieManagerTest {
@Autowired
private TokenProperties tokenProperties;

@MockBean
@MockitoBean
private RefreshTokenCookieProperties refreshTokenCookieProperties;

private final String domain = "example.com";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import com.example.solidconnection.common.exception.ErrorCode;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -77,9 +80,10 @@ class 토큰을_생성한다 {

private Duration getActualExpireTime(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtProperties.secret())
.parseClaimsJws(token)
.getBody();
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return Duration.ofMillis(claims.getExpiration().getTime() - claims.getIssuedAt().getTime());
}
}
Expand Down Expand Up @@ -114,8 +118,7 @@ class 토큰으로부터_subject_를_추출한다 {
@Test
void subject_가_없는_토큰의_subject_를_추출하면_예외가_발생한다() {
// given
Claims claims = Jwts.claims(new HashMap<>());
String subjectNotExistingToken = createExpiredToken(claims);
String subjectNotExistingToken = createExpiredToken(new HashMap<>());
String subjectBlankToken = tokenProvider.generateToken(new Subject(" "), expectedExpireTime);

// when, then
Expand Down Expand Up @@ -155,8 +158,9 @@ class 토큰으로부터_claim_을_추출한다 {
@Test
void 유효하지_않은_토큰의_claim_을_추출하면_예외가_발생한다() {
// given
Claims expectedClaims = Jwts.claims(new HashMap<>(Map.of(claimKey, claimValue)));
String token = createExpiredToken(expectedClaims);
Map<String, Object> claims = new HashMap<>();
claims.put(claimKey, claimValue);
String token = createExpiredToken(claims);

// when
assertThatCode(() -> tokenProvider.parseClaims(token, claimKey, String.class))
Expand Down Expand Up @@ -184,19 +188,22 @@ class 토큰으로부터_claim_을_추출한다 {

private String createExpiredToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() - 1000))
.signWith(SignatureAlgorithm.HS256, jwtProperties.secret())
.subject(subject)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() - 1000))
.signWith(getSigningKey())
.compact();
}

private String createExpiredToken(Claims claims) {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() - 1000))
.signWith(SignatureAlgorithm.HS256, jwtProperties.secret())
.compact();
private String createExpiredToken(Map<String, Object> claims) {
JwtBuilder builder = Jwts.builder()
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() - 1000));
claims.forEach(builder::claim);
return builder.signWith(getSigningKey()).compact();
}

private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.secret().getBytes(StandardCharsets.UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@DisplayName("OAuth 서비스 테스트")
@TestContainerSpringBootTest
Expand All @@ -30,7 +30,7 @@ class OAuthServiceTest {
@Autowired
private SiteUserFixture siteUserFixture;

@MockBean
@MockitoBean
private OAuthClientMap oauthClientMap;

private final AuthType authType = AuthType.KAKAO;
Expand Down
Loading
Loading