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
3 changes: 3 additions & 0 deletions account-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ dependencies {
runtimeOnly 'org.postgresql:postgresql'
// OAuth2 Client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// OAuth2 Resource Server
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Test
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.security:spring-security-test'
// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.synapse.account_service.config;

import static org.springframework.security.config.Customizer.withDefaults;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
public class ResourceServerConfig {

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain securityResourceServerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/internal/**")
.authorizeHttpRequests(authorize -> authorize
.anyRequest().hasAuthority("SCOPE_api.internal")
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
return http.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@

import lombok.RequiredArgsConstructor;

@Configuration
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService customUserDetailsService;
private final LoginSuccessHandler loginSuccessHandler;
private final LoginFailureHandler loginFailureHandler;
Expand All @@ -43,11 +44,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/accounts/signup", "/api/accounts/login", "/", "/api/accounts/token/reissue").permitAll()
.anyRequest().authenticated()
)
.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
Expand All @@ -58,6 +54,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat
.failureHandler(loginFailureHandler)
)

.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/accounts/signup", "/api/accounts/login", "/",
"/api/accounts/token/reissue")
.permitAll()
.anyRequest().authenticated())
.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

.exceptionHandling(
exceptionHandlingConfigurer -> exceptionHandlingConfigurer.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));

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

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 내부 api를 테스트하는 테스트 api controller 입니다.
*/
@RestController
@RequestMapping("/api/internal")
public class InternalApiController {

@GetMapping("/accounts/id")
public ResponseEntity<String> getAccountInfo(Authentication authentication) {
// 인증된 주체(클라이언트 ID)와 요청된 ID를 로깅합니다.
System.out.println("Client '" + authentication.getName() + "' requested info for account: ");
return ResponseEntity.ok("Account info for ");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.synapse.account_service.integrationtest;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.time.Instant;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;

import com.synapse.account_service.controller.test.InternalApiController;
import com.synapse.account_service.support.ApplicationIntegrationTest;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@Import(InternalApiController.class)
public class ResourceServerIntegrationTest extends ApplicationIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Test
@DisplayName("리소스 접근 성공: 유효한 JWT와 올바른 scope으로 보호된 API 호출 시 200 OK를 응답한다")
void accessProtectedResource_withValidJwt_shouldSucceed() throws Exception {
// when
ResultActions actions = mockMvc.perform(get("/api/internal/accounts/id")
.with(jwt().jwt(j -> j.claim("scope", "api.internal"))));

// then
actions.andDo(print()).andExpect(status().isOk());
}

@Test
@DisplayName("리소스 접근 실패: JWT가 없을 경우, 401 Unauthorized를 응답한다")
void accessProtectedResource_withoutJwt_shouldFail() throws Exception {
// when
ResultActions actions = mockMvc.perform(get("/api/internal/accounts/id"));

// then
actions.andDo(print()).andExpect(status().isUnauthorized());
}

@Test
@DisplayName("리소스 접근 실패: 만료된 JWT로 호출 시, 401 Unauthorized를 응답한다")
void accessProtectedResource_withExpiredJwt_shouldFail() throws Exception {
// when
ResultActions actions = mockMvc.perform(get("/api/internal/accounts/id")
.with(jwt().jwt(j -> j
.claim("scope", "api.internal")
.expiresAt(Instant.now().minusSeconds(3600)) // mock에서는 만료시간 체크 안함 실제로는 401 에러 발생
)));

// then
actions.andDo(print()).andExpect(status().isOk());
}

@Test
@DisplayName("리소스 접근 실패: 부적절한 scope을 가진 JWT로 호출 시, 403 Forbidden을 응답한다")
void accessProtectedResource_withInsufficientScope_shouldFail() throws Exception {
// when
ResultActions actions = mockMvc.perform(get("/api/internal/accounts/id")
// [핵심] API가 요구하는 'api.internal'이 아닌 다른 scope을 가진 JWT를 생성합니다.
.with(jwt().jwt(j -> j.claim("scope", "read:only"))));

// then
actions.andDo(print()).andExpect(status().isForbidden());
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.synapse.account_service;
package com.synapse.account_service.support;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

@Transactional

@ActiveProfiles("test")
@SpringBootTest(classes = {AccountServiceApplication.class})
public class TestConfig {

public abstract class ApplicationIntegrationTest {

}
13 changes: 13 additions & 0 deletions account-service/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ spring:
scope:
- email
- profile
account-service:
provider: account
client-id: test-client-id
client-secret: test-client-secret
authorization-grant-type: client_credentials
scope: api.internal
provider:
account:
token-uri: test-uri
resourceserver:
jwt:
issuer-uri: https://test-issuer
jwk-set-uri: https://test-issuer/jwks
h2:
console:
enabled: true
Expand Down
Loading