diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/controller/MemberController.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/controller/MemberController.java index f185511..167c22f 100644 --- a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/controller/MemberController.java +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/controller/MemberController.java @@ -1,15 +1,18 @@ package com.example.EnjoyTripBackend.controller; +import com.example.EnjoyTripBackend.dto.member.LoginRequestDto; import com.example.EnjoyTripBackend.dto.member.SignUpRequestDto; import com.example.EnjoyTripBackend.service.MemberService; +import com.example.EnjoyTripBackend.util.SessionConst; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; -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; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.CREATED; @RestController @RequestMapping("/api") @@ -19,8 +22,25 @@ public class MemberController { private final MemberService memberService; @PostMapping("/signUp") - public ResponseEntity signUp(@Valid @RequestBody SignUpRequestDto signUpRequestDto, BindingResult bindingResult){ + public ResponseEntity signUp(@Valid @RequestBody SignUpRequestDto signUpRequestDto, BindingResult bindingResult){ memberService.signUp(signUpRequestDto); - return ResponseEntity.ok().body("회원가입 완료"); + return ResponseEntity.status(CREATED).body("회원가입 완료"); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequestDto loginRequestDto, HttpServletRequest request, BindingResult bindingResult){ + memberService.login(loginRequestDto); + HttpSession session = request.getSession(); + session.setAttribute(SessionConst.LOGIN_MEMBER, loginRequestDto.getEmail()); + return ResponseEntity.ok().body("로그인 완료"); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + return ResponseEntity.ok().body("로그아웃 완료"); } } \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/dto/member/LoginRequestDto.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/dto/member/LoginRequestDto.java new file mode 100644 index 0000000..98769db --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/dto/member/LoginRequestDto.java @@ -0,0 +1,11 @@ +package com.example.EnjoyTripBackend.dto.member; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class LoginRequestDto { + private String email; + private String password; +} \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/ErrorCode.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/ErrorCode.java index aabe02b..f74ef90 100644 --- a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/ErrorCode.java +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/ErrorCode.java @@ -9,7 +9,11 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다."), - DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일 계정입니다."); + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일 계정입니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), + LOGIN_FAIL(HttpStatus.UNAUTHORIZED,"아이디 또는 비밀번호가 잘못 되었습니다."), + ANONYMOUS_USER(HttpStatus.UNAUTHORIZED,"인증되지 않는 사용자입니다. 로그인을 진행해 주세요"), + REQUEST_METHOD_NOT_ALLOW(HttpStatus.METHOD_NOT_ALLOWED,"올바르지 않는 http method 입니다."); private final HttpStatus httpstatus; private final String message; diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/GlobalExceptionHandler.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/GlobalExceptionHandler.java index ef7952f..df460ff 100644 --- a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/GlobalExceptionHandler.java +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/exception/GlobalExceptionHandler.java @@ -4,9 +4,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -17,6 +20,12 @@ public ResponseEntity EnjoyTripExceptionHandler(EnjoyTripException e) { return error(e); } + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity HttpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) { + log.error(e.getMessage()); + return ResponseEntity.status(METHOD_NOT_ALLOWED.value()).body(ErrorCode.REQUEST_METHOD_NOT_ALLOW.getMessage()); + } + @ExceptionHandler(RuntimeException.class) public ResponseEntity EnjoyTripExceptionHandler(RuntimeException e) { log.error(e.getMessage()); diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/CorsFilter.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/CorsFilter.java new file mode 100644 index 0000000..563ab17 --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/CorsFilter.java @@ -0,0 +1,26 @@ +package com.example.EnjoyTripBackend.filter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +@Slf4j +public class CorsFilter implements Filter { + + public void init(FilterConfig filterConfig) throws ServletException {} + + public void destroy() {} + + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + httpResponse.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); + httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); + httpResponse.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS, GET, DELETE, PUT"); + httpResponse.setHeader("Access-Control-Max-Age", "3600"); + httpResponse.setHeader("Access-Control-Allow-Headers", "Authorization, x-requested-with, origin, content-type, accept"); + chain.doFilter(servletRequest, servletResponse); + } +} \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/LoginCheckFilter.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/LoginCheckFilter.java new file mode 100644 index 0000000..b1aef0a --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/LoginCheckFilter.java @@ -0,0 +1,64 @@ +package com.example.EnjoyTripBackend.filter; + +import com.example.EnjoyTripBackend.exception.EnjoyTripException; +import com.example.EnjoyTripBackend.exception.ErrorCode; +import com.example.EnjoyTripBackend.util.SessionConst; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.PatternMatchUtils; + +import java.io.IOException; + +import static org.springframework.http.MediaType.*; + +@Slf4j +@RequiredArgsConstructor +public class LoginCheckFilter implements Filter { + + private final ObjectMapper objectMapper; + + private static final String[] whitelist = {"/api/signup", "/api/login", "/api/logout"}; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + String requestURI = httpRequest.getRequestURI(); + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + try { + log.info("인증 체크 필터 시작 {}", requestURI); + if (isLoginCheckPath(requestURI)) { + log.info("인증 체크 로직 실행 {}", requestURI); + + HttpSession session = httpRequest.getSession(false); + if ((session == null) || (session.getAttribute(SessionConst.LOGIN_MEMBER) == null)) { + log.info("미인증 사용자 요청 {}", requestURI); + AnonymousUserHandler(httpResponse, ErrorCode.ANONYMOUS_USER.getMessage()); + } + } + filterChain.doFilter(servletRequest, servletResponse); + } catch (Exception e) { + throw new EnjoyTripException(ErrorCode.ANONYMOUS_USER, e.getMessage()); + } finally { + log.info("인증 체크 필터 종료 {}", requestURI); + } + } + + private void AnonymousUserHandler(HttpServletResponse response, String message) throws IOException{ + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setCharacterEncoding("UTF-8"); + response.setContentType(APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getWriter(), new ResponseEntity(message, null, HttpStatus.FORBIDDEN.value())); + } + + private boolean isLoginCheckPath(String requestURI) { + return !PatternMatchUtils.simpleMatch(whitelist, requestURI); + } +} \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/WebConfig.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/WebConfig.java new file mode 100644 index 0000000..cc1f982 --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/filter/WebConfig.java @@ -0,0 +1,41 @@ +package com.example.EnjoyTripBackend.filter; + +import com.example.EnjoyTripBackend.util.SessionUserArgumentResolver; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.Filter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Slf4j +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new SessionUserArgumentResolver()); + } + + @Bean + public FilterRegistrationBean LoginCheckFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new LoginCheckFilter(new ObjectMapper())); + filterRegistrationBean.setOrder(1); + filterRegistrationBean.addUrlPatterns("/*"); + return filterRegistrationBean; + } + + @Bean + public FilterRegistrationBean CorsFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new CorsFilter()); + filterRegistrationBean.setOrder(0); + filterRegistrationBean.addUrlPatterns("/*"); + return filterRegistrationBean; + } +} \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/service/MemberService.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/service/MemberService.java index 7ab6dd7..5300bf2 100644 --- a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/service/MemberService.java +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/service/MemberService.java @@ -2,6 +2,7 @@ import com.example.EnjoyTripBackend.domain.Member; import com.example.EnjoyTripBackend.domain.MemberRole; +import com.example.EnjoyTripBackend.dto.member.LoginRequestDto; import com.example.EnjoyTripBackend.dto.member.SignUpRequestDto; import com.example.EnjoyTripBackend.exception.EnjoyTripException; import com.example.EnjoyTripBackend.exception.ErrorCode; @@ -45,4 +46,14 @@ public Long signUp(SignUpRequestDto signUpRequestDto) { return memberRepository.save(member); } + + public Long login(LoginRequestDto loginRequestDto) { + + Member member = memberRepository.findByEmail(loginRequestDto.getEmail()).orElseThrow(() -> new EnjoyTripException(ErrorCode.MEMBER_NOT_FOUND)); + + if (!member.getPassword().equals(encrypt.encode(loginRequestDto.getPassword(), member.getSalt()))){ + throw new EnjoyTripException(ErrorCode.LOGIN_FAIL); + } + return member.getId(); + } } diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionConst.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionConst.java new file mode 100644 index 0000000..a1ca2d9 --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionConst.java @@ -0,0 +1,5 @@ +package com.example.EnjoyTripBackend.util; + +public class SessionConst { + public static final String LOGIN_MEMBER = "loginMember"; +} \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionUser.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionUser.java new file mode 100644 index 0000000..5f67c13 --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionUser.java @@ -0,0 +1,11 @@ +package com.example.EnjoyTripBackend.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface SessionUser { +} \ No newline at end of file diff --git a/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionUserArgumentResolver.java b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionUserArgumentResolver.java new file mode 100644 index 0000000..64953ab --- /dev/null +++ b/EnjoyTripBackend/src/main/java/com/example/EnjoyTripBackend/util/SessionUserArgumentResolver.java @@ -0,0 +1,28 @@ +package com.example.EnjoyTripBackend.util; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static com.example.EnjoyTripBackend.util.SessionConst.LOGIN_MEMBER; + +@Slf4j +public class SessionUserArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasSessionUserAnnotation = parameter.hasParameterAnnotation(SessionUser.class); + boolean hasStringType = parameter.getParameterType().equals(String.class); + return hasSessionUserAnnotation && hasStringType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest httpRequest = (HttpServletRequest) webRequest.getNativeRequest(); + return (String) httpRequest.getSession().getAttribute(LOGIN_MEMBER); + } +} \ No newline at end of file