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'

// 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