diff --git a/build.gradle b/build.gradle index 175fe96..35b709a 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,9 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' } test { diff --git a/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java b/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java index 4cf8256..f04d851 100644 --- a/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java +++ b/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java @@ -1,5 +1,8 @@ package com.mycom.socket.auth.config; +import com.mycom.socket.auth.jwt.JWTFilter; +import com.mycom.socket.auth.jwt.JWTUtil; +import com.mycom.socket.auth.service.MemberDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,26 +11,33 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig{ + private final JWTUtil jwtUtil; + private final MemberDetailsService memberDetailsService; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .cors(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())) .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) + .addFilterBefore(new JWTFilter(jwtUtil, memberDetailsService), UsernamePasswordAuthenticationFilter.class) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers( - "/","/api/auth/**" + "/","/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**" ).permitAll() .anyRequest() .authenticated()); diff --git a/src/main/java/com/mycom/socket/auth/controller/AuthController.java b/src/main/java/com/mycom/socket/auth/controller/AuthController.java new file mode 100644 index 0000000..37e3618 --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/controller/AuthController.java @@ -0,0 +1,37 @@ +package com.mycom.socket.auth.controller; + +import com.mycom.socket.auth.dto.request.LoginRequestDto; +import com.mycom.socket.auth.dto.request.RegisterRequestDto; +import com.mycom.socket.auth.dto.response.LoginResponseDto; +import com.mycom.socket.auth.service.AuthService; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +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("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public LoginResponseDto login(@Valid @RequestBody LoginRequestDto request, + HttpServletResponse response) { + return authService.login(request, response); + } + + @PostMapping("/logout") + public void logout(HttpServletResponse response) { + authService.logout(response); + } + + @PostMapping("/register") + public Long register(@Valid @RequestBody RegisterRequestDto request) { + return authService.register(request); + } +} diff --git a/src/main/java/com/mycom/socket/auth/dto/request/.gitkeep b/src/main/java/com/mycom/socket/auth/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/mycom/socket/auth/dto/request/LoginRequestDto.java b/src/main/java/com/mycom/socket/auth/dto/request/LoginRequestDto.java new file mode 100644 index 0000000..18a035d --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/dto/request/LoginRequestDto.java @@ -0,0 +1,13 @@ +package com.mycom.socket.auth.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record LoginRequestDto( + @NotBlank(message = "이메일은 필수입니다") + @Email(message = "올바른 이메일 형식이 아닙니다") + String email, + + @NotBlank(message = "비밀번호는 필수입니다") + String password +) {} \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/go_socket/dto/request/MemberRegisterDto.java b/src/main/java/com/mycom/socket/auth/dto/request/RegisterRequestDto.java similarity index 89% rename from src/main/java/com/mycom/socket/go_socket/dto/request/MemberRegisterDto.java rename to src/main/java/com/mycom/socket/auth/dto/request/RegisterRequestDto.java index abc5b3d..b68b904 100644 --- a/src/main/java/com/mycom/socket/go_socket/dto/request/MemberRegisterDto.java +++ b/src/main/java/com/mycom/socket/auth/dto/request/RegisterRequestDto.java @@ -1,10 +1,10 @@ -package com.mycom.socket.go_socket.dto.request; +package com.mycom.socket.auth.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -public record MemberRegisterDto( +public record RegisterRequestDto( @NotBlank(message = "이메일은 필수입니다") @Email(message = "올바른 이메일 형식이 아닙니다") String email, diff --git a/src/main/java/com/mycom/socket/auth/dto/response/.gitkeep b/src/main/java/com/mycom/socket/auth/dto/response/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/mycom/socket/auth/dto/response/LoginResponseDto.java b/src/main/java/com/mycom/socket/auth/dto/response/LoginResponseDto.java new file mode 100644 index 0000000..e2cc1d4 --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/dto/response/LoginResponseDto.java @@ -0,0 +1,10 @@ +package com.mycom.socket.auth.dto.response; + +public record LoginResponseDto( + String email, + String nickname +) { + public static LoginResponseDto of(String email, String nickname) { + return new LoginResponseDto(email, nickname); + } +} diff --git a/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java b/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java new file mode 100644 index 0000000..55680a6 --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java @@ -0,0 +1,62 @@ +package com.mycom.socket.auth.jwt; + +import com.mycom.socket.auth.service.MemberDetailsService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JWTFilter extends OncePerRequestFilter { + + private final JWTUtil jwtUtil; + private final MemberDetailsService memberDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String token = resolveTokenFromCookie(request); + + try { + if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) { + String email = jwtUtil.getEmail(token); + UserDetails userDetails = memberDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + SecurityContextHolder.clearContext(); + } + + filterChain.doFilter(request, response); + } + + private String resolveTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("Authorization".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java b/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java new file mode 100644 index 0000000..2a6d57e --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java @@ -0,0 +1,60 @@ +package com.mycom.socket.auth.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +@Slf4j +public class JWTUtil { + + private final SecretKey secretKey; + + public JWTUtil(@Value("${jwt.secret}") String secret) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String createToken(String email) { + Claims claims = Jwts.claims().subject(email).build(); + Date now = new Date(); + // 30분 + long accessTokenValidityInMilliseconds = 1000 * 60 * 30; + Date validity = new Date(now.getTime() + accessTokenValidityInMilliseconds); + + return Jwts.builder() + .claims(claims) + .issuedAt(now) + .expiration(validity) + .signWith(secretKey) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return true; + } catch (Exception e) { + log.warn("JWT 토큰 검증 중 에러 발생: {}", e.getMessage()); + return false; + } + } + + public String getEmail(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/auth/security/LoginFilter.java b/src/main/java/com/mycom/socket/auth/security/LoginFilter.java new file mode 100644 index 0000000..e3e0900 --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/security/LoginFilter.java @@ -0,0 +1,85 @@ +package com.mycom.socket.auth.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mycom.socket.auth.jwt.JWTUtil; +import com.mycom.socket.global.dto.ApiResponse; +import com.mycom.socket.auth.dto.request.LoginRequestDto; +import com.mycom.socket.auth.dto.response.LoginResponseDto; +import com.mycom.socket.go_socket.entity.Member; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class LoginFilter extends UsernamePasswordAuthenticationFilter { + + private final JWTUtil jwtUtil; // JwtProvider 대신 JWTUtil 사용 + private final AuthenticationManager authenticationManager; + private final ObjectMapper objectMapper; + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { + try { + LoginRequestDto loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequestDto.class); + + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginRequest.email(), loginRequest.password()); + + return authenticationManager.authenticate(authenticationToken); + + } catch (IOException e) { + throw new RuntimeException("로그인 요청 처리 중 오류가 발생했습니다.", e); + } + } + + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, + FilterChain chain, Authentication authResult) throws IOException { + MemberDetails memberDetails = (MemberDetails) authResult.getPrincipal(); + Member member = memberDetails.getMember(); + + String token = jwtUtil.createToken(member.getEmail()); + + // HTTP Only 쿠키에 JWT 토큰 저장 + Cookie cookie = new Cookie("Authorization", token); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(1800); // 쿠키 만료시간 30분 + + // SameSite 속성 설정 추가 + response.setHeader("Set-Cookie", + String.format("Authorization=%s; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=1800", token)); + + LoginResponseDto loginResponse = new LoginResponseDto( + member.getEmail(), + member.getNickname() + ); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), ApiResponse.success("로그인 성공", loginResponse)); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + String errorMessage = "로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요."; + objectMapper.writeValue(response.getWriter(), ApiResponse.error(errorMessage)); + } +} \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/auth/security/MemberDetails.java b/src/main/java/com/mycom/socket/auth/security/MemberDetails.java new file mode 100644 index 0000000..3cd9fee --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/security/MemberDetails.java @@ -0,0 +1,55 @@ +package com.mycom.socket.auth.security; + +import com.mycom.socket.go_socket.entity.Member; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +@Getter +public class MemberDetails implements UserDetails { + + private final Member member; + + public MemberDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority(member.getRole().name())); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); // email을 username으로 사용 + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/auth/service/AuthService.java b/src/main/java/com/mycom/socket/auth/service/AuthService.java new file mode 100644 index 0000000..184f3b0 --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/service/AuthService.java @@ -0,0 +1,90 @@ +package com.mycom.socket.auth.service; + +import com.mycom.socket.auth.jwt.JWTUtil; +import com.mycom.socket.global.exception.BadRequestException; +import com.mycom.socket.global.exception.ConflictException; +import com.mycom.socket.auth.dto.request.LoginRequestDto; +import com.mycom.socket.auth.dto.request.RegisterRequestDto; +import com.mycom.socket.auth.dto.response.LoginResponseDto; +import com.mycom.socket.go_socket.entity.Member; +import com.mycom.socket.go_socket.entity.enums.MemberRole; +import com.mycom.socket.go_socket.repository.MemberRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JWTUtil jwtUtil; + + public LoginResponseDto login(LoginRequestDto request, HttpServletResponse response) { + Member member = memberRepository.findByEmail(request.email()) + .orElseThrow(() -> new BadRequestException("가입되지 않은 이메일입니다.")); + + if (!passwordEncoder.matches(request.password(), member.getPassword())) { + throw new BadRequestException("잘못된 비밀번호입니다."); + } + + String token = jwtUtil.createToken(member.getEmail()); + + // 쿠키 생성 및 설정 + Cookie cookie = new Cookie("Authorization", token); + cookie.setHttpOnly(true); // JavaScript에서 접근 불가 + cookie.setSecure(true); // HTTPS에서만 전송 + cookie.setPath("/"); // 모든 경로에서 접근 가능 + cookie.setMaxAge(1800); // 30분 + response.addCookie(cookie); + + return LoginResponseDto.of( + member.getEmail(), + member.getNickname() + ); + } + + // 이메일 인증 코드 전송 + + // 이메일 인증 코드 만료 + + @Transactional + public Long register(RegisterRequestDto request) { + // 이메일 중복 검사 + if (memberRepository.existsByEmail(request.email())) { + throw new ConflictException("이미 존재하는 이메일입니다."); + } + + // 닉네임 중복 검사 + if (memberRepository.existsByNickname(request.nickname())) { + throw new ConflictException("이미 존재하는 닉네임입니다."); + } + + // 이메일 인증 여부 확인 + + Member member = Member.builder() + .email(request.email()) + .password(passwordEncoder.encode(request.password())) + .nickname(request.nickname()) + .intro(request.intro()) + .role(MemberRole.USER) + .build(); + + Member savedMember = memberRepository.save(member); + return savedMember.getId(); + } + + public void logout(HttpServletResponse response) { + Cookie cookie = new Cookie("Authorization", null); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(0); // 즉시 만료 + response.addCookie(cookie); + } +} diff --git a/src/main/java/com/mycom/socket/auth/service/MemberDetailsService.java b/src/main/java/com/mycom/socket/auth/service/MemberDetailsService.java new file mode 100644 index 0000000..a5c4f48 --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/service/MemberDetailsService.java @@ -0,0 +1,25 @@ +package com.mycom.socket.auth.service; + +import com.mycom.socket.auth.security.MemberDetails; +import com.mycom.socket.go_socket.entity.Member; +import com.mycom.socket.go_socket.repository.MemberRepository; +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; + +@Service +@RequiredArgsConstructor +public class MemberDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + + return new MemberDetails(member); + } +} diff --git a/src/main/java/com/mycom/socket/go_socket/controller/RegisterController.java b/src/main/java/com/mycom/socket/go_socket/controller/RegisterController.java deleted file mode 100644 index 8f8a67f..0000000 --- a/src/main/java/com/mycom/socket/go_socket/controller/RegisterController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.mycom.socket.go_socket.controller; - -import com.mycom.socket.go_socket.dto.request.MemberRegisterDto; -import com.mycom.socket.go_socket.service.RegisterService; -import jakarta.validation.Valid; -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("/api/auth") -@RequiredArgsConstructor -public class RegisterController { - - private final RegisterService registerService; - - @PostMapping("/register") - public Long register(@Valid @RequestBody MemberRegisterDto request){ - return registerService.register(request); - } -} diff --git a/src/main/java/com/mycom/socket/go_socket/repository/MemberRepository.java b/src/main/java/com/mycom/socket/go_socket/repository/MemberRepository.java index 784ef40..1b2933f 100644 --- a/src/main/java/com/mycom/socket/go_socket/repository/MemberRepository.java +++ b/src/main/java/com/mycom/socket/go_socket/repository/MemberRepository.java @@ -3,7 +3,10 @@ import com.mycom.socket.go_socket.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { boolean existsByEmail(String email); boolean existsByNickname(String nickname); + Optional findByEmail(String email); } diff --git a/src/main/java/com/mycom/socket/go_socket/service/RegisterService.java b/src/main/java/com/mycom/socket/go_socket/service/RegisterService.java deleted file mode 100644 index e07adb9..0000000 --- a/src/main/java/com/mycom/socket/go_socket/service/RegisterService.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.mycom.socket.go_socket.service; - -import com.mycom.socket.global.exception.ConflictException; -import com.mycom.socket.go_socket.dto.request.MemberRegisterDto; -import com.mycom.socket.go_socket.entity.Member; -import com.mycom.socket.go_socket.entity.enums.MemberRole; -import com.mycom.socket.go_socket.repository.MemberRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class RegisterService { - - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - - // 이메일 인증 코드 전송 - - // 이메일 인증 코드 만료 - - @Transactional - public Long register(MemberRegisterDto request) { - // 이메일 중복 검사 - if (memberRepository.existsByEmail(request.email())) { - throw new ConflictException("이미 존재하는 이메일입니다."); - } - - // 닉네임 중복 검사 - if (memberRepository.existsByNickname(request.nickname())) { - throw new ConflictException("이미 존재하는 닉네임입니다."); - } - - // 이메일 인증 여부 확인 - - Member member = Member.builder() - .email(request.email()) - .password(passwordEncoder.encode(request.password())) - .nickname(request.nickname()) - .intro(request.intro()) - .role(MemberRole.USER) - .build(); - - Member savedMember = memberRepository.save(member); - return savedMember.getId(); - } -} diff --git a/src/test/java/com/mycom/socket/member/controller/RegisterControllerTest.java b/src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java similarity index 83% rename from src/test/java/com/mycom/socket/member/controller/RegisterControllerTest.java rename to src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java index 65cd810..e295323 100644 --- a/src/test/java/com/mycom/socket/member/controller/RegisterControllerTest.java +++ b/src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.mycom.socket.auth.config.SecurityConfig; -import com.mycom.socket.go_socket.controller.RegisterController; -import com.mycom.socket.go_socket.dto.request.MemberRegisterDto; -import com.mycom.socket.go_socket.service.RegisterService; +import com.mycom.socket.auth.controller.AuthController; +import com.mycom.socket.auth.dto.request.RegisterRequestDto; +import com.mycom.socket.auth.service.AuthService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -20,9 +20,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(RegisterController.class) +@WebMvcTest(AuthController.class) @Import(SecurityConfig.class) -class RegisterControllerTest { +class AuthControllerTest { @Autowired private MockMvc mockMvc; @@ -31,19 +31,19 @@ class RegisterControllerTest { private ObjectMapper objectMapper; @MockBean - private RegisterService registerService; + private AuthService authService; @Test @WithMockUser void 회원가입_성공() throws Exception { // given - MemberRegisterDto request = new MemberRegisterDto( + RegisterRequestDto request = new RegisterRequestDto( "test@example.com", "testUser", "password123", "안녕하세요" ); - given(registerService.register(any(MemberRegisterDto.class))) + given(authService.register(any(RegisterRequestDto.class))) .willReturn(1L); // when & then @@ -60,7 +60,7 @@ class RegisterControllerTest { @WithMockUser void 회원가입_실패_잘못된_입력값() throws Exception { // given - MemberRegisterDto request = new MemberRegisterDto( + RegisterRequestDto request = new RegisterRequestDto( "invalid-email", "t", "123", diff --git a/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java b/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java new file mode 100644 index 0000000..1fcebb9 --- /dev/null +++ b/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java @@ -0,0 +1,67 @@ +package com.mycom.socket.member.service; + +import com.mycom.socket.auth.dto.request.LoginRequestDto; +import com.mycom.socket.auth.dto.response.LoginResponseDto; +import com.mycom.socket.auth.service.AuthService; +import com.mycom.socket.go_socket.entity.Member; +import com.mycom.socket.go_socket.entity.enums.MemberRole; +import com.mycom.socket.go_socket.repository.MemberRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +class LoginIntegrationTest { + + @Autowired + private AuthService authService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setUp() { + Member testMember = Member.builder() + .email("test@test.com") + .password(passwordEncoder.encode("password")) + .nickname("tester") + .role(MemberRole.USER) + .build(); + + memberRepository.save(testMember); + } + + @Test + void 로그인통합테스트() { + // given + LoginRequestDto request = new LoginRequestDto("test@test.com", "password"); + HttpServletResponse response = new MockHttpServletResponse(); + + // when + LoginResponseDto loginResponse = authService.login(request, response); + Cookie cookie = ((MockHttpServletResponse) response).getCookie("Authorization"); + + // then + assertAll( + () -> assertEquals("test@test.com", loginResponse.email()), + () -> assertEquals("tester", loginResponse.nickname()), + () -> assertNotNull(cookie), + () -> assertTrue(cookie.isHttpOnly()), + () -> assertTrue(cookie.getSecure()), + () -> assertEquals("/", cookie.getPath()), + () -> assertEquals(1800, cookie.getMaxAge()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/mycom/socket/member/service/LoginTest.java b/src/test/java/com/mycom/socket/member/service/LoginTest.java new file mode 100644 index 0000000..aabbbeb --- /dev/null +++ b/src/test/java/com/mycom/socket/member/service/LoginTest.java @@ -0,0 +1,129 @@ +package com.mycom.socket.member.service; + +import com.mycom.socket.auth.dto.request.LoginRequestDto; +import com.mycom.socket.auth.dto.response.LoginResponseDto; +import com.mycom.socket.auth.jwt.JWTUtil; +import com.mycom.socket.auth.service.AuthService; +import com.mycom.socket.global.exception.BadRequestException; +import com.mycom.socket.go_socket.entity.Member; +import com.mycom.socket.go_socket.entity.enums.MemberRole; +import com.mycom.socket.go_socket.repository.MemberRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LoginTest { + + @InjectMocks + private AuthService authService; + + @Mock + private MemberRepository memberRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private JWTUtil jwtUtil; + + @Mock + private HttpServletResponse response; + + @Test + void 로그인성공() { + // given + String email = "test@test.com"; + String password = "password"; + String nickname = "tester"; + String encodedPassword = "encodedPassword"; + String token = "test.token.here"; + + LoginRequestDto request = new LoginRequestDto(email, password); + Member member = Member.builder() + .email(email) + .password(encodedPassword) + .nickname(nickname) + .role(MemberRole.USER) + .build(); + + // when + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + when(passwordEncoder.matches(password, encodedPassword)).thenReturn(true); + when(jwtUtil.createToken(email)).thenReturn(token); + + LoginResponseDto response = authService.login(request, this.response); + + // then + ArgumentCaptor cookieCaptor = ArgumentCaptor.forClass(Cookie.class); + verify(this.response).addCookie(cookieCaptor.capture()); + Cookie cookie = cookieCaptor.getValue(); + + assertAll( + () -> assertEquals(email, response.email()), + () -> assertEquals(nickname, response.nickname()), + () -> assertEquals("Authorization", cookie.getName()), + () -> assertEquals(token, cookie.getValue()), + () -> assertTrue(cookie.isHttpOnly()), + () -> assertTrue(cookie.getSecure()), + () -> assertEquals("/", cookie.getPath()), + () -> assertEquals(1800, cookie.getMaxAge()) + ); + + verify(memberRepository).findByEmail(email); + verify(passwordEncoder).matches(password, encodedPassword); + verify(jwtUtil).createToken(email); + } + + @Test + void 로그인실패_이메일없음() { + // given + String email = "nonexistent@test.com"; + String password = "password"; + LoginRequestDto request = new LoginRequestDto(email, password); + + // when + when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); + + // then + assertThrows(BadRequestException.class, () -> authService.login(request, response)); + verify(memberRepository).findByEmail(email); + verifyNoInteractions(response); // 쿠키가 설정되지 않았는지 확인 + } + + @Test + void 로그인실패_비밀번호틀림() { + // given + String email = "test@test.com"; + String password = "wrongpassword"; + String encodedPassword = "encodedPassword"; + LoginRequestDto request = new LoginRequestDto(email, password); + + Member member = Member.builder() + .email(email) + .password(encodedPassword) + .role(MemberRole.USER) + .build(); + + // when + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + when(passwordEncoder.matches(password, encodedPassword)).thenReturn(false); + + // then + assertThrows(BadRequestException.class, () -> authService.login(request, response)); + verify(memberRepository).findByEmail(email); + verify(passwordEncoder).matches(password, encodedPassword); + verifyNoInteractions(response); // 쿠키가 설정되지 않았는지 확인 + } +} \ No newline at end of file diff --git a/src/test/java/com/mycom/socket/member/service/RegisterServiceTest.java b/src/test/java/com/mycom/socket/member/service/RegisterServiceTest.java index 4936e30..2131fd5 100644 --- a/src/test/java/com/mycom/socket/member/service/RegisterServiceTest.java +++ b/src/test/java/com/mycom/socket/member/service/RegisterServiceTest.java @@ -1,10 +1,9 @@ package com.mycom.socket.member.service; -import com.mycom.socket.global.exception.ConflictException; -import com.mycom.socket.go_socket.dto.request.MemberRegisterDto; +import com.mycom.socket.auth.dto.request.RegisterRequestDto; +import com.mycom.socket.auth.service.AuthService; import com.mycom.socket.go_socket.entity.Member; import com.mycom.socket.go_socket.repository.MemberRepository; -import com.mycom.socket.go_socket.service.RegisterService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -23,7 +22,7 @@ class RegisterServiceTest { @InjectMocks - private RegisterService registerService; + private AuthService authService; @Mock private MemberRepository memberRepository; @@ -34,7 +33,7 @@ class RegisterServiceTest { @Test void 회원가입_성공() { // given - MemberRegisterDto request = new MemberRegisterDto( + RegisterRequestDto request = new RegisterRequestDto( "test@example.com", "testUser", "password123", @@ -58,7 +57,7 @@ class RegisterServiceTest { given(memberRepository.save(any(Member.class))).willReturn(savedMember); // when - Long memberId = registerService.register(request); + Long memberId = authService.register(request); // then assertThat(memberId).isEqualTo(1L);