Skip to content
Merged
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
7 changes: 6 additions & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,10 @@ jobs:
export COOLSMS_API_SECRET=${{ secrets.COOLSMS_API_SECRET }}
export COOLSMS_SENDER=${{ secrets.COOLSMS_SENDER }}

export FCM_KEY_PATH=/app/firebase/${{ secrets.FCM_FILE_NAME }}

# Spring Boot 앱만 pull
docker compose -f docker-compose.yml pull spring-boot-app
docker compose -f docker-compose.yml up -d --no-deps spring-boot-app

# 전체 재시작 (Redis는 변경 없으면 그대로 유지됨)
docker compose -f docker-compose.yml up -d
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ jobs:
test:
runs-on: ubuntu-latest

services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3

Expand All @@ -21,4 +26,6 @@ jobs:
run: chmod +x gradlew

- name: Run tests
env:
SPRING_PROFILES_ACTIVE: test
run: ./gradlew test --stacktrace --no-daemon
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.module:jackson-module-parameter-names'

implementation 'org.springframework.boot:spring-boot-starter-security'

Expand Down
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
- "80:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
FCM_KEY_PATH: ${FCM_KEY_PATH}
S3_BUCKET: ${S3_BUCKET}
DB_URL: ${DB_URL}
DB_USERNAME: ${DB_USERNAME}
Expand All @@ -14,17 +15,17 @@ services:
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
ADMIN_SECRET: ${ADMIN_SECRET}
KAKAO_APP_KEY: ${KAKAO_APP_KEY}
REDIS_HOST: redis
REDIS_PORT: 6379
COOLSMS_API_KEY: ${COOLSMS_API_KEY}
COOLSMS_API_SECRET: ${COOLSMS_API_SECRET}
COOLSMS_SENDER: ${COOLSMS_SENDER}
volumes:
- /home/ec2-user/app/firebase:/app/firebase:ro
depends_on:
- redis
networks:
- spring-boot-app-network

redis: # Redis 서비스 추가!
redis:
image: redis:7-alpine
container_name: redis
ports:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@

import com.meetkey.server.domain.auth.service.AuthService;
import com.meetkey.server.domain.auth.service.SmsService;
import com.meetkey.server.domain.member.entity.Member;
import com.meetkey.server.domain.member.enums.Provider;
import com.meetkey.server.domain.member.enums.Role;
import com.meetkey.server.domain.member.repository.MemberRepository;
import com.meetkey.server.domain.member.service.MemberService;
import com.meetkey.server.global.apiPayload.response.BasicResponse;
import com.meetkey.server.global.apiPayload.status.CommonErrorStatus;
import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus;
import com.meetkey.server.global.security.CustomUserDetails;
import com.meetkey.server.global.security.jwt.JwtUtil;
import com.meetkey.server.global.security.jwt.dto.JwtResDTO;
import com.meetkey.server.global.security.oauth.dto.OauthReqDTO;

import com.meetkey.server.global.security.oauth.dto.OidcDTO;
import com.meetkey.server.global.security.oauth.kakao.KakaoOauthClient;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -37,6 +34,7 @@
public class AuthController {
private final AuthService authService;
private final SmsService smsService;
private final KakaoOauthClient kakaoOauthClient;


@Value("${admin.secret}")
Expand Down Expand Up @@ -140,4 +138,11 @@ public ResponseEntity<BasicResponse<Boolean>> verifyAuthCode(
.body(BasicResponse.success(CommonSuccessStatus._OK, true));
}

// OIDC Cache 설정 확인 테스트
@GetMapping("/test/kakao-keys")
public ResponseEntity<?> getKeys() {
OidcDTO.OIDCPublicKeys keys = kakaoOauthClient.getKakaoOIDCOpenKeys();

return ResponseEntity.ok(keys);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class AuthService {
@Value(("{apple.app-key}"))
private String appleAppKey;

private final KakaoOauthClient kakaoClient;
private final KakaoOauthClient kakaoOauthClient;
private final AppleOauthClient appleClient;

private final OauthOidcHelper oAuthOIDCHelper;
Expand Down Expand Up @@ -173,7 +173,7 @@ private String getAppleProviderIdFromIdToken(String idToken){
}

private String getKakaoProviderIdFromIdToken(String idToken){
OidcDTO.OIDCPublicKeys response = kakaoClient.getKakaoOIDCOpenKeys();
OidcDTO.OIDCPublicKeys response = kakaoOauthClient.getKakaoOIDCOpenKeys();
OidcDTO.OIDCDecodePayload payload = oAuthOIDCHelper.getPayloadFromIdToken(
idToken,
"https://kauth.kakao.com",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package com.meetkey.server.domain.chat.message.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

@Configuration
@RequiredArgsConstructor
public class RedisSubscriberConfig {

private final RedisConnectionFactory connectionFactory;
private final ChatRedisSubscriber chatRedisSubscriber;

public RedisSubscriberConfig(
RedisConnectionFactory connectionFactory,
ChatRedisSubscriber chatRedisSubscriber) {
this.connectionFactory = connectionFactory;
this.chatRedisSubscriber = chatRedisSubscriber;
}

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/com/meetkey/server/global/config/FcmConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

@Configuration
@Profile("!test")
public class FcmConfig {

@Value("${fcm.key.path}")
Expand All @@ -20,7 +24,7 @@ public class FcmConfig {
@PostConstruct
public void init() {
try {
InputStream serviceAccount = new ClassPathResource(fcmKeyPath).getInputStream();
InputStream serviceAccount = getResourceStream();
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
Expand All @@ -31,4 +35,12 @@ public void init() {
throw new RuntimeException("FCM 초기화 실패", e);
}
}

private InputStream getResourceStream() throws IOException {
// 경로가 /로 시작하면 외부 파일 시스템(prod), 아니면 클래스패스(local)에서 읽도록 구성
if (fcmKeyPath.startsWith("/")) {
return new FileInputStream(fcmKeyPath);
}
return new ClassPathResource(fcmKeyPath).getInputStream();
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/meetkey/server/global/config/FeignConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.meetkey.server.global.config;

import feign.Logger;
import feign.slf4j.Slf4jLogger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

@Bean
public Logger feignLogger() {
return new Slf4jLogger();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.meetkey.server.global.config;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;

public class RecordSupportingTypeResolver extends ObjectMapper.DefaultTypeResolverBuilder {
public RecordSupportingTypeResolver(ObjectMapper.DefaultTyping t, PolymorphicTypeValidator ptv) {
super(t, ptv);
}

@Override
public boolean useForType(JavaType t){
boolean isRecord = t.getRawClass().isRecord();
boolean superResult = super.useForType(t);

if (isRecord) {
return true;
}
return superResult;
}
}
71 changes: 70 additions & 1 deletion src/main/java/com/meetkey/server/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
package com.meetkey.server.global.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public ObjectMapper redisObjectMapper() {
Expand All @@ -22,7 +40,20 @@ public ObjectMapper redisObjectMapper() {
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper redisObjectMapper) {
public RedisConnectionFactory RedisConnectionFactory(){
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);

LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
factory.afterPropertiesSet();
return factory;
}

@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory,
ObjectMapper redisObjectMapper) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

Expand All @@ -35,6 +66,44 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connec

return template;
}

// SMS, RefreshToken용
@Bean
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
return template;
}

// OIDC 공개용 키 저장
@Bean
public RedisCacheManager oidcCacheManager(
RedisConnectionFactory connectionFactory
){
ObjectMapper cacheMapper = new ObjectMapper();
RecordSupportingTypeResolver typeResolver = new RecordSupportingTypeResolver(ObjectMapper.DefaultTyping.NON_FINAL, cacheMapper.getPolymorphicTypeValidator());
StdTypeResolverBuilder initializedResolver = typeResolver.init(JsonTypeInfo.Id.CLASS, null);
initializedResolver = initializedResolver.inclusion(JsonTypeInfo.As.PROPERTY);
cacheMapper.setDefaultTyping(initializedResolver);

RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(cacheMapper))
)
// 키 앞에 "jwk:" 접두사를 붙임
.computePrefixWith(cacheName -> "jwk:" + cacheName + "::")
.entryTtl(Duration.ofDays(3));

return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(config)
.build();
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public record OIDCDecodePayload (
public record OIDCPublicKey (
// JWK
String kid,
String kty,
String alg,
String use,
String n,
String e
){}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.meetkey.server.global.security.oauth.kakao;

import com.meetkey.server.global.config.FeignConfig;
import com.meetkey.server.global.config.OauthConfig;
import com.meetkey.server.global.security.oauth.dto.OidcDTO;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(
name = "KakaoAuthClient",
url = "https://kauth.kakao.com",
configuration = OauthConfig.class
configuration = {OauthConfig.class, FeignConfig.class}
)
public interface KakaoOauthClient {
// @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager")
@Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager")
@GetMapping("/.well-known/jwks.json")
OidcDTO.OIDCPublicKeys getKakaoOIDCOpenKeys();
}
6 changes: 2 additions & 4 deletions src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@

spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}

host: localhost
port: 6379
jwt:
secret: ${JWT_SECRET_KEY:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}

Expand Down
Loading