diff --git a/src/main/java/com/example/redunm/config/PasswordConfig.java b/src/main/java/com/example/redunm/config/PasswordConfig.java new file mode 100644 index 0000000..17e4cd5 --- /dev/null +++ b/src/main/java/com/example/redunm/config/PasswordConfig.java @@ -0,0 +1,15 @@ +package com.example.redunm.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/example/redunm/config/SecurityConfig.java b/src/main/java/com/example/redunm/config/SecurityConfig.java index 7af63db..7d253eb 100644 --- a/src/main/java/com/example/redunm/config/SecurityConfig.java +++ b/src/main/java/com/example/redunm/config/SecurityConfig.java @@ -1,58 +1,69 @@ package com.example.redunm.config; +import com.example.redunm.filter.JsonAuthenticationFilter; +import com.example.redunm.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { + private final UserService userService; + + @Autowired + public SecurityConfig(UserService userService) { + this.userService = userService; + } + @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화 - .cors(Customizer.withDefaults()) // 기본 CORS 설정 + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authManager) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .cors(Customizer.withDefaults()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + ) .authorizeHttpRequests(auth -> auth .requestMatchers( - "/api/auth/signup/**", // 회원가입 관련 요청 허용 - "/api/auth/login/**", // 로그인 관련 요청 허용 - "/api/cart/**", // 장바구니 API 요청 허용 - "/api/data-models/**", // 데이터 모델 API 요청 허용 - "/css/**", // 정적 리소스 허용 + "/api/auth/signup/**", + "/api/auth/login/**", + "/api/cart/**", + "/api/data-models/**", + "/css/**", "/js/**", "/models/**" ).permitAll() - .anyRequest().authenticated() // 그 외의 요청은 인증 필요 + .anyRequest().authenticated() ) - - .formLogin(form -> form - .loginPage("/login") // 커스텀 로그인 페이지 - .loginProcessingUrl("/api/auth/login") // 로그인 요청 URL - .defaultSuccessUrl("/home", true) // 로그인 성공 후 이동 URL + .logout(logout -> logout + .logoutUrl("/api/auth/logout") + .logoutSuccessUrl("/") .permitAll() ) + .formLogin(form -> form.disable()); - .logout(logout -> logout - .logoutUrl("/api/auth/logout") // 로그아웃 URL - .logoutSuccessUrl("/") // 로그아웃 성공 후 이동 URL - .permitAll() - ); + JsonAuthenticationFilter jsonAuthenticationFilter = new JsonAuthenticationFilter("/api/auth/login", authManager, userService); + http.addFilterBefore(jsonAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } } diff --git a/src/main/java/com/example/redunm/config/WebConfig.java b/src/main/java/com/example/redunm/config/WebConfig.java index b70ccbd..415be0e 100644 --- a/src/main/java/com/example/redunm/config/WebConfig.java +++ b/src/main/java/com/example/redunm/config/WebConfig.java @@ -1,24 +1,17 @@ package com.example.redunm.config; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.*; @Configuration -public class WebConfig { +public class WebConfig implements WebMvcConfigurer { - @Bean - public WebMvcConfigurer corsConfigurer() { - return new WebMvcConfigurer() { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins("http://192.168.0.20:3000", "http://localhost:3000") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } - }; + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); } } diff --git a/src/main/java/com/example/redunm/dto/SignUpRequest.java b/src/main/java/com/example/redunm/dto/SignUpRequest.java index 74897a6..2965014 100644 --- a/src/main/java/com/example/redunm/dto/SignUpRequest.java +++ b/src/main/java/com/example/redunm/dto/SignUpRequest.java @@ -2,29 +2,25 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public class SignUpRequest { - @NotBlank(message = "아이디는 필수입니다.") + @NotBlank(message = "Username is mandatory") private String username; - @NotBlank(message = "비밀번호는 필수입니다.") - @Size(min = 6, message = "비밀번호는 최소 6자 이상이어야 합니다.") - @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{6,}$", - message = "비밀번호는 대문자, 소문자, 숫자, 특수문자를 포함해야 합니다.") + @NotBlank(message = "Password is mandatory") + @Size(min = 8, message = "Password must be at least 8 characters") private String password; - @NotBlank(message = "비밀번호 확인은 필수입니다.") + @NotBlank(message = "Confirm Password is mandatory") private String confirmPassword; - @NotBlank(message = "이메일은 필수입니다.") - @Email(message = "유효한 이메일 형식을 입력해주세요.") + @NotBlank(message = "Email is mandatory") + @Email(message = "Email should be valid") private String email; - @NotBlank(message = "전화번호는 필수입니다.") - @Pattern(regexp = "^\\d{10,15}$", message = "유효한 전화번호를 입력해주세요.") + @NotBlank(message = "Phone number is mandatory") private String phone; // Getters and Setters diff --git a/src/main/java/com/example/redunm/entity/User.java b/src/main/java/com/example/redunm/entity/User.java index f9c80ee..b0d7172 100644 --- a/src/main/java/com/example/redunm/entity/User.java +++ b/src/main/java/com/example/redunm/entity/User.java @@ -2,9 +2,13 @@ import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; @Document(collection = "users") -public class User { +public class User implements UserDetails { @Id private String id; @@ -23,19 +27,20 @@ public void setId(String id) { this.id = id; } + @Override public String getUsername() { - return username; + return email; } public void setUsername(String username) { this.username = username; } + @Override public String getPassword() { return password; } - // 비밀번호는 암호화된 상태로 설정됨 public void setPassword(String password) { this.password = password; } @@ -55,4 +60,29 @@ public String getPhone() { public void setPhone(String phone) { this.phone = phone; } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } } diff --git a/src/main/java/com/example/redunm/exception/GlobalExceptionHandler.java b/src/main/java/com/example/redunm/exception/GlobalExceptionHandler.java index 518aa1c..84e2166 100644 --- a/src/main/java/com/example/redunm/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/redunm/exception/GlobalExceptionHandler.java @@ -2,17 +2,34 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; -import org.springframework.web.HttpRequestMethodNotSupportedException; -@ControllerAdvice +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) { - return ResponseEntity - .status(HttpStatus.METHOD_NOT_ALLOWED) - .body("지원되지 않는 HTTP 메서드입니다. " + ex.getMethod() + "이 엔드포인트에서 지원되지 않습니다."); + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions( + MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(Exception.class) + public ResponseEntity> handleAllExceptions(Exception ex) { + Map error = new HashMap<>(); + error.put("message", ex.getMessage()); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } } diff --git a/src/main/java/com/example/redunm/filter/JsonAuthenticationFilter.java b/src/main/java/com/example/redunm/filter/JsonAuthenticationFilter.java new file mode 100644 index 0000000..7c82e5b --- /dev/null +++ b/src/main/java/com/example/redunm/filter/JsonAuthenticationFilter.java @@ -0,0 +1,63 @@ +package com.example.redunm.filter; + +import com.example.redunm.login.LoginRequest; +import com.example.redunm.service.UserService; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.io.IOException; + +public class JsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final UserService userService; + + public JsonAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager, UserService userService) { + super(defaultFilterProcessesUrl); + setAuthenticationManager(authenticationManager); + this.userService = userService; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + + if (!request.getMethod().equals("POST")) { + throw new ServletException("Authentication method not supported: " + request.getMethod()); + } + + LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class); + + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( + loginRequest.getEmail(), + loginRequest.getPassword() + ); + + return getAuthenticationManager().authenticate(authRequest); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + SecurityContextHolder.getContext().setAuthentication(authResult); + response.setContentType("application/json"); + response.getWriter().write("{\"message\": \"로그인 성공\"}"); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"error\": \"이메일 또는 비밀번호가 잘못되었습니다.\"}"); + } +} diff --git a/src/main/java/com/example/redunm/login/LoginController.java b/src/main/java/com/example/redunm/login/LoginController.java deleted file mode 100644 index c305875..0000000 --- a/src/main/java/com/example/redunm/login/LoginController.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.redunm.login; - -import com.example.redunm.entity.User; -import com.example.redunm.service.UserService; -import com.example.redunm.login.LoginRequest; -import jakarta.servlet.http.HttpSession; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -@RestController -@RequestMapping("/api/auth/login") -public class LoginController { - - @Autowired - private UserService userService; - - private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); - - // POST 요청 - @PostMapping - public ResponseEntity login(@RequestBody LoginRequest loginRequest, - HttpSession session) { - String email = loginRequest.getEmail(); - String password = loginRequest.getPassword(); - - if (email == null || password == null) { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body("이메일과 비밀번호를 모두 입력해주세요."); - } - - var optionalUser = userService.findByEmail(email); - if (optionalUser.isEmpty()) { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body("존재하지 않는 이메일입니다."); - } - - User user = optionalUser.get(); - - if (!passwordEncoder.matches(password, user.getPassword())) { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body("비밀번호가 일치하지 않습니다."); - } - - session.setAttribute("loggedInUser", user); - - return ResponseEntity.ok("로그인 성공"); - } - - // GET 요청에 대한 처리 추가 - @GetMapping - public ResponseEntity handleGetLogin() { - return ResponseEntity - .status(HttpStatus.METHOD_NOT_ALLOWED) - .body("GET 메서드는 /api/auth/login 엔드포인트에서 지원되지 않습니다. POST 메서드를 사용하세요."); - } - - // 로그아웃 처리 (POST 방식) - @PostMapping("/logout") - public ResponseEntity logout(HttpSession session) { - session.invalidate(); - return ResponseEntity.ok("로그아웃 성공"); - } -} diff --git a/src/main/java/com/example/redunm/login/LoginRequest.java b/src/main/java/com/example/redunm/login/LoginRequest.java index 5c2f933..a7fa47e 100644 --- a/src/main/java/com/example/redunm/login/LoginRequest.java +++ b/src/main/java/com/example/redunm/login/LoginRequest.java @@ -1,9 +1,16 @@ package com.example.redunm.login; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + public class LoginRequest { + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "유효한 이메일 형식을 입력해주세요.") private String email; - private String password; + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; public String getEmail() { return email; diff --git a/src/main/java/com/example/redunm/login/LoginResponse.java b/src/main/java/com/example/redunm/login/LoginResponse.java new file mode 100644 index 0000000..4d62723 --- /dev/null +++ b/src/main/java/com/example/redunm/login/LoginResponse.java @@ -0,0 +1,18 @@ +package com.example.redunm.login; + +public class LoginResponse { + private String message; + + public LoginResponse(String message) { + this.message = message; + } + + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/example/redunm/repository/UserRepository.java b/src/main/java/com/example/redunm/repository/UserRepository.java index 55c8d5c..f46942c 100644 --- a/src/main/java/com/example/redunm/repository/UserRepository.java +++ b/src/main/java/com/example/redunm/repository/UserRepository.java @@ -2,11 +2,8 @@ import com.example.redunm.entity.User; import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; - import java.util.Optional; -@Repository public interface UserRepository extends MongoRepository { Optional findByUsername(String username); Optional findByEmail(String email); diff --git a/src/main/java/com/example/redunm/service/UserService.java b/src/main/java/com/example/redunm/service/UserService.java index 8399f84..33bfc7c 100644 --- a/src/main/java/com/example/redunm/service/UserService.java +++ b/src/main/java/com/example/redunm/service/UserService.java @@ -3,13 +3,14 @@ import com.example.redunm.entity.User; import com.example.redunm.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.*; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.Optional; @Service -public class UserService { +public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @@ -38,15 +39,14 @@ public User save(User user) { } public boolean isDuplicate(User user) { - if (findByUsername(user.getUsername()).isPresent()) { - return true; - } - if (findByEmail(user.getEmail()).isPresent()) { - return true; - } - if (findByPhone(user.getPhone()).isPresent()) { - return true; - } - return false; + return findByUsername(user.getUsername()).isPresent() + || findByEmail(user.getEmail()).isPresent() + || findByPhone(user.getPhone()).isPresent(); + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); } } diff --git a/src/main/java/com/example/redunm/signup/SignUpController.java b/src/main/java/com/example/redunm/signup/AuthController.java similarity index 77% rename from src/main/java/com/example/redunm/signup/SignUpController.java rename to src/main/java/com/example/redunm/signup/AuthController.java index 2c1d54e..fdc415a 100644 --- a/src/main/java/com/example/redunm/signup/SignUpController.java +++ b/src/main/java/com/example/redunm/signup/AuthController.java @@ -13,17 +13,17 @@ import java.util.Map; @RestController -@RequestMapping("/api/auth/signup") -public class SignUpController { +@RequestMapping("/api/auth") +public class AuthController { private final UserService userService; @Autowired - public SignUpController(UserService userService) { + public AuthController(UserService userService) { this.userService = userService; } - @PostMapping + @PostMapping("/signup") public ResponseEntity signup(@Valid @RequestBody SignUpRequest signUpRequest) { if (!signUpRequest.getPassword().equals(signUpRequest.getConfirmPassword())) { return ResponseEntity @@ -51,7 +51,7 @@ public ResponseEntity signup(@Valid @RequestBody SignUpRequest signUpRequest) User user = new User(); user.setUsername(signUpRequest.getUsername()); - user.setPassword(signUpRequest.getPassword()); + user.setPassword(signUpRequest.getPassword()); // PasswordEncoder는 UserService.save()에서 처리됨 user.setEmail(signUpRequest.getEmail()); user.setPhone(signUpRequest.getPhone()); @@ -63,10 +63,12 @@ public ResponseEntity signup(@Valid @RequestBody SignUpRequest signUpRequest) return ResponseEntity.ok(response); } - @GetMapping("/success") - public ResponseEntity success() { + // 로그아웃 엔드포인트 (Spring Security에서 처리) + @PostMapping("/logout") + public ResponseEntity logout() { + // 로그아웃은 SecurityConfig에서 처리되므로, 별도의 로직이 필요 없습니다. Map response = new HashMap<>(); - response.put("message", "회원가입이 성공적으로 완료되었습니다."); + response.put("message", "로그아웃이 성공적으로 완료되었습니다."); return ResponseEntity.ok(response); } }