diff --git a/src/main/java/com/retrip/auth/application/config/SecurityConfig.java b/src/main/java/com/retrip/auth/application/config/SecurityConfig.java index 357a3e3..0d56265 100644 --- a/src/main/java/com/retrip/auth/application/config/SecurityConfig.java +++ b/src/main/java/com/retrip/auth/application/config/SecurityConfig.java @@ -1,11 +1,12 @@ package com.retrip.auth.application.config; -import com.retrip.auth.application.in.MemberService; +import com.retrip.auth.application.in.MemberQueryService; import com.retrip.auth.infra.adapter.in.rest.filter.JwtAuthenticationFilter; import com.retrip.auth.infra.adapter.in.rest.filter.LoginAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -30,16 +31,17 @@ public LoginAuthenticationFilter loginAuthenticationFilter(JwtConfig jwtConfig, } @Bean - public JwtAuthenticationFilter jwtAuthenticationFilter(JwtConfig jwtConfig){ + public JwtAuthenticationFilter jwtAuthenticationFilter(JwtConfig jwtConfig) { return new JwtAuthenticationFilter(jwtConfig); } + @Bean public AuthenticationManager authenticationManager( HttpSecurity http, UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider, - MemberService memberService) throws Exception { + MemberQueryService memberQueryService) throws Exception { return http.authenticationProvider(usernamePasswordAuthenticationProvider) - .userDetailsService(memberService) + .userDetailsService(memberQueryService) .getSharedObject(AuthenticationManagerBuilder.class) .build(); } @@ -50,7 +52,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, LoginAuthentic http.csrf(AbstractHttpConfigurer::disable) .addFilterAt(loginAuthenticationFilter, BasicAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()); + http.authorizeHttpRequests(auth -> { + auth + .requestMatchers(HttpMethod.POST, "users").permitAll() + .requestMatchers( + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**" + ).permitAll() + .anyRequest().authenticated(); + } + ); return http.build(); } diff --git a/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java index 8efb427..1cb7c4e 100644 --- a/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java +++ b/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java @@ -1,6 +1,6 @@ package com.retrip.auth.application.config; -import com.retrip.auth.application.in.MemberService; +import com.retrip.auth.application.in.MemberQueryService; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; @@ -15,7 +15,7 @@ @RequiredArgsConstructor public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { - private final MemberService memberService; + private final MemberQueryService memberQueryService; private final PasswordEncoder passwordEncoder; @Override @@ -24,7 +24,7 @@ public Authentication authenticate(Authentication authentication) String username = authentication.getName(); String password = String.valueOf(authentication.getCredentials()); - UserDetails user = memberService.loadUserByUsername(username); + UserDetails user = memberQueryService.loadUserByUsername(username); if (!passwordEncoder.matches(password, user.getPassword())) { throw new BadCredentialsException("Bad credentials"); diff --git a/src/main/java/com/retrip/auth/application/in/MemberQueryService.java b/src/main/java/com/retrip/auth/application/in/MemberQueryService.java new file mode 100644 index 0000000..4dfd53c --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/MemberQueryService.java @@ -0,0 +1,26 @@ +package com.retrip.auth.application.in; + +import com.retrip.auth.application.config.CustomUserDetails; +import com.retrip.auth.application.out.repository.MemberQueryRepository; +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.exception.MemberNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberQueryService implements UserDetailsService { + + private final MemberQueryRepository memberQueryRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Member member = memberQueryRepository.findByEmailWithAuthorities(username).orElseThrow(MemberNotFoundException::new); + return new CustomUserDetails(member); + } +} diff --git a/src/main/java/com/retrip/auth/application/in/MemberService.java b/src/main/java/com/retrip/auth/application/in/MemberService.java index e5a7d97..75ce6b7 100644 --- a/src/main/java/com/retrip/auth/application/in/MemberService.java +++ b/src/main/java/com/retrip/auth/application/in/MemberService.java @@ -1,28 +1,27 @@ package com.retrip.auth.application.in; -import com.retrip.auth.application.config.CustomUserDetails; +import com.retrip.auth.application.in.request.MemberCreateRequest; +import com.retrip.auth.application.in.response.MemberCreateResponse; import com.retrip.auth.application.in.usercase.ManageMemberUseCase; -import com.retrip.auth.application.out.repository.MemberQueryRepository; import com.retrip.auth.application.out.repository.MemberRepository; import com.retrip.auth.domain.entity.Member; -import com.retrip.auth.domain.exception.MemberNotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor @Transactional -public class MemberService implements ManageMemberUseCase, UserDetailsService { +public class MemberService implements ManageMemberUseCase { - private final MemberQueryRepository memberQueryRepository; + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - Member member = memberQueryRepository.findByEmailWithAuthorities(username).orElseThrow(MemberNotFoundException::new); - return new CustomUserDetails(member); + public MemberCreateResponse createUser(MemberCreateRequest request) { + String encode = passwordEncoder.encode(request.password()); + Member member = memberRepository.save(request.to(encode)); + return MemberCreateResponse.of(member); } } diff --git a/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java b/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java new file mode 100644 index 0000000..8337743 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java @@ -0,0 +1,18 @@ +package com.retrip.auth.application.in.request; + +import com.retrip.auth.domain.entity.Member; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Member 회원가입 Request") +public record MemberCreateRequest( + @Schema(description = "이메일") + String email, + @Schema(description = "비밀번호") + String password, + @Schema(description = "사용자 이름") + String name +) { + public Member to(String encodePassword) { + return Member.create(name, email, encodePassword); + } +} diff --git a/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java b/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java new file mode 100644 index 0000000..fbdd953 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java @@ -0,0 +1,15 @@ +package com.retrip.auth.application.in.response; + +import com.retrip.auth.domain.entity.Member; + +import java.util.UUID; + +public record MemberCreateResponse( + UUID id, + String email, + String name +) { + public static MemberCreateResponse of(Member member) { + return new MemberCreateResponse(member.getId(), member.getEmail().getValue(), member.getName().getValue()); + } +} diff --git a/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java b/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java index 5c3c073..bf3790a 100644 --- a/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java +++ b/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java @@ -1,6 +1,9 @@ package com.retrip.auth.application.in.usercase; +import com.retrip.auth.application.in.request.MemberCreateRequest; +import com.retrip.auth.application.in.response.MemberCreateResponse; public interface ManageMemberUseCase { + MemberCreateResponse createUser(MemberCreateRequest request); } diff --git a/src/main/java/com/retrip/auth/domain/entity/Member.java b/src/main/java/com/retrip/auth/domain/entity/Member.java index 010c883..aca4b04 100644 --- a/src/main/java/com/retrip/auth/domain/entity/Member.java +++ b/src/main/java/com/retrip/auth/domain/entity/Member.java @@ -4,13 +4,11 @@ import com.retrip.auth.domain.vo.MemberName; import com.retrip.auth.domain.vo.MemberPassword; import jakarta.persistence.*; +import lombok.*; import java.util.List; import java.util.UUID; -import lombok.*; -import org.springframework.security.crypto.password.PasswordEncoder; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Builder @@ -37,7 +35,7 @@ public class Member extends BaseEntity { private Authorities authorities; - public static Member create(String name, String email, String password, List authorities) { + public static Member create(String name, String email, String password, List authorities) { Member member = Member.builder() .id(UUID.randomUUID()) .name(new MemberName(name)) @@ -47,4 +45,16 @@ public static Member create(String name, String email, String password, List URI = List.of("/login", "/users"); private final JwtConfig jwtConfig; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token = getToken(request.getHeader("Authorization")); + String token = getToken(request.getHeader(AUTHORIZATION_HEADER)); SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8)); if (token == null || !validToken(token, key)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); @@ -86,6 +88,10 @@ private List getAuthorities(String authorities) { @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { //로그인 제외 모든 필터 타기 - return request.getRequestURI().equals("/login"); + return URI.contains(request.getRequestURI()) + || request.getRequestURI().startsWith("/swagger-ui") + || request.getRequestURI().startsWith("/v3/api-docs") + || request.getRequestURI().startsWith("/swagger-resources") + || request.getRequestURI().startsWith("/webjars"); } } diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java new file mode 100644 index 0000000..fdf1e8a --- /dev/null +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java @@ -0,0 +1,29 @@ +package com.retrip.auth.infra.adapter.in.rest.in; + +import com.retrip.auth.application.in.request.MemberCreateRequest; +import com.retrip.auth.application.in.response.MemberCreateResponse; +import com.retrip.auth.application.in.usercase.ManageMemberUseCase; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +@Tag(name = "User", description = "회원 관련 API") +public class MemberController { + + private final ManageMemberUseCase manageMemberUseCase; + + @PostMapping + @Schema(description = "회원 가입") + public MemberCreateResponse createUser( + @RequestBody MemberCreateRequest request + ){ + return manageMemberUseCase.createUser(request); + } +} diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/TestController.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/TestController.java index af14c19..c370a7d 100644 --- a/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/TestController.java +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/TestController.java @@ -1,5 +1,7 @@ package com.retrip.auth.infra.adapter.in.rest.in; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -7,6 +9,8 @@ @RestController @RequestMapping("/test") +@RequiredArgsConstructor +@Tag(name = "Test", description = "테스트 관련 API") public class TestController { @GetMapping public String test(Authentication authentication){ diff --git a/src/main/java/com/retrip/auth/infra/config/SwaggerConfig.java b/src/main/java/com/retrip/auth/infra/config/SwaggerConfig.java index b35fa66..39e3b87 100644 --- a/src/main/java/com/retrip/auth/infra/config/SwaggerConfig.java +++ b/src/main/java/com/retrip/auth/infra/config/SwaggerConfig.java @@ -23,11 +23,4 @@ public OpenAPI springShopOpenAPI() { .version("v0.0.1") ); } - @Bean - public GroupedOpenApi authApi(){ - return GroupedOpenApi.builder() - .group("auth") - .pathsToMatch("/auth/**") - .build(); - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 79a9a59..398c679 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,3 +38,7 @@ token: expire-min: 120 refresh: expire-min: 1051200 #2년 + +springdoc: + swagger-ui: + use-root-path: true diff --git a/src/test/java/com/retrip/auth/application/in/MemberServiceTest.java b/src/test/java/com/retrip/auth/application/in/MemberServiceTest.java new file mode 100644 index 0000000..46d17ce --- /dev/null +++ b/src/test/java/com/retrip/auth/application/in/MemberServiceTest.java @@ -0,0 +1,32 @@ +package com.retrip.auth.application.in; + + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import com.retrip.auth.application.in.request.MemberCreateRequest; +import com.retrip.auth.application.in.response.MemberCreateResponse; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MemberServiceTest { + + @Autowired + private MemberService memberService; + + @Test + void 회원가입_성공() throws Exception { + // given + MemberCreateRequest request = new MemberCreateRequest("test@naver.com", "1234", "test"); + + //when + MemberCreateResponse response = memberService.createUser(request); + + //then + assertThat(response.id()).isNotNull(); + assertThat(response.name()).isEqualTo("test"); + assertThat(response.email()).isEqualTo("test@naver.com"); + } + +} diff --git a/src/test/java/com/retrip/auth/application/in/factory/BaseMemberServiceTest.java b/src/test/java/com/retrip/auth/application/in/factory/BaseMemberServiceTest.java new file mode 100644 index 0000000..ffdd347 --- /dev/null +++ b/src/test/java/com/retrip/auth/application/in/factory/BaseMemberServiceTest.java @@ -0,0 +1,18 @@ +package com.retrip.auth.application.in.factory; + +import com.retrip.auth.application.in.MemberService; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class BaseMemberServiceTest { + @Autowired + protected MemberService memberService; + + + + @BeforeEach + void setUp() { + } +} diff --git a/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilterTest.java b/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilterTest.java index b687694..2e2b6c7 100644 --- a/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilterTest.java +++ b/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilterTest.java @@ -1,7 +1,9 @@ package com.retrip.auth.infra.adapter.in.rest.filter; +import com.fasterxml.jackson.databind.ObjectMapper; import com.retrip.auth.application.in.request.LoginRequest; +import com.retrip.auth.application.in.request.MemberCreateRequest; import com.retrip.auth.infra.adapter.in.rest.filter.base.BaseLoginAuthenticationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -44,4 +46,22 @@ class LoginAuthenticationFilterTest extends BaseLoginAuthenticationTest { .andExpect(jsonPath("$.accessToken").isNotEmpty()) .andExpect(jsonPath("$.refreshToken").isNotEmpty()); } + + @Test + void 유저_생성_성공() throws Exception { + // given + MemberCreateRequest request = new MemberCreateRequest("test@naver.com", "1234", "test"); + + //when + String json = new ObjectMapper().writeValueAsString(request); + + // when & then + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.email").isNotEmpty()); + } } diff --git a/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/base/BaseLoginAuthenticationTest.java b/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/base/BaseLoginAuthenticationTest.java index ff58e90..b14b5e0 100644 --- a/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/base/BaseLoginAuthenticationTest.java +++ b/src/test/java/com/retrip/auth/infra/adapter/in/rest/filter/base/BaseLoginAuthenticationTest.java @@ -1,7 +1,7 @@ package com.retrip.auth.infra.adapter.in.rest.filter.base; import com.querydsl.jpa.impl.JPAQueryFactory; -import com.retrip.auth.application.in.MemberService; +import com.retrip.auth.application.in.MemberQueryService; import com.retrip.auth.application.out.repository.MemberQueryRepository; import com.retrip.auth.application.out.repository.MemberRepository; import com.retrip.auth.domain.entity.Member; @@ -23,14 +23,14 @@ public abstract class BaseLoginAuthenticationTest { @Autowired protected JPAQueryFactory jpaQueryFactory; protected MemberQueryRepository memberQueryRepository; - protected MemberService memberService; + protected MemberQueryService memberQueryService; protected Member member; @BeforeEach void setUp() { memberQueryRepository = new MemberQuerydslRepository(jpaQueryFactory); - memberService = new MemberService(memberQueryRepository); + memberQueryService = new MemberQueryService(memberQueryRepository); member = Member.create( "테스트", "test@naver.com",