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
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import static umc.th.juinjang.common.code.status.ErrorStatus.*;

import java.util.Map;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -81,4 +84,10 @@ public ApiResponse<Void> createMemberAgreeVersion(@AuthenticationPrincipal Membe
return ApiResponse.onSuccess(null);
}

@Operation(summary = "닉네임 중복 여부")
@GetMapping("/members/nickname/exists")
public ApiResponse<Map<String, Boolean>> isNicknameExists(@RequestParam String nickname) {
return ApiResponse.onSuccess(Map.of("exists", memberService.isNicknameExists(nickname)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public void createMemberAgreeVersion(final Member member,
getMember(member).updateAgreeVersion(memberAgreeVersionPostRequest.agreeVersion());
}

public boolean isNicknameExists(String nickname) {
return memberRepository.existsByNickname(nickname);
}

private Member getMember(Member member) {
return memberRepository.findById(member.getMemberId()).orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND));
}
Expand Down
166 changes: 94 additions & 72 deletions src/main/java/umc/th/juinjang/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package umc.th.juinjang.auth.config;

import lombok.RequiredArgsConstructor;
import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
Expand All @@ -16,85 +17,106 @@
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import lombok.RequiredArgsConstructor;
import umc.th.juinjang.auth.jwt.JwtAuthenticationFilter;
import umc.th.juinjang.auth.jwt.JwtExceptionFilter;
import umc.th.juinjang.auth.jwt.JwtService;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;

private final JwtService jwtService;

private final JwtExceptionFilter jwtExceptionFilter;

private final Environment environment;
@Bean
@Order(0)
public WebSecurityCustomizer webSecurityCustomizer(){
String[] activeProfiles = environment.getActiveProfiles();
boolean isProd = Arrays.asList(activeProfiles).contains("prod");

//prod아닐때
if (!isProd) {
return web -> web.ignoring()
.requestMatchers("/swagger-ui/**", "/swagger/**", "/swagger-resources/**", "/swagger-ui.html", "/test",
"/configuration/ui", "/v3/api-docs/**", "/h2-console/**", "/api/auth/regenerate-token",
"/api/auth/kakao/**", "/api/auth/apple/**", "/actuator/prometheus",
"/api/auth/v2/apple/**", "/api/auth/v2/kakao/**");
}
else {
return web -> web.ignoring()
.requestMatchers("/h2-console/**", "/api/auth/regenerate-token",
"/api/auth/kakao/**", "/api/auth/apple/**", "/actuator/prometheus",
"/api/auth/v2/apple/**", "/api/auth/v2/kakao/**");
}

}

//선언 방식이 3.x에서 바뀜
@Bean AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception
{ return authConfiguration.getAuthenticationManager(); }

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(Customizer.withDefaults())
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 세션을 사용하지 않는다고 설정함
)
.addFilter(new JwtAuthenticationFilter(authenticationManager(authenticationConfiguration),jwtService))
// JwtAuthenticationFilter를 필터에 넣음
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests
.requestMatchers(
AntPathRequestMatcher.antMatcher("/api/auth/**")
).authenticated()
.requestMatchers(
AntPathRequestMatcher.antMatcher("/h2-console/**")
).permitAll()

.anyRequest().authenticated()

)
.headers(
headersConfigurer ->
headersConfigurer
.frameOptions(
HeadersConfigurer.FrameOptionsConfig::sameOrigin
)
)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);

return http.build();
}
private final AuthenticationConfiguration authenticationConfiguration;

private final JwtService jwtService;

private final JwtExceptionFilter jwtExceptionFilter;

private final Environment environment;

// 공통적으로 허용되는 URL 패턴
private static final String[] COMMON_WHITELIST_URLS = {
"/h2-console/**",
"/api/auth/regenerate-token",
"/api/auth/kakao/**",
"/api/auth/apple/**",
"/actuator/prometheus",
"/api/auth/v2/apple/**",
"/api/auth/v2/kakao/**",
"/api/members/nickname/exists"
};

// 개발 환경에서만 추가로 허용되는 URL 패턴
private static final String[] DEV_WHITELIST_URLS = {
"/swagger-ui/**",
"/swagger/**",
"/swagger-resources/**",
"/swagger-ui.html",
"/test",
"/configuration/ui",
"/v3/api-docs/**"
};

@Bean
@Order(0)
public WebSecurityCustomizer webSecurityCustomizer() {
String[] activeProfiles = environment.getActiveProfiles();
boolean isProd = Arrays.asList(activeProfiles).contains("prod");

//prod아닐때
if (!isProd) {
return web -> web.ignoring()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

오늘도 테스트 코드 잘봤습니다!! 이부분 따로 list로 만들어서 관리해도 좋을 것 같다는 의견 내봅니다

    private static final String[] authWhiteList = {"/api/v2", ..생략};
    ...생략

.requestMatchers(authWhiteList);

.requestMatchers(COMMON_WHITELIST_URLS)
.requestMatchers(DEV_WHITELIST_URLS);
} else {
return web -> web.ignoring()
.requestMatchers(COMMON_WHITELIST_URLS);
}

}

//선언 방식이 3.x에서 바뀜
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception {
return authConfiguration.getAuthenticationManager();
}

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(Customizer.withDefaults())
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 세션을 사용하지 않는다고 설정함
)
.addFilter(new JwtAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtService))
// JwtAuthenticationFilter를 필터에 넣음
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests
.requestMatchers(
AntPathRequestMatcher.antMatcher("/api/members/nickname/exists"),
AntPathRequestMatcher.antMatcher("/h2-console/**")
).permitAll()
.requestMatchers(
AntPathRequestMatcher.antMatcher("/api/auth/**")
).authenticated()
.anyRequest().authenticated()

)
.headers(
headersConfigurer ->
headersConfigurer
.frameOptions(
HeadersConfigurer.FrameOptionsConfig::sameOrigin
)
)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);

return http.build();
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying
@Query("UPDATE Member m SET m.introduction = :introduction WHERE m.memberId = :id")
void patchIntroduction(@Param("id") Long id, @Param("introduction") String introduction);

boolean existsByNickname(String nickname);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package umc.th.juinjang.api.member.controller;

import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;

import com.fasterxml.jackson.databind.ObjectMapper;

import umc.th.juinjang.api.member.service.MemberService;

@WebMvcTest(MemberController.class)
@WithMockUser
class MemberControllerTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private MemberService memberService;

@DisplayName("닉네임 중복 체크 API - 중복되지 않은 닉네임")
@Test
void checkNickname_whenNicknameDoesNotExist_thenReturnFalse() throws Exception {
// given
String nickname = "newNickname";
given(memberService.isNicknameExists(nickname)).willReturn(false);

// when & then
mockMvc.perform(get("/api/members/nickname/exists")
.param("nickname", nickname)
)
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.result.exists").value(false));
}

@DisplayName("닉네임 중복 체크 API - 중복된 닉네임")
@Test
void checkNickname_whenNicknameExists_thenReturnTrue() throws Exception {
// given
String nickname = "existingNickname";
given(memberService.isNicknameExists(nickname)).willReturn(true);

// when & then
mockMvc.perform(get("/api/members/nickname/exists")
.param("nickname", nickname))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.result.exists").value(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import static org.assertj.core.api.Assertions.*;

import java.time.LocalDateTime;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
Expand All @@ -16,6 +19,7 @@
import umc.th.juinjang.domain.member.model.MemberProvider;
import umc.th.juinjang.domain.member.repository.MemberRepository;
import umc.th.juinjang.domain.pencilaccount.repository.PencilAccountRepository;
import umc.th.juinjang.testutil.fixture.MemberFixture;

@ActiveProfiles("test")
@SpringBootTest
Expand All @@ -32,8 +36,8 @@ public class MemberServiceTest {

@AfterEach
void tearDown() {
memberRepository.deleteAllInBatch();
pencilAccountRepository.deleteAllInBatch();
memberRepository.deleteAllInBatch();
}

private final String DEFAULT_EMAIL = "test@naver.com";
Expand Down Expand Up @@ -73,6 +77,59 @@ void patchIntroduction() {
assertThat(updatedMember.getIntroduction()).isEqualTo(changedIntroduction);
}

@Nested
@DisplayName("닉네임 중복 검사")
class NicknameExistsTest {

// static 필드로 선언
private static String existingNickname;

@BeforeEach
void setUp() {
// given - 여러 멤버 데이터 한 번만 설정
existingNickname = "테스트1";
String nickname2 = "테스트2";
String nickname3 = "테스트3";

Member member1 = MemberFixture.createMemberWithParams(
"custom1@example.com", 11111111L, existingNickname,
"안녕하세요", "https://custom.image.url");

Member member2 = MemberFixture.createMemberWithParams(
"custom2@example.com", 2222222L, nickname2,
"안녕하세요", "https://custom.image.url");

Member member3 = MemberFixture.createMemberWithParams(
"custom3@example.com", 3333333L, nickname3,
"안녕하세요", "https://custom.image.url");

memberRepository.saveAll(List.of(member1, member2, member3));
}

@DisplayName("닉네임이 중복되었을 때, 중복 여부를 True 로 반환한다")
@Test
void returnsTrueWhenNicknameExists() {
// when
boolean result = memberService.isNicknameExists(existingNickname);

// then
assertThat(result).isTrue();
}

@DisplayName("닉네임이 중복되지 않았을 때, 중복 여부를 False 로 반환한다")
@Test
void returnsFalseWhenNicknameDoesNotExist() {
// given
String nonExistingNickname = "존재하지않는닉네임";

// when
boolean result = memberService.isNicknameExists(nonExistingNickname);

// then
assertThat(result).isFalse();
}
}

private Member createDefaultMember() {
return Member.builder()
.email(DEFAULT_EMAIL)
Expand Down
Loading