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: 7 additions & 0 deletions account-service/account-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ repositories {

dependencies {
implementation project(':account-service-api')

//Spring Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
//Spring Prometheus
implementation 'io.micrometer:micrometer-registry-prometheus'
//Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

def generated = 'src/main/generated'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@EnableJpaAuditing
@ComponentScan
@EnableAutoConfiguration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.synapse.account_service.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.synapse.account_service.domain.RefreshToken;

@Configuration
public class RedisConfig {
@Bean
RedisTemplate<String, RefreshToken> refreshTokenRedisTemplate(RedisConnectionFactory connectionFactory) {
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);

var template = new RedisTemplate<String, RefreshToken>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, RefreshToken.class));

return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final LoginSuccessHandler loginSuccessHandler;
private final LoginFailureHandler loginFailureHandler;
private final ObjectMapper objectMapper;
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomOidcUserService customOidcUserService;
private final ObjectMapper objectMapper;
private final PasswordEncoder passwordEncoder;


Expand All @@ -57,7 +57,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat

.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/accounts/signup", "/api/accounts/login", "/",
"/api/accounts/token/reissue")
"/api/accounts/token/reissue",
"/actuator/health", "/actuator/info",
"/actuator/prometheus", "/actuator/metrics", "/actuator/mappings")
.permitAll()
.anyRequest().authenticated())
.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.synapse.account_service.service.AccountService;
import com.synapse.account_service_api.dto.request.SignUpRequest;
import com.synapse.account_service_api.dto.response.SignUpResponse;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.synapse.account_service.domain;

import java.util.UUID;

import lombok.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {
private UUID memberId;
private String token;

@Builder
public RefreshToken(UUID memberId, String token) {
this.memberId = memberId;
this.token = token;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "members")
@Table(name = "members", indexes = {
@Index(name = "idx_member_username", columnList = "username")
})
public class Member extends BaseEntity {

@Id
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.synapse.account_service.exception.ExceptionType;
import com.synapse.account_service_api.dto.request.SignUpRequest;
import com.synapse.account_service_api.dto.response.SignUpResponse;

import com.synapse.account_service_api.event.MemberDomainEvent;

import io.eventuate.tram.events.aggregates.ResultWithDomainEvents;
Expand All @@ -32,6 +33,7 @@ public class AccountService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

// private final MemberDomainEventPublisher memberDomainEventPublisher;

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx
ProviderUserRequest providerUserRequest = new ProviderUserRequest(member);
ProviderUser providerUser = providerUser(providerUserRequest);

return new PrincipalUser(providerUser);
return new PrincipalUser(providerUser, member);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.synapse.account_service.service;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import java.util.UUID;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.synapse.account_service.domain.RefreshToken;
import com.synapse.account_service.domain.entity.Member;
import com.synapse.account_service.domain.entity.RefreshToken;
import com.synapse.account_service.domain.repository.MemberRepository;
import com.synapse.account_service.domain.repository.RefreshTokenRepository;
import com.synapse.account_service.exception.ExceptionType;
import com.synapse.account_service.exception.JWTValidationException;
import com.synapse.account_service.exception.NotFoundException;
Expand All @@ -22,29 +26,28 @@
@RequiredArgsConstructor
public class TokenManagementService {
private final JwtTokenService jwtTokenService;
private final RefreshTokenRepository refreshTokenRepository;
private final MemberRepository memberRepository;
private final RedisTemplate<String, RefreshToken> refreshTokenRedisTemplate;

public void saveOrUpdateRefreshToken(UUID memberId, TokenResult refreshToken) {
refreshTokenRepository.findById(memberId)
.ifPresentOrElse(
// 기존 토큰이 있으면, 새 토큰으로 값을 업데이트 (재로그인 시)
existingToken -> existingToken.updateToken(refreshToken.token()),
// 기존 토큰이 없으면, 새로 생성하여 저장 (최초 로그인)
() -> {
RefreshToken newRefreshToken = new RefreshToken(memberId, refreshToken.token());
refreshTokenRepository.save(newRefreshToken);
});
String redisKey = "refresh_token:" + memberId.toString();
RefreshToken refreshTokenEntity = new RefreshToken(memberId, refreshToken.token());

long ttlSeconds = ChronoUnit.SECONDS.between(Instant.now(), refreshToken.expiresAt());
refreshTokenRedisTemplate.opsForValue().set(redisKey, refreshTokenEntity, ttlSeconds, TimeUnit.SECONDS);
}

public TokenResponse reissueTokens(String requestRefreshToken) {
UUID memberId = jwtTokenService.getMemberIdFrom(requestRefreshToken);
String redisKey = "refresh_token:" + memberId.toString();

RefreshToken storedToken = refreshTokenRepository.findById(memberId)
.orElseThrow(() -> new JWTValidationException(ExceptionType.INVALID_REFRESH_TOKEN));
RefreshToken storedToken = refreshTokenRedisTemplate.opsForValue().get(redisKey);
if (storedToken == null) {
throw new JWTValidationException(ExceptionType.INVALID_REFRESH_TOKEN);
}

if (!storedToken.getToken().equals(requestRefreshToken)) {
refreshTokenRepository.delete(storedToken);
refreshTokenRedisTemplate.delete(redisKey);
throw new JWTValidationException(ExceptionType.TAMPERED_REFRESH_TOKEN);
}

Expand All @@ -55,7 +58,10 @@ public TokenResponse reissueTokens(String requestRefreshToken) {

TokenResponse newTokens = jwtTokenService.createTokenResponse(memberId.toString(), role);

storedToken.updateToken(newTokens.refreshToken().token());
// Redis에 새로운 RefreshToken 저장
RefreshToken newRefreshToken = new RefreshToken(memberId, newTokens.refreshToken().token());
long ttlSeconds = Duration.between(Instant.now(), newTokens.refreshToken().expiresAt()).getSeconds();
refreshTokenRedisTemplate.opsForValue().set(redisKey, newRefreshToken, ttlSeconds, TimeUnit.SECONDS);

return newTokens;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ spring:
url: jdbc:postgresql://${local-db.postgres.host}:${local-db.postgres.port}/${local-db.postgres.name}
username: ${local-db.postgres.username}
password: ${local-db.postgres.password}
hikari:
maximum-pool-size: 70
minimum-idle: 10

data:
redis:
host: ${local-redis.host}
port: ${local-redis.port}
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8

jpa:
properties:
Expand All @@ -13,8 +26,13 @@ spring:
highlight:
sql: true
hbm2ddl:
auto: create
auto: update
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
batch_size: 30
order_inserts: true
order_updates: true
default_batch_fetch_size: 100
open-in-view: false
show-sql: true

Expand Down
21 changes: 21 additions & 0 deletions account-service/account-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
server:
port: 1001
tomcat:
mbeanregistry:
enabled: true

spring:
main:
Expand All @@ -16,3 +19,21 @@ spring:
- security/application-db.yml
- security/application-jwt.yml
- security/application-oauth2.yml

management:
server:
port: 6001
endpoints:
web:
exposure:
include: health,info,prometheus,mappings, metrics
endpoint:
health:
show-details: always
prometheus:
metrics:
export:
enabled: true
metrics:
tags:
application: ${spring.application.name}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 로그인 시 username 조회 쿼리 플랜
EXPLAIN SELECT * FROM members WHERE username = 'test';

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.UUID;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

Expand Down Expand Up @@ -41,14 +44,16 @@ public class LoginIntegrationTest extends TestConfig{

@BeforeEach
void setUp() {
memberRepository.deleteAll();
Member testMember = Member.builder()
.id(UUID.randomUUID())
.email("test_user1234@example.com")
.username(TEST_USERNAME)
.password(passwordEncoder.encode(TEST_PASSWORD))
.role(MemberRole.USER)
.provider("local")
.build();
memberRepository.save(testMember);
testMember = memberRepository.save(testMember);
}

@Test
Expand Down
Loading
Loading