From a5fcdd36def8430332f2d7753b2b2903efa0d590 Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Tue, 19 Nov 2024 20:24:47 +0900 Subject: [PATCH 01/10] =?UTF-8?q?#1=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=8B=9C=ED=81=90?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=EC=84=A4=EC=A0=95=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- board/build.gradle | 8 +- .../com/main/board/config/SecurityConfig.java | 38 ++++++++ .../main/board/exception/CustomException.java | 4 + .../main/board/exception/GlobalException.java | 4 + .../board/security/CustomUserDetails.java | 55 ++++++++++++ .../security/PrincipalDetailsService.java | 24 ++++++ .../java/com/main/board/user/DTO/UserDTO.java | 86 +++++++++++++++++++ .../main/java/com/main/board/user/User.java | 32 +++++++ .../board/user/controller/UserController.java | 30 +++++++ .../com/main/board/user/mapper/UserMapper.xml | 20 +++++ .../board/user/repository/UserRepository.java | 13 +++ .../main/board/user/service/UserService.java | 48 +++++++++++ .../main/java/com/main/board/util/Bcrypt.java | 18 ++++ .../board/user/service/UserServiceTest.java | 14 +++ 14 files changed, 390 insertions(+), 4 deletions(-) create mode 100644 board/src/main/java/com/main/board/config/SecurityConfig.java create mode 100644 board/src/main/java/com/main/board/exception/CustomException.java create mode 100644 board/src/main/java/com/main/board/exception/GlobalException.java create mode 100644 board/src/main/java/com/main/board/security/CustomUserDetails.java create mode 100644 board/src/main/java/com/main/board/security/PrincipalDetailsService.java create mode 100644 board/src/main/java/com/main/board/user/DTO/UserDTO.java create mode 100644 board/src/main/java/com/main/board/user/User.java create mode 100644 board/src/main/java/com/main/board/user/controller/UserController.java create mode 100644 board/src/main/java/com/main/board/user/mapper/UserMapper.xml create mode 100644 board/src/main/java/com/main/board/user/repository/UserRepository.java create mode 100644 board/src/main/java/com/main/board/user/service/UserService.java create mode 100644 board/src/main/java/com/main/board/util/Bcrypt.java create mode 100644 board/src/test/java/com/main/board/user/service/UserServiceTest.java diff --git a/board/build.gradle b/board/build.gradle index 1804e72..89cdfba 100644 --- a/board/build.gradle +++ b/board/build.gradle @@ -25,18 +25,18 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3' - //testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'mysql:mysql-connector-java:8.0.32' - //runtimeOnly 'com.mysql:mysql-connector-j' 이거 랑 위에꺼랑 뭔차이? -} + implementation 'org.springframework.boot:spring-boot-starter-validation' + } tasks.named('test') { useJUnitPlatform() diff --git a/board/src/main/java/com/main/board/config/SecurityConfig.java b/board/src/main/java/com/main/board/config/SecurityConfig.java new file mode 100644 index 0000000..2f79a40 --- /dev/null +++ b/board/src/main/java/com/main/board/config/SecurityConfig.java @@ -0,0 +1,38 @@ +package com.main.board.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf((auth) -> auth.disable()) + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginProcessingUrl("/users/login") + .usernameParameter("user_id") + .passwordParameter("password") + .successHandler(customAuthenticationSuccessHandler) + .failureHandler(customAuthenticationFailureHandler) + .permitAll() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/board/src/main/java/com/main/board/exception/CustomException.java b/board/src/main/java/com/main/board/exception/CustomException.java new file mode 100644 index 0000000..d2d8016 --- /dev/null +++ b/board/src/main/java/com/main/board/exception/CustomException.java @@ -0,0 +1,4 @@ +package com.main.board.exception; + +public class CustomException { +} diff --git a/board/src/main/java/com/main/board/exception/GlobalException.java b/board/src/main/java/com/main/board/exception/GlobalException.java new file mode 100644 index 0000000..3ed3c3f --- /dev/null +++ b/board/src/main/java/com/main/board/exception/GlobalException.java @@ -0,0 +1,4 @@ +package com.main.board.exception; + +public class GlobalException { +} diff --git a/board/src/main/java/com/main/board/security/CustomUserDetails.java b/board/src/main/java/com/main/board/security/CustomUserDetails.java new file mode 100644 index 0000000..6d917ef --- /dev/null +++ b/board/src/main/java/com/main/board/security/CustomUserDetails.java @@ -0,0 +1,55 @@ +package com.main.board.security; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + + private final User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + ArrayList auth = new ArrayList(); + auth.add(new SimpleGrantedAuthority(AUTHORITY)); + return auth; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @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/board/src/main/java/com/main/board/security/PrincipalDetailsService.java b/board/src/main/java/com/main/board/security/PrincipalDetailsService.java new file mode 100644 index 0000000..6f71e9e --- /dev/null +++ b/board/src/main/java/com/main/board/security/PrincipalDetailsService.java @@ -0,0 +1,24 @@ +package com.main.board.security; + +import com.main.board.user.repository.UserRepository; +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 PrincipalDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String user_id) throws UsernameNotFoundException { + boolean user = userRepository.findUserById(user_id); + if(!user) { + throw new UsernameNotFoundException("해당하는 사용자가 없습니다."); + } + return new PrincipalDetails(user); + } +} diff --git a/board/src/main/java/com/main/board/user/DTO/UserDTO.java b/board/src/main/java/com/main/board/user/DTO/UserDTO.java new file mode 100644 index 0000000..acde585 --- /dev/null +++ b/board/src/main/java/com/main/board/user/DTO/UserDTO.java @@ -0,0 +1,86 @@ +package com.main.board.user.DTO; + +import com.main.board.user.User; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + + +@Getter +@NoArgsConstructor +public class UserDTO { + + @NotBlank + private String user_id; + @NotBlank + private String password; + @NotBlank + private String name; + @NotNull + private LocalDate create_date; + + + + public UserDTO(String user_id, String password, String name, LocalDate create_date) { + this.user_id = validateId(user_id); + this.password = validatePaswd(password); + this.name = name; + this.create_date = create_date; + } + + public String validateId(String user_id) { + if (user_id.length() < 5 || user_id.length() > 20) { + throw new IllegalArgumentException("아이디는 5자 이상 20자 이하로 입력해주세요."); + } + return user_id; + } + + public String validatePaswd(String password) { + if (password.length() < 8 || password.length() > 20) { + throw new IllegalArgumentException("비밀번호는 8자 이상 20자 이하로 입력해주세요."); + } + return password; + } + + public User toUserEntity(String pwd) { + return new User(this.user_id, pwd, this.name, this.create_date); + } + + + @Getter + @NoArgsConstructor + public static class SignUpResponse { + private String user_id; + private String password; + private String name; + private LocalDate create_date; + + public SignUpResponse(User user) { + this.user_id = user.getUser_id(); + this.password = user.getPassword(); + this.name = user.getName(); + this.create_date = user.getCreate_date(); + } + } + + @Getter + @NoArgsConstructor + public static class LoginResponse { + private String user_id; + private String password; + private String name; + private LocalDate create_date; + + public LoginResponse(User user) { + this.user_id = user.getUser_id(); + this.password = user.getPassword(); + this.name = user.getName(); + this.create_date = user.getCreate_date(); + } + } + +} diff --git a/board/src/main/java/com/main/board/user/User.java b/board/src/main/java/com/main/board/user/User.java new file mode 100644 index 0000000..77414d1 --- /dev/null +++ b/board/src/main/java/com/main/board/user/User.java @@ -0,0 +1,32 @@ +package com.main.board.user; + +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@ToString +public class User { + + + private String user_id; + private String password; + private String name; + private LocalDate create_date; + + + public User(String user_id, String password, String name, LocalDate create_date) { + this.user_id = user_id; + this.password = password; + this.name = name; + this.create_date = create_date; + } + + public static User create(String email, String password, String name, LocalDate create_date) { + LocalDateTime now = DateTimeUtils.nowFromZone(); + return new User(email, password, name, birthDate, phone, role, now, now); + } + +} diff --git a/board/src/main/java/com/main/board/user/controller/UserController.java b/board/src/main/java/com/main/board/user/controller/UserController.java new file mode 100644 index 0000000..e84d3be --- /dev/null +++ b/board/src/main/java/com/main/board/user/controller/UserController.java @@ -0,0 +1,30 @@ +package com.main.board.user.controller; + +import com.main.board.user.DTO.UserDTO; +import com.main.board.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController("/users") +public class UserController { + + private final UserService userService; + + + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody UserDTO userDTO) { + try { + return new ResponseEntity<>(userService.signUp(userDTO), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + + +} diff --git a/board/src/main/java/com/main/board/user/mapper/UserMapper.xml b/board/src/main/java/com/main/board/user/mapper/UserMapper.xml new file mode 100644 index 0000000..5fbcc89 --- /dev/null +++ b/board/src/main/java/com/main/board/user/mapper/UserMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + INSERT INTO user (user_id, password, name, create_date) + VALUES (#{user_id}, #{password}, #{name}, #{create_date}); + + + + + \ No newline at end of file diff --git a/board/src/main/java/com/main/board/user/repository/UserRepository.java b/board/src/main/java/com/main/board/user/repository/UserRepository.java new file mode 100644 index 0000000..3f84e9a --- /dev/null +++ b/board/src/main/java/com/main/board/user/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.main.board.user.repository; + +import com.main.board.user.DTO.UserDTO; +import com.main.board.user.User; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Optional; + +@Mapper +public interface UserRepository { + void save(User user); + boolean findUserById(String user_id); +} diff --git a/board/src/main/java/com/main/board/user/service/UserService.java b/board/src/main/java/com/main/board/user/service/UserService.java new file mode 100644 index 0000000..a76cad6 --- /dev/null +++ b/board/src/main/java/com/main/board/user/service/UserService.java @@ -0,0 +1,48 @@ +package com.main.board.user.service; + +import com.main.board.user.DTO.UserDTO; +import com.main.board.user.User; +import com.main.board.user.repository.UserRepository; +import com.main.board.util.Bcrypt; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.SQLException; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + private final Bcrypt bcrypt; + + + @Transactional + public UserDTO.SignUpResponse signUp(UserDTO userDTO) { + String encryptPwd = bcrypt.encrypt(userDTO.getPassword()); + User entity = userDTO.toUserEntity(encryptPwd); + userRepository.save(entity); + return new UserDTO.SignUpResponse(entity); + } + + public UserDTO.LoginResponse login(UserDTO userDTO) { + Boolean userCheck = userRepository.findUserById(userDTO.getUser_id()); + + if(userCheck) { + User user = userRepository.findByEmail(userDTO.getUser_id()); + if(bcrypt.isMatch(userDTO.getPassword(), user.getPassword())) { + return new UserDTO.LoginResponse(user); + } + else { + throw new UserNotFoundException("비밀번호가 일치하지 않습니다."); + } + } + else { + throw new UserNotFoundException("해당하는 사용자가 없습니다."); + } + + return new UserDTO.LoginResponse(user); + } +} diff --git a/board/src/main/java/com/main/board/util/Bcrypt.java b/board/src/main/java/com/main/board/util/Bcrypt.java new file mode 100644 index 0000000..eb26c1a --- /dev/null +++ b/board/src/main/java/com/main/board/util/Bcrypt.java @@ -0,0 +1,18 @@ +package com.main.board.util; + +import org.springframework.security.crypto.bcrypt.BCrypt; +import org.springframework.stereotype.Component; + +@Component +public final class Bcrypt { + + public String encrypt(String password) { + return BCrypt.hashpw(password,BCrypt.gensalt()); + } + + public boolean isMatch(String password, String hashed) { + return BCrypt.checkpw(password,hashed); + } + + +} diff --git a/board/src/test/java/com/main/board/user/service/UserServiceTest.java b/board/src/test/java/com/main/board/user/service/UserServiceTest.java new file mode 100644 index 0000000..7eb38ab --- /dev/null +++ b/board/src/test/java/com/main/board/user/service/UserServiceTest.java @@ -0,0 +1,14 @@ +package com.main.board.user.service; + +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest + +class UserServiceTest { + + + + +} \ No newline at end of file From 2fd005cc3babb73d1b9ab7a098f94e130d17ccd0 Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Tue, 10 Dec 2024 20:28:35 +0900 Subject: [PATCH 02/10] =?UTF-8?q?#1=5F2=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomUserDetails.java | 23 ++--- .../main/board/config/LoginFailHandler.java | 33 +++++++ .../board/config/LoginSuccessHandler.java | 31 +++++++ .../board/config/PrincipalDetailsService.java | 25 ++++++ .../com/main/board/config/SecurityConfig.java | 43 +++++++--- .../com/main/board/member/DTO/MemberDTO.java | 52 +++++++++++ .../main/board/member/DTO/SignUpResponse.java | 19 ++++ .../java/com/main/board/member/Member.java | 29 +++++++ .../controller/MemberController.java} | 17 ++-- .../main/board/member/mapper/MemberMapper.xml | 20 +++++ .../member/repository/MemberRepository.java | 12 +++ .../board/member/service/MemberService.java | 29 +++++++ .../security/PrincipalDetailsService.java | 24 ------ .../java/com/main/board/user/DTO/UserDTO.java | 86 ------------------- .../main/java/com/main/board/user/User.java | 32 ------- .../com/main/board/user/mapper/UserMapper.xml | 20 ----- .../board/user/repository/UserRepository.java | 13 --- .../main/board/user/service/UserService.java | 48 ----------- .../main/java/com/main/board/util/Bcrypt.java | 28 +++++- .../service/UserServiceTest.java | 4 +- 20 files changed, 329 insertions(+), 259 deletions(-) rename board/src/main/java/com/main/board/{security => config}/CustomUserDetails.java (60%) create mode 100644 board/src/main/java/com/main/board/config/LoginFailHandler.java create mode 100644 board/src/main/java/com/main/board/config/LoginSuccessHandler.java create mode 100644 board/src/main/java/com/main/board/config/PrincipalDetailsService.java create mode 100644 board/src/main/java/com/main/board/member/DTO/MemberDTO.java create mode 100644 board/src/main/java/com/main/board/member/DTO/SignUpResponse.java create mode 100644 board/src/main/java/com/main/board/member/Member.java rename board/src/main/java/com/main/board/{user/controller/UserController.java => member/controller/MemberController.java} (52%) create mode 100644 board/src/main/java/com/main/board/member/mapper/MemberMapper.xml create mode 100644 board/src/main/java/com/main/board/member/repository/MemberRepository.java create mode 100644 board/src/main/java/com/main/board/member/service/MemberService.java delete mode 100644 board/src/main/java/com/main/board/security/PrincipalDetailsService.java delete mode 100644 board/src/main/java/com/main/board/user/DTO/UserDTO.java delete mode 100644 board/src/main/java/com/main/board/user/User.java delete mode 100644 board/src/main/java/com/main/board/user/mapper/UserMapper.xml delete mode 100644 board/src/main/java/com/main/board/user/repository/UserRepository.java delete mode 100644 board/src/main/java/com/main/board/user/service/UserService.java rename board/src/test/java/com/main/board/{user => member}/service/UserServiceTest.java (55%) diff --git a/board/src/main/java/com/main/board/security/CustomUserDetails.java b/board/src/main/java/com/main/board/config/CustomUserDetails.java similarity index 60% rename from board/src/main/java/com/main/board/security/CustomUserDetails.java rename to board/src/main/java/com/main/board/config/CustomUserDetails.java index 6d917ef..e3032a1 100644 --- a/board/src/main/java/com/main/board/security/CustomUserDetails.java +++ b/board/src/main/java/com/main/board/config/CustomUserDetails.java @@ -1,7 +1,8 @@ -package com.main.board.security; +package com.main.board.config; +import com.main.board.member.Member; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; @@ -9,27 +10,29 @@ public class CustomUserDetails implements UserDetails { - private final User user; + // security에 user가 아닌 member엔티티를 사용함 + private final Member member; - public CustomUserDetails(User user) { - this.user = user; + public CustomUserDetails(Member member) { + this.member = member; } @Override public Collection getAuthorities() { - ArrayList auth = new ArrayList(); - auth.add(new SimpleGrantedAuthority(AUTHORITY)); - return auth; + ArrayList auth = new ArrayList(); //ArrayList객체생성 + auth.add(new SimpleGrantedAuthority("ROLE_USER"));//ROLE_USER권한을 부여 + return auth; //권한리스트반환 } @Override public String getPassword() { - return user.getPassword(); + return member.getPassword(); } + @Override public String getUsername() { - return user.getUsername(); + return member.getName(); } @Override diff --git a/board/src/main/java/com/main/board/config/LoginFailHandler.java b/board/src/main/java/com/main/board/config/LoginFailHandler.java new file mode 100644 index 0000000..0313b12 --- /dev/null +++ b/board/src/main/java/com/main/board/config/LoginFailHandler.java @@ -0,0 +1,33 @@ +package com.main.board.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/* + 기본적으로 시큐리티에서 로그인 실패시에는 SimpleUrlAuthenticationFailureHandler를 사용한다. + 사용자가 인증에 실패하면 AuthenticationException이 발생 + SimpleUrlAuthenticationFailureHandler는 기본적으로 실패 후 로그인 페이지로 리다이렉트하며, URL에 ?error를 추가 + */ + +public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + //로그인 실패로그 + System.out.println("로그인 실패: " + exception.getMessage()); + // 실패 응답 커스터마이즈 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 401 Unauthorized + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write("{\"error\": \"Login failed\", \"message\": \"" + exception.getMessage() + "\"}"); + } +} diff --git a/board/src/main/java/com/main/board/config/LoginSuccessHandler.java b/board/src/main/java/com/main/board/config/LoginSuccessHandler.java new file mode 100644 index 0000000..656b853 --- /dev/null +++ b/board/src/main/java/com/main/board/config/LoginSuccessHandler.java @@ -0,0 +1,31 @@ +package com.main.board.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + + +public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + // 성공 로그 출력 + System.out.println("로그인 성공: " + authentication.getName()); + + // 성공 응답 커스터마이징 (예: JSON 응답 반환) + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write("{\"message\": \"Login successful\", \"user\": \"" + authentication.getName() + "\"}"); + } + + +} diff --git a/board/src/main/java/com/main/board/config/PrincipalDetailsService.java b/board/src/main/java/com/main/board/config/PrincipalDetailsService.java new file mode 100644 index 0000000..e30f93e --- /dev/null +++ b/board/src/main/java/com/main/board/config/PrincipalDetailsService.java @@ -0,0 +1,25 @@ +package com.main.board.config; + +import com.main.board.member.Member; +import com.main.board.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +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 PrincipalDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + //로그인시 유저를 찾고 유저가 있으면 CustomUserDetails를 반환 + @Override + public CustomUserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { + Member member = memberRepository.findMemberById(userId) + .orElseThrow(() -> new UsernameNotFoundException("해당하는 사용자가 없습니다.")); + return new CustomUserDetails(member); + } +} diff --git a/board/src/main/java/com/main/board/config/SecurityConfig.java b/board/src/main/java/com/main/board/config/SecurityConfig.java index 2f79a40..cfbb480 100644 --- a/board/src/main/java/com/main/board/config/SecurityConfig.java +++ b/board/src/main/java/com/main/board/config/SecurityConfig.java @@ -1,38 +1,55 @@ package com.main.board.config; +import com.main.board.util.Bcrypt; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -@Configuration -@EnableWebSecurity +@Configuration // 스프링부트에게 이 클래스가 설정파일임을 알려줌 (빈등록) +@EnableWebSecurity // Spring Security 활성화 기본보안 필터체인이 적용된다 public class SecurityConfig { + //로그인은 당연히 폼으로 사용할것같다는 가정 하에 설정 (JSON이있을라나?) + // TODO http://localhost:8081/member/signup 회원가입 테스트시 포스트맨에서 시큐리티 기본 폼화면이 리턴됨 @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf((auth) -> auth.disable()) + .csrf((auth) -> auth.disable()) // csrf 비활성화 (REST API등 비상태 통신에서는 CSRF토큰이 필요하지 않을수있다) .authorizeHttpRequests((auth) -> auth - .requestMatchers("/").permitAll() - .anyRequest().authenticated() + .requestMatchers("/", "/member/signup").permitAll() // "/" 경로는 모든 사용자에게 허용 + .anyRequest().authenticated() // 그 외의 경로는 인증된 사용자에게만 허용 ) .formLogin(form -> form - .loginProcessingUrl("/users/login") - .usernameParameter("user_id") - .passwordParameter("password") - .successHandler(customAuthenticationSuccessHandler) - .failureHandler(customAuthenticationFailureHandler) - .permitAll() + .loginProcessingUrl("/login") // 로그인 처리 URL + .usernameParameter("userId") //폼에서 받을 아이디 파라미터 + .passwordParameter("password") // 폼에서 받을 비밀번호 파라미터 + .failureHandler(loginFailHandler()) + .successHandler(loginSuccessHandler()) + .permitAll() // 로그인 페이지는 모든 사용자에게 허용 ); return http.build(); } + //해당 방식으로 스프링 시큐리티가 CustomUserDetails 객체에서 반환된 비밀번호와 로그인 요청에서 받은 비밀번호를 비교. @Bean public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + return new Bcrypt(); + } + + //인스턴스 생성 + @Bean + public AuthenticationFailureHandler loginFailHandler() { + return new LoginFailHandler(); + } + + //인스턴스 생성 + @Bean + public AuthenticationSuccessHandler loginSuccessHandler() { + return new LoginSuccessHandler(); } } diff --git a/board/src/main/java/com/main/board/member/DTO/MemberDTO.java b/board/src/main/java/com/main/board/member/DTO/MemberDTO.java new file mode 100644 index 0000000..e624847 --- /dev/null +++ b/board/src/main/java/com/main/board/member/DTO/MemberDTO.java @@ -0,0 +1,52 @@ +package com.main.board.member.DTO; + +import com.main.board.member.Member; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + + +@Getter +@NoArgsConstructor +public class MemberDTO { + + @NotBlank + private String userId; + @NotBlank + private String password; + @NotBlank + private String name; + @NotNull + private LocalDate createDate; + + + + public MemberDTO(String userId, String password, String name, LocalDate createDate) { + this.userId = validateId(userId); + this.password = validatePaswd(password); + this.name = name; + this.createDate = createDate; + } + + public String validateId(String userId) { + if (userId.length() < 5 || userId.length() > 20) { + throw new IllegalArgumentException("아이디는 5자 이상 20자 이하로 입력해주세요."); + } + return userId; + } + + public String validatePaswd(String password) { + if (password.length() < 8 || password.length() > 20) { + throw new IllegalArgumentException("비밀번호는 8자 이상 20자 이하로 입력해주세요."); + } + return password; + } + + public Member toMemberEntity(String pwd) { + return new Member(this.userId, pwd, this.name, this.createDate); + } + +} diff --git a/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java b/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java new file mode 100644 index 0000000..9ebfe54 --- /dev/null +++ b/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java @@ -0,0 +1,19 @@ +package com.main.board.member.DTO; + +import com.main.board.member.Member; + +import java.time.LocalDate; + +public class SignUpResponse { + private String memberId; + private String password; + private String name; + private LocalDate createDate; + + public SignUpResponse(Member member) { + this.memberId = member.getMemberId(); + this.password = member.getPassword(); + this.name = member.getName(); + this.createDate = member.getCreateDate(); + } +} diff --git a/board/src/main/java/com/main/board/member/Member.java b/board/src/main/java/com/main/board/member/Member.java new file mode 100644 index 0000000..bebf5e6 --- /dev/null +++ b/board/src/main/java/com/main/board/member/Member.java @@ -0,0 +1,29 @@ +package com.main.board.member; + +import lombok.Getter; +import lombok.ToString; +import org.springframework.security.core.userdetails.User; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@ToString +public class Member { + + + private String memberId; + private String password; + private String name; + private LocalDate createDate; + + + public Member(String memberId, String password, String name, LocalDate createDate) { + this.memberId = memberId; + this.password = password; + this.name = name; + this.createDate = createDate; + } + + +} diff --git a/board/src/main/java/com/main/board/user/controller/UserController.java b/board/src/main/java/com/main/board/member/controller/MemberController.java similarity index 52% rename from board/src/main/java/com/main/board/user/controller/UserController.java rename to board/src/main/java/com/main/board/member/controller/MemberController.java index e84d3be..a27b422 100644 --- a/board/src/main/java/com/main/board/user/controller/UserController.java +++ b/board/src/main/java/com/main/board/member/controller/MemberController.java @@ -1,7 +1,8 @@ -package com.main.board.user.controller; +package com.main.board.member.controller; -import com.main.board.user.DTO.UserDTO; -import com.main.board.user.service.UserService; +import com.main.board.member.DTO.MemberDTO; +import com.main.board.member.DTO.SignUpResponse; +import com.main.board.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,16 +11,16 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor -@RestController("/users") -public class UserController { +@RestController("/member") +public class MemberController { - private final UserService userService; + private final MemberService memberService; @PostMapping("/signup") - public ResponseEntity signup(@RequestBody UserDTO userDTO) { + public ResponseEntity signup(@RequestBody MemberDTO memberDTO) { try { - return new ResponseEntity<>(userService.signUp(userDTO), HttpStatus.OK); + return new ResponseEntity<>(memberService.signUp(memberDTO), HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml b/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml new file mode 100644 index 0000000..64f470c --- /dev/null +++ b/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + INSERT INTO member (member_id, password, name, create_date) + VALUES (#{memberId}, #{password}, #{name}, #{createDate}); + + + + + \ No newline at end of file diff --git a/board/src/main/java/com/main/board/member/repository/MemberRepository.java b/board/src/main/java/com/main/board/member/repository/MemberRepository.java new file mode 100644 index 0000000..311ad5f --- /dev/null +++ b/board/src/main/java/com/main/board/member/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package com.main.board.member.repository; + +import com.main.board.member.Member; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Optional; + +@Mapper +public interface MemberRepository { + void save(Member member); + Optional findMemberById(String user_id); // DetailsService에서 orElseThrow때문에 Optional사용 +} diff --git a/board/src/main/java/com/main/board/member/service/MemberService.java b/board/src/main/java/com/main/board/member/service/MemberService.java new file mode 100644 index 0000000..5c6b2ec --- /dev/null +++ b/board/src/main/java/com/main/board/member/service/MemberService.java @@ -0,0 +1,29 @@ +package com.main.board.member.service; + +import com.main.board.member.DTO.MemberDTO; +import com.main.board.member.DTO.SignUpResponse; +import com.main.board.member.Member; +import com.main.board.member.repository.MemberRepository; +import com.main.board.util.Bcrypt; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + private final Bcrypt bcrypt; + + + @Transactional + public SignUpResponse signUp(MemberDTO memberDTO) { + String encryptPwd = bcrypt.encrypt(memberDTO.getPassword()); + Member entity = memberDTO.toMemberEntity(encryptPwd); + memberRepository.save(entity); + return new SignUpResponse(entity); + } + +} diff --git a/board/src/main/java/com/main/board/security/PrincipalDetailsService.java b/board/src/main/java/com/main/board/security/PrincipalDetailsService.java deleted file mode 100644 index 6f71e9e..0000000 --- a/board/src/main/java/com/main/board/security/PrincipalDetailsService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.main.board.security; - -import com.main.board.user.repository.UserRepository; -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 PrincipalDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Override - public UserDetails loadUserByUsername(String user_id) throws UsernameNotFoundException { - boolean user = userRepository.findUserById(user_id); - if(!user) { - throw new UsernameNotFoundException("해당하는 사용자가 없습니다."); - } - return new PrincipalDetails(user); - } -} diff --git a/board/src/main/java/com/main/board/user/DTO/UserDTO.java b/board/src/main/java/com/main/board/user/DTO/UserDTO.java deleted file mode 100644 index acde585..0000000 --- a/board/src/main/java/com/main/board/user/DTO/UserDTO.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.main.board.user.DTO; - -import com.main.board.user.User; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; - - -@Getter -@NoArgsConstructor -public class UserDTO { - - @NotBlank - private String user_id; - @NotBlank - private String password; - @NotBlank - private String name; - @NotNull - private LocalDate create_date; - - - - public UserDTO(String user_id, String password, String name, LocalDate create_date) { - this.user_id = validateId(user_id); - this.password = validatePaswd(password); - this.name = name; - this.create_date = create_date; - } - - public String validateId(String user_id) { - if (user_id.length() < 5 || user_id.length() > 20) { - throw new IllegalArgumentException("아이디는 5자 이상 20자 이하로 입력해주세요."); - } - return user_id; - } - - public String validatePaswd(String password) { - if (password.length() < 8 || password.length() > 20) { - throw new IllegalArgumentException("비밀번호는 8자 이상 20자 이하로 입력해주세요."); - } - return password; - } - - public User toUserEntity(String pwd) { - return new User(this.user_id, pwd, this.name, this.create_date); - } - - - @Getter - @NoArgsConstructor - public static class SignUpResponse { - private String user_id; - private String password; - private String name; - private LocalDate create_date; - - public SignUpResponse(User user) { - this.user_id = user.getUser_id(); - this.password = user.getPassword(); - this.name = user.getName(); - this.create_date = user.getCreate_date(); - } - } - - @Getter - @NoArgsConstructor - public static class LoginResponse { - private String user_id; - private String password; - private String name; - private LocalDate create_date; - - public LoginResponse(User user) { - this.user_id = user.getUser_id(); - this.password = user.getPassword(); - this.name = user.getName(); - this.create_date = user.getCreate_date(); - } - } - -} diff --git a/board/src/main/java/com/main/board/user/User.java b/board/src/main/java/com/main/board/user/User.java deleted file mode 100644 index 77414d1..0000000 --- a/board/src/main/java/com/main/board/user/User.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.main.board.user; - -import lombok.Getter; -import lombok.ToString; - -import java.time.LocalDate; -import java.time.LocalDateTime; - -@Getter -@ToString -public class User { - - - private String user_id; - private String password; - private String name; - private LocalDate create_date; - - - public User(String user_id, String password, String name, LocalDate create_date) { - this.user_id = user_id; - this.password = password; - this.name = name; - this.create_date = create_date; - } - - public static User create(String email, String password, String name, LocalDate create_date) { - LocalDateTime now = DateTimeUtils.nowFromZone(); - return new User(email, password, name, birthDate, phone, role, now, now); - } - -} diff --git a/board/src/main/java/com/main/board/user/mapper/UserMapper.xml b/board/src/main/java/com/main/board/user/mapper/UserMapper.xml deleted file mode 100644 index 5fbcc89..0000000 --- a/board/src/main/java/com/main/board/user/mapper/UserMapper.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - INSERT INTO user (user_id, password, name, create_date) - VALUES (#{user_id}, #{password}, #{name}, #{create_date}); - - - - - \ No newline at end of file diff --git a/board/src/main/java/com/main/board/user/repository/UserRepository.java b/board/src/main/java/com/main/board/user/repository/UserRepository.java deleted file mode 100644 index 3f84e9a..0000000 --- a/board/src/main/java/com/main/board/user/repository/UserRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.main.board.user.repository; - -import com.main.board.user.DTO.UserDTO; -import com.main.board.user.User; -import org.apache.ibatis.annotations.Mapper; - -import java.util.Optional; - -@Mapper -public interface UserRepository { - void save(User user); - boolean findUserById(String user_id); -} diff --git a/board/src/main/java/com/main/board/user/service/UserService.java b/board/src/main/java/com/main/board/user/service/UserService.java deleted file mode 100644 index a76cad6..0000000 --- a/board/src/main/java/com/main/board/user/service/UserService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.main.board.user.service; - -import com.main.board.user.DTO.UserDTO; -import com.main.board.user.User; -import com.main.board.user.repository.UserRepository; -import com.main.board.util.Bcrypt; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.sql.SQLException; - -@Service -@RequiredArgsConstructor -public class UserService { - - private final UserRepository userRepository; - - private final Bcrypt bcrypt; - - - @Transactional - public UserDTO.SignUpResponse signUp(UserDTO userDTO) { - String encryptPwd = bcrypt.encrypt(userDTO.getPassword()); - User entity = userDTO.toUserEntity(encryptPwd); - userRepository.save(entity); - return new UserDTO.SignUpResponse(entity); - } - - public UserDTO.LoginResponse login(UserDTO userDTO) { - Boolean userCheck = userRepository.findUserById(userDTO.getUser_id()); - - if(userCheck) { - User user = userRepository.findByEmail(userDTO.getUser_id()); - if(bcrypt.isMatch(userDTO.getPassword(), user.getPassword())) { - return new UserDTO.LoginResponse(user); - } - else { - throw new UserNotFoundException("비밀번호가 일치하지 않습니다."); - } - } - else { - throw new UserNotFoundException("해당하는 사용자가 없습니다."); - } - - return new UserDTO.LoginResponse(user); - } -} diff --git a/board/src/main/java/com/main/board/util/Bcrypt.java b/board/src/main/java/com/main/board/util/Bcrypt.java index eb26c1a..99c9979 100644 --- a/board/src/main/java/com/main/board/util/Bcrypt.java +++ b/board/src/main/java/com/main/board/util/Bcrypt.java @@ -1,18 +1,42 @@ package com.main.board.util; import org.springframework.security.crypto.bcrypt.BCrypt; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +//PasswordEncoder는 시큐리티에서 비밀번호 암호화를 적용하기위해 상속 @Component -public final class Bcrypt { +public final class Bcrypt implements PasswordEncoder { + + /* + 작업팩터 동적할당 (작업팩터는 Bcrypt의 해시작업을 얼마나 복잡하게 할지 결정하는 값이다. 4~31사이의 값이 가능하다) + 작업팩터가 높을수록 해시작업이 복잡해지고 그만큼 시간이 오래걸린다. (기본값은 10) + 작업팩터가 높을수록 보안이 높아지지만 그만큼 시간이 오래걸린다 + */ + private static final int WORK_FACTOR = 12; // 12로 설정 public String encrypt(String password) { - return BCrypt.hashpw(password,BCrypt.gensalt()); + return BCrypt.hashpw(password,BCrypt.gensalt(WORK_FACTOR)); } public boolean isMatch(String password, String hashed) { return BCrypt.checkpw(password,hashed); } + //PasswordEncoder를 상속받아서 구현해야하는 메소드 + @Override + public String encode(CharSequence rawPassword) { + return org.springframework.security.crypto.bcrypt.BCrypt.hashpw(rawPassword.toString(), + org.springframework.security.crypto.bcrypt.BCrypt.gensalt(WORK_FACTOR)); + } + + //PasswordEncoder를 상속받아서 구현해야하는 메소드 + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return org.springframework.security.crypto.bcrypt.BCrypt.checkpw(rawPassword.toString(), encodedPassword); + } + + //그의외 커스텀가능사항은 DTO에서 비밀번호검증이 이루어지지만(복잡도검사도가능) 여기서도 가능하다 뭐가좋은지는 개인이판단 + } diff --git a/board/src/test/java/com/main/board/user/service/UserServiceTest.java b/board/src/test/java/com/main/board/member/service/UserServiceTest.java similarity index 55% rename from board/src/test/java/com/main/board/user/service/UserServiceTest.java rename to board/src/test/java/com/main/board/member/service/UserServiceTest.java index 7eb38ab..4bd0613 100644 --- a/board/src/test/java/com/main/board/user/service/UserServiceTest.java +++ b/board/src/test/java/com/main/board/member/service/UserServiceTest.java @@ -1,9 +1,7 @@ -package com.main.board.user.service; +package com.main.board.member.service; import org.springframework.boot.test.context.SpringBootTest; -import static org.junit.jupiter.api.Assertions.*; - @SpringBootTest class UserServiceTest { From 35e27071dc3b1f361cbffd5d9ad3c4341f7803ed Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Fri, 13 Dec 2024 07:25:58 +0900 Subject: [PATCH 03/10] =?UTF-8?q?#1=5F3=20=EC=84=B8=EC=85=98=EC=B6=94?= =?UTF-8?q?=EA=B0=80,JSON=EB=A1=9C=EA=B7=B8=EC=9D=B8=ED=98=95=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/board/config/CustomUserDetails.java | 1 - .../com/main/board/config/SecurityConfig.java | 71 +++++++++++++++---- ...ilsService.java => UserDetailService.java} | 5 +- .../main/board/login/DTO/LoginRequest.java | 11 +++ .../login/controller/LoginController.java | 65 +++++++++++++++++ .../util/{Bcrypt.java => BcryptEncoding.java} | 2 +- 6 files changed, 135 insertions(+), 20 deletions(-) rename board/src/main/java/com/main/board/config/{PrincipalDetailsService.java => UserDetailService.java} (81%) create mode 100644 board/src/main/java/com/main/board/login/DTO/LoginRequest.java create mode 100644 board/src/main/java/com/main/board/login/controller/LoginController.java rename board/src/main/java/com/main/board/util/{Bcrypt.java => BcryptEncoding.java} (96%) diff --git a/board/src/main/java/com/main/board/config/CustomUserDetails.java b/board/src/main/java/com/main/board/config/CustomUserDetails.java index e3032a1..3427588 100644 --- a/board/src/main/java/com/main/board/config/CustomUserDetails.java +++ b/board/src/main/java/com/main/board/config/CustomUserDetails.java @@ -10,7 +10,6 @@ public class CustomUserDetails implements UserDetails { - // security에 user가 아닌 member엔티티를 사용함 private final Member member; public CustomUserDetails(Member member) { diff --git a/board/src/main/java/com/main/board/config/SecurityConfig.java b/board/src/main/java/com/main/board/config/SecurityConfig.java index cfbb480..d7480fb 100644 --- a/board/src/main/java/com/main/board/config/SecurityConfig.java +++ b/board/src/main/java/com/main/board/config/SecurityConfig.java @@ -1,10 +1,16 @@ package com.main.board.config; -import com.main.board.util.Bcrypt; +import com.main.board.util.BcryptEncoding; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.SessionManagementConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -14,31 +20,66 @@ @EnableWebSecurity // Spring Security 활성화 기본보안 필터체인이 적용된다 public class SecurityConfig { - //로그인은 당연히 폼으로 사용할것같다는 가정 하에 설정 (JSON이있을라나?) - // TODO http://localhost:8081/member/signup 회원가입 테스트시 포스트맨에서 시큐리티 기본 폼화면이 리턴됨 + private UserDetailService userDetailsService; + + public SecurityConfig(UserDetailService userDetailsService) { + this.userDetailsService = userDetailsService; + } + @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //final HttpSecurity http 이점이있는가? http .csrf((auth) -> auth.disable()) // csrf 비활성화 (REST API등 비상태 통신에서는 CSRF토큰이 필요하지 않을수있다) + .httpBasic((auth) -> auth.disable()) // httpBasic 비활성화 + .formLogin((auth) -> auth.disable()) // formLogin 비활성화 .authorizeHttpRequests((auth) -> auth - .requestMatchers("/", "/member/signup").permitAll() // "/" 경로는 모든 사용자에게 허용 - .anyRequest().authenticated() // 그 외의 경로는 인증된 사용자에게만 허용 - ) - .formLogin(form -> form - .loginProcessingUrl("/login") // 로그인 처리 URL - .usernameParameter("userId") //폼에서 받을 아이디 파라미터 - .passwordParameter("password") // 폼에서 받을 비밀번호 파라미터 - .failureHandler(loginFailHandler()) - .successHandler(loginSuccessHandler()) - .permitAll() // 로그인 페이지는 모든 사용자에게 허용 + .requestMatchers("/", "/member/signup", "/auth/login").permitAll() // "/" 경로는 모든 사용자에게 허용 + .anyRequest().authenticated()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) //세션정책설정 (인증이 필요할때만 생성) + /* + **세션 고정 공격(Session Fixation Attack)**을 방지하기 위한 설정입니다. + 세션 고정 공격은 공격자가 사용자의 세션 ID를 미리 설정하거나 가로채어 악용하는 공격입니다. + + Spring Security에서는 세션 고정 공격을 방지하기 위해 새로운 세션을 생성하는 방식을 제공합니다. + newSession 설정은 인증 후 항상 새로운 세션을 생성합니다. + 사용자가 로그인하거나 인증할 때 기존 세션을 폐기하고 새로운 세션을 생성합니다. + 이로 인해 기존 세션 ID가 무효화되며, 세션 고정 공격을 방지합니다. + */ + .sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession)//세션고정공격방지 + .maximumSessions(1) // 동시세션수 제한 (하나의 사용자계정이 유지할수있는 세션의 수를 제한) ); + return http.build(); } + // 로그인시에 Spring Security의 인증 진입점이다 클라이언트가 제공한 인증정보를 받아 AuthenticationManager를 통해 인증을 위임한다, Bean설정 필수 + // LoginController에서 사용 + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + /* + 실제인증을 처리하는 부분인 Provider이다 + DaoAuthenticationProvider는 UserDetailsService를 통해 사용자 정보를 가져오고 비밀번호를 확인한다 + + */ + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + + provider.setUserDetailsService(userDetailsService); // 사용자 정보를 로드할 서비스 + provider.setPasswordEncoder(passwordEncoder()); // 비밀번호 암호화 확인 + + return provider; + } + + //해당 방식으로 스프링 시큐리티가 CustomUserDetails 객체에서 반환된 비밀번호와 로그인 요청에서 받은 비밀번호를 비교. @Bean public PasswordEncoder passwordEncoder() { - return new Bcrypt(); + return new BcryptEncoding(); } //인스턴스 생성 diff --git a/board/src/main/java/com/main/board/config/PrincipalDetailsService.java b/board/src/main/java/com/main/board/config/UserDetailService.java similarity index 81% rename from board/src/main/java/com/main/board/config/PrincipalDetailsService.java rename to board/src/main/java/com/main/board/config/UserDetailService.java index e30f93e..8cabe01 100644 --- a/board/src/main/java/com/main/board/config/PrincipalDetailsService.java +++ b/board/src/main/java/com/main/board/config/UserDetailService.java @@ -3,15 +3,14 @@ import com.main.board.member.Member; import com.main.board.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.User; -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; +// Spring Security는 해당 객체를 기반으로 인증을 처리한다 @Service @RequiredArgsConstructor -public class PrincipalDetailsService implements UserDetailsService { +public class UserDetailService implements UserDetailsService { private final MemberRepository memberRepository; diff --git a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java new file mode 100644 index 0000000..3e40e51 --- /dev/null +++ b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java @@ -0,0 +1,11 @@ +package com.main.board.login.DTO; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + private String username; + private String password; +} diff --git a/board/src/main/java/com/main/board/login/controller/LoginController.java b/board/src/main/java/com/main/board/login/controller/LoginController.java new file mode 100644 index 0000000..0a2b71c --- /dev/null +++ b/board/src/main/java/com/main/board/login/controller/LoginController.java @@ -0,0 +1,65 @@ +package com.main.board.login.controller; + +import com.main.board.login.DTO.LoginRequest; +import jakarta.servlet.http.HttpSession; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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 //JSON을 받기위한 어노테이션 +@RequestMapping("/auth") +public class LoginController { + + private final AuthenticationManager authenticationManager; + + public LoginController(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpSession session) { + try { + /* + 인증 요청 + 토큰이라고 해서 토큰방식을 사용하는것이 아니고 + Spring Security에서 토큰 기반의 인증을 수행하는 객체이다 + Spring Secutiry 내부 에서 사용자 인증 정보를 담기위한 객체이다 + */ + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); //토큰생성 + /* + 1. authenticationManager가 Provider에게 인증을 위임 + 2. config에 설정한 내용대로 UserDetailsService를 통해 유저정보를 가져온다 + 3. 입력된비밀번호와 가져온정보의 비밀번호를 비교하여 인증에 성공하면 Authentication 객체를 생성하여 리턴 + */ + Authentication authentication = authenticationManager.authenticate(token); // AuthenticationManager를 통해 인증을 시도한다 + + // 세션에 인증 정보 저장 + /* + SecurityContextHolder는 SpringSecurity의 인증 정보를 저장하고 조회하는 컨텍스트 + 1. 인증이 성공하면 Authentication 객체를 SecurityContextHolder에 저장 + 2. 이후 클라인언트의 요청은 사용자 인증상태를 유지하게끔 한다 + 3. SPRING_SECURITY_CONTEXT는 SpringSecurity에서 사용하는 세션의 기본키이다 + */ + SecurityContextHolder.getContext().setAuthentication(authentication); + session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); + + return ResponseEntity.ok("Login successful"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password"); + } + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpSession session) { + session.invalidate(); //로그아웃시에 세션을 무효화하여 저장된 모든 데이터를 삭제한다 "SPRING_SECURITY_CONTEXT"해당 키 삭제 + SecurityContextHolder.clearContext(); //SecurityContextHolder의 인증정보를 삭제한다 + return ResponseEntity.ok("Logout successful"); + } +} diff --git a/board/src/main/java/com/main/board/util/Bcrypt.java b/board/src/main/java/com/main/board/util/BcryptEncoding.java similarity index 96% rename from board/src/main/java/com/main/board/util/Bcrypt.java rename to board/src/main/java/com/main/board/util/BcryptEncoding.java index 99c9979..df2c67c 100644 --- a/board/src/main/java/com/main/board/util/Bcrypt.java +++ b/board/src/main/java/com/main/board/util/BcryptEncoding.java @@ -6,7 +6,7 @@ //PasswordEncoder는 시큐리티에서 비밀번호 암호화를 적용하기위해 상속 @Component -public final class Bcrypt implements PasswordEncoder { +public final class BcryptEncoding implements PasswordEncoder { /* 작업팩터 동적할당 (작업팩터는 Bcrypt의 해시작업을 얼마나 복잡하게 할지 결정하는 값이다. 4~31사이의 값이 가능하다) From 0f32fd705aded023b8a82a7df4ccd3fc8e35c116 Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Sat, 21 Dec 2024 17:42:54 +0900 Subject: [PATCH 04/10] =?UTF-8?q?#1=5F4=20test=EC=BD=94=EB=93=9C=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=8B=9C=EB=8F=84=EB=B0=8F=20validation=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{MemberDTO.java => SignupRequest.java} | 8 +-- .../member/controller/MemberController.java | 6 +- .../main/board/member/mapper/MemberMapper.xml | 2 +- .../board/member/service/MemberService.java | 13 ++--- .../java/com/main/board/util/Validator.java | 56 +++++++++++++++++++ .../member/service/MemberServiceTest.java | 43 ++++++++++++++ 6 files changed, 113 insertions(+), 15 deletions(-) rename board/src/main/java/com/main/board/member/DTO/{MemberDTO.java => SignupRequest.java} (85%) create mode 100644 board/src/main/java/com/main/board/util/Validator.java create mode 100644 board/src/test/java/com/main/board/member/service/MemberServiceTest.java diff --git a/board/src/main/java/com/main/board/member/DTO/MemberDTO.java b/board/src/main/java/com/main/board/member/DTO/SignupRequest.java similarity index 85% rename from board/src/main/java/com/main/board/member/DTO/MemberDTO.java rename to board/src/main/java/com/main/board/member/DTO/SignupRequest.java index e624847..f44f07d 100644 --- a/board/src/main/java/com/main/board/member/DTO/MemberDTO.java +++ b/board/src/main/java/com/main/board/member/DTO/SignupRequest.java @@ -11,12 +11,12 @@ @Getter @NoArgsConstructor -public class MemberDTO { +public class SignupRequest { @NotBlank private String userId; @NotBlank - private String password; + private String rawPassword; @NotBlank private String name; @NotNull @@ -24,9 +24,9 @@ public class MemberDTO { - public MemberDTO(String userId, String password, String name, LocalDate createDate) { + public SignupRequest(String userId, String password, String name, LocalDate createDate) { this.userId = validateId(userId); - this.password = validatePaswd(password); + this.rawPassword = validatePaswd(password); this.name = name; this.createDate = createDate; } diff --git a/board/src/main/java/com/main/board/member/controller/MemberController.java b/board/src/main/java/com/main/board/member/controller/MemberController.java index a27b422..c999ef7 100644 --- a/board/src/main/java/com/main/board/member/controller/MemberController.java +++ b/board/src/main/java/com/main/board/member/controller/MemberController.java @@ -1,6 +1,6 @@ package com.main.board.member.controller; -import com.main.board.member.DTO.MemberDTO; +import com.main.board.member.DTO.SignupRequest; import com.main.board.member.DTO.SignUpResponse; import com.main.board.member.service.MemberService; import lombok.RequiredArgsConstructor; @@ -18,9 +18,9 @@ public class MemberController { @PostMapping("/signup") - public ResponseEntity signup(@RequestBody MemberDTO memberDTO) { + public ResponseEntity signup(@RequestBody SignupRequest signupRequest) { try { - return new ResponseEntity<>(memberService.signUp(memberDTO), HttpStatus.OK); + return new ResponseEntity<>(memberService.signUp(signupRequest), HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml b/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml index 64f470c..70ad6c4 100644 --- a/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml +++ b/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml @@ -10,7 +10,7 @@ VALUES (#{memberId}, #{password}, #{name}, #{createDate}); - SELECT EXISTS (SELECT * FROM member WHERE member_id = #{memberId} diff --git a/board/src/main/java/com/main/board/member/service/MemberService.java b/board/src/main/java/com/main/board/member/service/MemberService.java index 5c6b2ec..96d10e3 100644 --- a/board/src/main/java/com/main/board/member/service/MemberService.java +++ b/board/src/main/java/com/main/board/member/service/MemberService.java @@ -1,10 +1,10 @@ package com.main.board.member.service; -import com.main.board.member.DTO.MemberDTO; +import com.main.board.member.DTO.SignupRequest; import com.main.board.member.DTO.SignUpResponse; import com.main.board.member.Member; import com.main.board.member.repository.MemberRepository; -import com.main.board.util.Bcrypt; +import com.main.board.util.BcryptEncoding; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,14 +14,13 @@ public class MemberService { private final MemberRepository memberRepository; - - private final Bcrypt bcrypt; + private final BcryptEncoding bcrypt; @Transactional - public SignUpResponse signUp(MemberDTO memberDTO) { - String encryptPwd = bcrypt.encrypt(memberDTO.getPassword()); - Member entity = memberDTO.toMemberEntity(encryptPwd); + public SignUpResponse signUp(SignupRequest signupRequest) { + String encryptPwd = bcrypt.encrypt(signupRequest.getRawPassword()); + Member entity = signupRequest.toMemberEntity(encryptPwd); memberRepository.save(entity); return new SignUpResponse(entity); } diff --git a/board/src/main/java/com/main/board/util/Validator.java b/board/src/main/java/com/main/board/util/Validator.java new file mode 100644 index 0000000..9bfa50d --- /dev/null +++ b/board/src/main/java/com/main/board/util/Validator.java @@ -0,0 +1,56 @@ +/*package com.main.board.util; + +public class Validator extends RuntimeException { + + public static void notBlank(final String input, final String fieldName) { + if (input == null || input.isBlank()) { + throw new RuntimeException(("%s 은/는 null 또는 공백이 될 수 없습니다.".formatted(fieldName))); + } + } + + public static void maxLength(final CharSequence input, final int maxLength, final String fieldName) { + if (maxLength <= 0) { + throw new RuntimeException("최대 길이는 0 이하일 수 없습니다."); + } + + if (input.length() > maxLength) { + throw new RuntimeException(fieldName + " 의 길이는 " + maxLength + " 글자 이하여야 합니다."); + } + } + + public static void minLength(final CharSequence input, final int minLength, final String fieldName) { + if (minLength <= 0) { + throw new RuntimeException("최소 길이는 0 이하일 수 없습니다."); + } + + if (input.length() < minLength) { + throw new RuntimeException(fieldName + " 의 길이는 " + minLength + " 글자 이상이어야 합니다."); + } + } + + public static void minValue(final long value, final long minValue, final String fieldName) { + if (value < minValue) { + throw new RuntimeException(fieldName + " 은/는 " + minValue + " 이상이어야 합니다."); + } + } + + public static void maxValue(final BigDecimal value, final int maxValue, final String fieldName) { + final var convertedMaxValue = BigDecimal.valueOf(maxValue); + + if (value.compareTo(convertedMaxValue) > 0) { + throw new RuntimeException(fieldName + " 은/는 " + maxValue + " 이하이어야 합니다."); + } + } + + public static void notNegative(final BigDecimal value, final String fieldName) { + if (value.compareTo(BigDecimal.ZERO) < 0) { + throw new RuntimeException(fieldName + " 은/는 음수가 될 수 없습니다."); + } + } + + public static void matchesRegex(final CharSequence input, final String pattern, final String patternDescription, final String fieldName) { + if (!input.toString().matches(pattern)) { + throw new RuntimeException(fieldName + " 은/는 " + patternDescription + " 조건을 충족해야 합니다."); + } + } +}*/ diff --git a/board/src/test/java/com/main/board/member/service/MemberServiceTest.java b/board/src/test/java/com/main/board/member/service/MemberServiceTest.java new file mode 100644 index 0000000..eb7472c --- /dev/null +++ b/board/src/test/java/com/main/board/member/service/MemberServiceTest.java @@ -0,0 +1,43 @@ +package com.main.board.member.service; + +import com.main.board.member.DTO.SignupRequest; +import com.main.board.member.repository.MemberRepository; +import com.main.board.util.BcryptEncoding; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class MemberServiceTest { + + @Mock + private MemberService memberService; + + @Mock + private BcryptEncoding bcryptEncoding; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("회원가입 테스트") + void signUp() { + // given + String rawPwd = "1234"; + String encodingPwd = bcryptEncoding.encrypt(rawPwd); + SignupRequest signupRequest = new SignupRequest("test", "1234", "김현성", LocalDate.now()); + + // when + memberService.signUp(signupRequest); + String actual = String.valueOf(memberRepository.findMemberById("test")); + // then + assertEquals(encodingPwd, bcryptEncoding.encrypt(rawPwd)); + assertThat(actual.getId()).isEqualTo(signupRequest); + + } +} \ No newline at end of file From 26ae524f074b98a1a17b7143dbde0feb6b9eca79 Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Thu, 26 Dec 2024 18:33:32 +0900 Subject: [PATCH 05/10] =?UTF-8?q?#1=5F4=20Exception=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/board/config/LoginFailHandler.java | 33 ----------- .../board/config/LoginSuccessHandler.java | 31 ---------- .../com/main/board/config/SecurityConfig.java | 17 +----- .../main/board/exception/CustomException.java | 4 -- .../main/board/exception/GlobalException.java | 4 -- .../main/board/login/DTO/LoginRequest.java | 2 +- .../login/controller/LoginController.java | 47 +++------------- .../exception/LoginExceptionHandler.java | 38 +++++++++++++ .../board/login/service/LoginService.java | 40 +++++++++++++ .../main/board/member/DTO/SignUpResponse.java | 2 - .../member/controller/MemberController.java | 6 +- .../exception/SignUpExceptionHandler.java | 19 +++++++ .../main/board/member/mapper/MemberMapper.xml | 2 +- .../board/member/service/MemberService.java | 6 +- ...BcryptEncoding.java => BcryptEncoder.java} | 2 +- .../com/main/board/util/PassWordEncoder.java | 20 +++++++ .../java/com/main/board/util/Validator.java | 56 ------------------- .../member/service/MemberServiceTest.java | 43 -------------- .../board/member/service/UserServiceTest.java | 12 ---- 19 files changed, 135 insertions(+), 249 deletions(-) delete mode 100644 board/src/main/java/com/main/board/config/LoginFailHandler.java delete mode 100644 board/src/main/java/com/main/board/config/LoginSuccessHandler.java delete mode 100644 board/src/main/java/com/main/board/exception/CustomException.java delete mode 100644 board/src/main/java/com/main/board/exception/GlobalException.java create mode 100644 board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java create mode 100644 board/src/main/java/com/main/board/login/service/LoginService.java create mode 100644 board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java rename board/src/main/java/com/main/board/util/{BcryptEncoding.java => BcryptEncoder.java} (96%) create mode 100644 board/src/main/java/com/main/board/util/PassWordEncoder.java delete mode 100644 board/src/main/java/com/main/board/util/Validator.java delete mode 100644 board/src/test/java/com/main/board/member/service/MemberServiceTest.java delete mode 100644 board/src/test/java/com/main/board/member/service/UserServiceTest.java diff --git a/board/src/main/java/com/main/board/config/LoginFailHandler.java b/board/src/main/java/com/main/board/config/LoginFailHandler.java deleted file mode 100644 index 0313b12..0000000 --- a/board/src/main/java/com/main/board/config/LoginFailHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.main.board.config; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -/* - 기본적으로 시큐리티에서 로그인 실패시에는 SimpleUrlAuthenticationFailureHandler를 사용한다. - 사용자가 인증에 실패하면 AuthenticationException이 발생 - SimpleUrlAuthenticationFailureHandler는 기본적으로 실패 후 로그인 페이지로 리다이렉트하며, URL에 ?error를 추가 - */ - -public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler { - - @Override - public void onAuthenticationFailure(HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - //로그인 실패로그 - System.out.println("로그인 실패: " + exception.getMessage()); - // 실패 응답 커스터마이즈 - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 401 Unauthorized - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write("{\"error\": \"Login failed\", \"message\": \"" + exception.getMessage() + "\"}"); - } -} diff --git a/board/src/main/java/com/main/board/config/LoginSuccessHandler.java b/board/src/main/java/com/main/board/config/LoginSuccessHandler.java deleted file mode 100644 index 656b853..0000000 --- a/board/src/main/java/com/main/board/config/LoginSuccessHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.main.board.config; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - - -public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) throws IOException, ServletException { - // 성공 로그 출력 - System.out.println("로그인 성공: " + authentication.getName()); - - // 성공 응답 커스터마이징 (예: JSON 응답 반환) - response.setStatus(HttpServletResponse.SC_OK); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write("{\"message\": \"Login successful\", \"user\": \"" + authentication.getName() + "\"}"); - } - - -} diff --git a/board/src/main/java/com/main/board/config/SecurityConfig.java b/board/src/main/java/com/main/board/config/SecurityConfig.java index d7480fb..7652035 100644 --- a/board/src/main/java/com/main/board/config/SecurityConfig.java +++ b/board/src/main/java/com/main/board/config/SecurityConfig.java @@ -1,6 +1,6 @@ package com.main.board.config; -import com.main.board.util.BcryptEncoding; +import com.main.board.util.PassWordEncoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -13,8 +13,6 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @Configuration // 스프링부트에게 이 클래스가 설정파일임을 알려줌 (빈등록) @EnableWebSecurity // Spring Security 활성화 기본보안 필터체인이 적용된다 @@ -79,18 +77,7 @@ public AuthenticationProvider authenticationProvider() { //해당 방식으로 스프링 시큐리티가 CustomUserDetails 객체에서 반환된 비밀번호와 로그인 요청에서 받은 비밀번호를 비교. @Bean public PasswordEncoder passwordEncoder() { - return new BcryptEncoding(); + return new PassWordEncoder(); } - //인스턴스 생성 - @Bean - public AuthenticationFailureHandler loginFailHandler() { - return new LoginFailHandler(); - } - - //인스턴스 생성 - @Bean - public AuthenticationSuccessHandler loginSuccessHandler() { - return new LoginSuccessHandler(); - } } diff --git a/board/src/main/java/com/main/board/exception/CustomException.java b/board/src/main/java/com/main/board/exception/CustomException.java deleted file mode 100644 index d2d8016..0000000 --- a/board/src/main/java/com/main/board/exception/CustomException.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.main.board.exception; - -public class CustomException { -} diff --git a/board/src/main/java/com/main/board/exception/GlobalException.java b/board/src/main/java/com/main/board/exception/GlobalException.java deleted file mode 100644 index 3ed3c3f..0000000 --- a/board/src/main/java/com/main/board/exception/GlobalException.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.main.board.exception; - -public class GlobalException { -} diff --git a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java index 3e40e51..ba72542 100644 --- a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java +++ b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java @@ -7,5 +7,5 @@ @Setter public class LoginRequest { private String username; - private String password; + private String rawPassword; } diff --git a/board/src/main/java/com/main/board/login/controller/LoginController.java b/board/src/main/java/com/main/board/login/controller/LoginController.java index 0a2b71c..3b7408c 100644 --- a/board/src/main/java/com/main/board/login/controller/LoginController.java +++ b/board/src/main/java/com/main/board/login/controller/LoginController.java @@ -1,12 +1,9 @@ package com.main.board.login.controller; import com.main.board.login.DTO.LoginRequest; +import com.main.board.login.service.LoginService; import jakarta.servlet.http.HttpSession; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,49 +14,23 @@ @RequestMapping("/auth") public class LoginController { - private final AuthenticationManager authenticationManager; - public LoginController(AuthenticationManager authenticationManager) { - this.authenticationManager = authenticationManager; + private final LoginService loginService; + + public LoginController(LoginService loginService) { + this.loginService = loginService; } @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpSession session) { - try { - /* - 인증 요청 - 토큰이라고 해서 토큰방식을 사용하는것이 아니고 - Spring Security에서 토큰 기반의 인증을 수행하는 객체이다 - Spring Secutiry 내부 에서 사용자 인증 정보를 담기위한 객체이다 - */ - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); //토큰생성 - /* - 1. authenticationManager가 Provider에게 인증을 위임 - 2. config에 설정한 내용대로 UserDetailsService를 통해 유저정보를 가져온다 - 3. 입력된비밀번호와 가져온정보의 비밀번호를 비교하여 인증에 성공하면 Authentication 객체를 생성하여 리턴 - */ - Authentication authentication = authenticationManager.authenticate(token); // AuthenticationManager를 통해 인증을 시도한다 - - // 세션에 인증 정보 저장 - /* - SecurityContextHolder는 SpringSecurity의 인증 정보를 저장하고 조회하는 컨텍스트 - 1. 인증이 성공하면 Authentication 객체를 SecurityContextHolder에 저장 - 2. 이후 클라인언트의 요청은 사용자 인증상태를 유지하게끔 한다 - 3. SPRING_SECURITY_CONTEXT는 SpringSecurity에서 사용하는 세션의 기본키이다 - */ - SecurityContextHolder.getContext().setAuthentication(authentication); - session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); - - return ResponseEntity.ok("Login successful"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password"); - } + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + loginService.login(loginRequest); + return ResponseEntity.ok("Login successful"); } @PostMapping("/logout") public ResponseEntity logout(HttpSession session) { session.invalidate(); //로그아웃시에 세션을 무효화하여 저장된 모든 데이터를 삭제한다 "SPRING_SECURITY_CONTEXT"해당 키 삭제 SecurityContextHolder.clearContext(); //SecurityContextHolder의 인증정보를 삭제한다 - return ResponseEntity.ok("Logout successful"); + return ResponseEntity.ok().build(); // build()는 바디없이 빈 응답을 생성한다 } } diff --git a/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java b/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java new file mode 100644 index 0000000..55db332 --- /dev/null +++ b/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java @@ -0,0 +1,38 @@ +package com.main.board.login.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/* + @RestControllerAdvice 와 @ControllerAdvice 는 예외처리를 전역적으로 처리하기 위한 어노테이션 (spring 3.2부터 지원 Rest는 4.3부터 지원) + 두개의 차이는 Rest즉 @ResponseBody가 유무 차이로 응답이 Json으로 내려주는지 아닌지의 차이이다 + @ResponseBody는 메소드의 반환값을 Http 응답 바디에 직접 넣어주겠다는 의미이다 + */ +@RestControllerAdvice(basePackages = "com.main.board.login.controller") +public class LoginExceptionHandler { + + /* + @ExceptionHandler 어노테이션은 특정 예외가 발생했을 때 메소드가 처리하도록 하는 어노테이션이다 + 여기서는 BadCredentialsException 예외가 발생했을 때 handleBadCredentialsException 메소드가 처리하도록 한다 + ProblemDetail은 Spring(6부터)에서 제공하는 클래스로 예외처리시 상태코드와 상세정보를 담아서 반환할 수 있다 + 기존방식으로 처리하게되면 ResponseEntity> 형식으로 직접 관리를 해야하지만 ProblemDetail을 사용하면 편리하게 처리할 수 있다 + */ + + //로그인 실패 (비밀번호 불일치) 예외처리 + @ExceptionHandler(BadCredentialsException.class) + public ProblemDetail handleBadCredentialsException(BadCredentialsException e) { + return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다"); + } + + //로그인 실패 (아이디 불일치) 예외처리 + @ExceptionHandler(UsernameNotFoundException.class) + public ProblemDetail handleUsernameNotFoundException(UsernameNotFoundException e) { + return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "아이디를 찾을수없습니다"); + } + + +} diff --git a/board/src/main/java/com/main/board/login/service/LoginService.java b/board/src/main/java/com/main/board/login/service/LoginService.java new file mode 100644 index 0000000..8736fe7 --- /dev/null +++ b/board/src/main/java/com/main/board/login/service/LoginService.java @@ -0,0 +1,40 @@ +package com.main.board.login.service; + +import com.main.board.login.DTO.LoginRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final AuthenticationManager authenticationManager; + + public void login(LoginRequest loginRequest) { + /* + 인증 요청 + 토큰이라고 해서 토큰방식을 사용하는것이 아니고 + Spring Security에서 토큰 기반의 인증을 수행하는 객체이다 + Spring Secutiry 내부 에서 사용자 인증 정보를 담기위한 객체이다 + */ + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getRawPassword()); //토큰생성 + /* + 1. authenticationManager가 Provider에게 인증을 위임 + 2. config에 설정한 내용대로 UserDetailsService를 통해 유저정보를 가져온다 + 3. 입력된비밀번호와 가져온정보의 비밀번호를 비교하여 인증에 성공하면 Authentication 객체를 생성하여 리턴 + */ + Authentication authentication = authenticationManager.authenticate(token); // AuthenticationManager를 통해 인증을 시도한다 + // 세션에 인증 정보 저장 + /* + SecurityContextHolder는 SpringSecurity의 인증 정보를 저장하고 조회하는 컨텍스트 + 1. 인증이 성공하면 Authentication 객체를 SecurityContextHolder에 저장 + 2. 이후 클라인언트의 요청은 사용자 인증상태를 유지하게끔 한다 + 3. SPRING_SECURITY_CONTEXT는 SpringSecurity에서 사용하는 세션의 기본키이다 + */ + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java b/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java index 9ebfe54..2224df0 100644 --- a/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java +++ b/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java @@ -6,13 +6,11 @@ public class SignUpResponse { private String memberId; - private String password; private String name; private LocalDate createDate; public SignUpResponse(Member member) { this.memberId = member.getMemberId(); - this.password = member.getPassword(); this.name = member.getName(); this.createDate = member.getCreateDate(); } diff --git a/board/src/main/java/com/main/board/member/controller/MemberController.java b/board/src/main/java/com/main/board/member/controller/MemberController.java index c999ef7..9828353 100644 --- a/board/src/main/java/com/main/board/member/controller/MemberController.java +++ b/board/src/main/java/com/main/board/member/controller/MemberController.java @@ -19,11 +19,7 @@ public class MemberController { @PostMapping("/signup") public ResponseEntity signup(@RequestBody SignupRequest signupRequest) { - try { - return new ResponseEntity<>(memberService.signUp(signupRequest), HttpStatus.OK); - } catch (Exception e) { - return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); - } + return ResponseEntity.ok(memberService.signUp(signupRequest)); } diff --git a/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java b/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java new file mode 100644 index 0000000..39566fc --- /dev/null +++ b/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java @@ -0,0 +1,19 @@ +package com.main.board.member.exception; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + + +@RestControllerAdvice(basePackages = "com.main.board.member.controller") +public class SignUpExceptionHandler { + + //회원가입 실패 + @ExceptionHandler(Exception.class) + public ProblemDetail handleGeneralException(Exception e) { + return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "회원가입에 실패하였습니다."); + } +} diff --git a/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml b/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml index 70ad6c4..3cb8913 100644 --- a/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml +++ b/board/src/main/java/com/main/board/member/mapper/MemberMapper.xml @@ -11,7 +11,7 @@ - SELECT EXISTS (SELECT 1 - FROM member - WHERE member_id = #{memberId} - ); - - - \ No newline at end of file diff --git a/board/src/main/java/com/main/board/member/repository/MemberRepository.java b/board/src/main/java/com/main/board/member/repository/MemberRepository.java index 311ad5f..debeccc 100644 --- a/board/src/main/java/com/main/board/member/repository/MemberRepository.java +++ b/board/src/main/java/com/main/board/member/repository/MemberRepository.java @@ -8,5 +8,6 @@ @Mapper public interface MemberRepository { void save(Member member); - Optional findMemberById(String user_id); // DetailsService에서 orElseThrow때문에 Optional사용 + boolean existsByEmail(String email); + Optional findMemberByEmail(String email); // DetailsService에서 orElseThrow때문에 Optional사용 } diff --git a/board/src/main/java/com/main/board/member/service/MemberService.java b/board/src/main/java/com/main/board/member/service/MemberService.java index d02aece..e6265e4 100644 --- a/board/src/main/java/com/main/board/member/service/MemberService.java +++ b/board/src/main/java/com/main/board/member/service/MemberService.java @@ -3,6 +3,7 @@ import com.main.board.member.DTO.SignupRequest; import com.main.board.member.DTO.SignUpResponse; import com.main.board.member.Member; +import com.main.board.member.exception.EmailDuplicatedException; import com.main.board.member.repository.MemberRepository; import com.main.board.util.BcryptEncoder; import lombok.RequiredArgsConstructor; @@ -19,10 +20,14 @@ public class MemberService { @Transactional public SignUpResponse signUp(SignupRequest signupRequest) { - String encryptPwd = bcryptEncoder.encrypt(signupRequest.getRawPassword()); - Member entity = signupRequest.toMemberEntity(encryptPwd); - memberRepository.save(entity); - return new SignUpResponse(entity); + if(memberRepository.existsByEmail(signupRequest.getEmail())) { + throw new EmailDuplicatedException("이미 가입된 이메일입니다."); + } + String encryptPwd = bcryptEncoder.encrypt(signupRequest.getRawPassword()); + Member entity = signupRequest.toMemberEntity(encryptPwd); + memberRepository.save(entity); + SignUpResponse response = new SignUpResponse(entity); + return response; } } diff --git a/board/src/main/resources/application-db.properties b/board/src/main/resources/application-db.properties deleted file mode 100644 index 88cade7..0000000 --- a/board/src/main/resources/application-db.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.datasource.url=jdbc:mysql://223.130.158.110:3306/board?useSSL=false&serverTimezone=UTC -spring.datasource.username=kim -spring.datasource.password=1234 -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/board/src/main/resources/mappers/MemberMapper.xml b/board/src/main/resources/mappers/MemberMapper.xml new file mode 100644 index 0000000..796c6bd --- /dev/null +++ b/board/src/main/resources/mappers/MemberMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + INSERT INTO member (email, password, create_at) + VALUES (#{email}, #{encryptPwd}, #{createAt}); + + + + + + + \ No newline at end of file From 8326c73be32e313b304867d918704bc349dd7328 Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Mon, 13 Jan 2025 15:41:14 +0900 Subject: [PATCH 09/10] =?UTF-8?q?#1=5F8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=99=84=EC=84=B1(=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=99=84=EB=A3=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/board/login/DTO/LoginRequest.java | 6 ++++- .../login/controller/LoginController.java | 3 ++- .../exception/LoginExceptionHandler.java | 27 +++++++++++++++---- .../board/login/service/LoginService.java | 2 +- .../main/board/member/DTO/SignupRequest.java | 2 +- .../exception/SignUpExceptionHandler.java | 4 +-- .../com/main/board/util/BcryptEncoder.java | 21 +-------------- .../com/main/board/util/PassWordEncoder.java | 2 +- .../main/resources/mappers/MemberMapper.xml | 2 +- 9 files changed, 35 insertions(+), 34 deletions(-) diff --git a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java index 816b9a3..89fa2d7 100644 --- a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java +++ b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java @@ -1,11 +1,15 @@ package com.main.board.login.DTO; +import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @Getter @Setter public class LoginRequest { - private String memberId; + @NotEmpty(message = "이메일을 입력해주세요.") + private String email; + + @NotEmpty(message = "비밀번호를 입력해주세요.") private String rawPassword; } diff --git a/board/src/main/java/com/main/board/login/controller/LoginController.java b/board/src/main/java/com/main/board/login/controller/LoginController.java index 3b7408c..cd12514 100644 --- a/board/src/main/java/com/main/board/login/controller/LoginController.java +++ b/board/src/main/java/com/main/board/login/controller/LoginController.java @@ -3,6 +3,7 @@ import com.main.board.login.DTO.LoginRequest; import com.main.board.login.service.LoginService; import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +23,7 @@ public LoginController(LoginService loginService) { } @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest) { loginService.login(loginRequest); return ResponseEntity.ok("Login successful"); } diff --git a/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java b/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java index 55db332..f2d083b 100644 --- a/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java +++ b/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java @@ -4,6 +4,8 @@ import org.springframework.http.ProblemDetail; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -23,15 +25,30 @@ public class LoginExceptionHandler { */ //로그인 실패 (비밀번호 불일치) 예외처리 + // UsernameNotFoundException은 필요없다 (BadCredentialsException로 spring security에서 처리) @ExceptionHandler(BadCredentialsException.class) public ProblemDetail handleBadCredentialsException(BadCredentialsException e) { - return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다"); + return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "아이디or비밀번호가 일치하지 않습니다"); } - //로그인 실패 (아이디 불일치) 예외처리 - @ExceptionHandler(UsernameNotFoundException.class) - public ProblemDetail handleUsernameNotFoundException(UsernameNotFoundException e) { - return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "아이디를 찾을수없습니다"); + //유효성 검증 실패 처리 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // 첫 번째 오류 메시지 추출 + FieldError fieldError = e.getBindingResult().getFieldErrors().get(0); // 첫 번째 오류만 처리 + String field = fieldError.getField(); + String defaultMessage = fieldError.getDefaultMessage(); + + // 이메일 오류 + if ("email".equals(field)) { + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); + } + + // 비밀번호 오류 + if ("rawPassword".equals(field)) { + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); + } + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "유효성 검증 실패"); } diff --git a/board/src/main/java/com/main/board/login/service/LoginService.java b/board/src/main/java/com/main/board/login/service/LoginService.java index 88e40d3..4a5575a 100644 --- a/board/src/main/java/com/main/board/login/service/LoginService.java +++ b/board/src/main/java/com/main/board/login/service/LoginService.java @@ -21,7 +21,7 @@ public void login(LoginRequest loginRequest) { Spring Security에서 토큰 기반의 인증을 수행하는 객체이다 Spring Secutiry 내부 에서 사용자 인증 정보를 담기위한 객체이다 */ - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getMemberId(), loginRequest.getRawPassword()); //토큰생성 + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getRawPassword()); //토큰생성 /* 1. authenticationManager가 Provider에게 인증을 위임 2. config에 설정한 내용대로 UserDetailsService를 통해 유저정보를 가져온다 diff --git a/board/src/main/java/com/main/board/member/DTO/SignupRequest.java b/board/src/main/java/com/main/board/member/DTO/SignupRequest.java index ad3eadc..509562e 100644 --- a/board/src/main/java/com/main/board/member/DTO/SignupRequest.java +++ b/board/src/main/java/com/main/board/member/DTO/SignupRequest.java @@ -18,7 +18,7 @@ public class SignupRequest { @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$", message = "유효한 이메일 형식이아닙니다") private String email; - @NotEmpty + @NotEmpty(message = "비밀번호를 입력해주세요.") @Size(min=6, max=20, message = "비밀번호는 6자 이상 20자 이하로 입력해주세요.") @Pattern(regexp = "^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,}$", message = "비밀번호는 소문자,숫자,특수문자 를포함해야 합니다.") private String rawPassword; diff --git a/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java b/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java index 3e4446b..64c5308 100644 --- a/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java +++ b/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java @@ -48,9 +48,7 @@ public ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotVali if ("rawPassword".equals(field)) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); } - - - return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "유효성 검증 실패"); + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "회원가입 유효성 검증 실패"); } } diff --git a/board/src/main/java/com/main/board/util/BcryptEncoder.java b/board/src/main/java/com/main/board/util/BcryptEncoder.java index 27a70f9..53a318e 100644 --- a/board/src/main/java/com/main/board/util/BcryptEncoder.java +++ b/board/src/main/java/com/main/board/util/BcryptEncoder.java @@ -6,7 +6,7 @@ //PasswordEncoder는 시큐리티에서 비밀번호 암호화를 적용하기위해 상속 @Component -public final class BcryptEncoder implements PasswordEncoder { +public final class BcryptEncoder { /* 작업팩터 동적할당 (작업팩터는 Bcrypt의 해시작업을 얼마나 복잡하게 할지 결정하는 값이다. 4~31사이의 값이 가능하다) @@ -19,24 +19,5 @@ public String encrypt(String password) { return BCrypt.hashpw(password,BCrypt.gensalt(WORK_FACTOR)); } - public boolean isMatch(String password, String hashed) { - return BCrypt.checkpw(password,hashed); - } - - //PasswordEncoder를 상속받아서 구현해야하는 메소드 - @Override - public String encode(CharSequence rawPassword) { - return org.springframework.security.crypto.bcrypt.BCrypt.hashpw(rawPassword.toString(), - org.springframework.security.crypto.bcrypt.BCrypt.gensalt(WORK_FACTOR)); - } - - //PasswordEncoder를 상속받아서 구현해야하는 메소드 - @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { - return org.springframework.security.crypto.bcrypt.BCrypt.checkpw(rawPassword.toString(), encodedPassword); - } - - //그의외 커스텀가능사항은 DTO에서 비밀번호검증이 이루어지지만(복잡도검사도가능) 여기서도 가능하다 뭐가좋은지는 개인이판단 - } diff --git a/board/src/main/java/com/main/board/util/PassWordEncoder.java b/board/src/main/java/com/main/board/util/PassWordEncoder.java index f07cf3c..46ec80a 100644 --- a/board/src/main/java/com/main/board/util/PassWordEncoder.java +++ b/board/src/main/java/com/main/board/util/PassWordEncoder.java @@ -13,7 +13,7 @@ public String encode(CharSequence rawPassword) { } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { - return rawPassword.toString().equals(encodedPassword); + return BCrypt.checkpw(rawPassword.toString(),encodedPassword); } diff --git a/board/src/main/resources/mappers/MemberMapper.xml b/board/src/main/resources/mappers/MemberMapper.xml index 796c6bd..2591a5f 100644 --- a/board/src/main/resources/mappers/MemberMapper.xml +++ b/board/src/main/resources/mappers/MemberMapper.xml @@ -15,7 +15,7 @@ From fdea6428b6a227e1f4d541ead408211c18a5217a Mon Sep 17 00:00:00 2001 From: rlawltjd8547 Date: Tue, 14 Jan 2025 10:34:04 +0900 Subject: [PATCH 10/10] =?UTF-8?q?#1=5F9=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=B0=A9=EC=8B=9D->security=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ndler.java => GlobalExceptionHandler.java} | 34 +++++----- .../CustomAuthenticationEntryPoint.java | 27 ++++++++ ...mUsernamePasswordAuthenticationFilter.java | 67 +++++++++++++++++++ .../com/main/board/config/SecurityConfig.java | 19 ++++-- .../main/board/login/DTO/LoginRequest.java | 3 - .../login/controller/LoginController.java | 37 ---------- .../exception/LoginExceptionHandler.java | 55 --------------- .../board/login/service/LoginService.java | 42 ------------ .../main/board/member/DTO/SignUpResponse.java | 20 ------ .../member/controller/MemberController.java | 7 +- .../board/member/service/MemberService.java | 11 ++- .../com/main/board/util/BcryptEncoder.java | 23 ------- .../com/main/board/util/PassWordEncoder.java | 2 + 13 files changed, 136 insertions(+), 211 deletions(-) rename board/src/main/java/com/main/board/{member/exception/SignUpExceptionHandler.java => GlobalExceptionHandler.java} (66%) create mode 100644 board/src/main/java/com/main/board/config/CustomAuthenticationEntryPoint.java create mode 100644 board/src/main/java/com/main/board/config/CustomUsernamePasswordAuthenticationFilter.java delete mode 100644 board/src/main/java/com/main/board/login/controller/LoginController.java delete mode 100644 board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java delete mode 100644 board/src/main/java/com/main/board/login/service/LoginService.java delete mode 100644 board/src/main/java/com/main/board/member/DTO/SignUpResponse.java delete mode 100644 board/src/main/java/com/main/board/util/BcryptEncoder.java diff --git a/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java b/board/src/main/java/com/main/board/GlobalExceptionHandler.java similarity index 66% rename from board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java rename to board/src/main/java/com/main/board/GlobalExceptionHandler.java index 64c5308..a039898 100644 --- a/board/src/main/java/com/main/board/member/exception/SignUpExceptionHandler.java +++ b/board/src/main/java/com/main/board/GlobalExceptionHandler.java @@ -1,54 +1,58 @@ -package com.main.board.member.exception; - +package com.main.board; +import com.main.board.member.exception.EmailDuplicatedException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice(basePackages = "com.main.board.member.controller") -public class SignUpExceptionHandler { +@ControllerAdvice +public class GlobalExceptionHandler { - //회원가입 실패 + //서버 500 에러 @ExceptionHandler(Exception.class) public ProblemDetail handleGeneralException() { - return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "회원가입에 실패하였습니다."); + return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "서버에러입니다 다시시도해주세요."); } - // 이메일 중복 예외 처리 + // 이메일 중복 에러 @ExceptionHandler(EmailDuplicatedException.class) public ProblemDetail handleEmailDuplicatedException() { return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "이미 사용중인 이메일입니다."); } - //파라마터가 제대로 전달되지않았을때 + //nullpoint 에러 (파라미터 누락) @ExceptionHandler(NullPointerException.class) public ProblemDetail handleNullPointerException() { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "파라미터가 제대로 전달되지 않았습니다."); } - //유효성 검증 실패 처리 + // IllegalArgumentException 처리 + @ExceptionHandler(IllegalArgumentException.class) + public ProblemDetail handleIllegalArgumentException(IllegalArgumentException e) { + if(e.getMessage() != null) { + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); + } + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "파라미터가 제대로 전달되지 않았습니다."); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { // 첫 번째 오류 메시지 추출 FieldError fieldError = e.getBindingResult().getFieldErrors().get(0); // 첫 번째 오류만 처리 String field = fieldError.getField(); String defaultMessage = fieldError.getDefaultMessage(); - // 이메일 오류 if ("email".equals(field)) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); } - // 비밀번호 오류 if ("rawPassword".equals(field)) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); } - return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "회원가입 유효성 검증 실패"); + return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "유효성 검증 실패"); } } diff --git a/board/src/main/java/com/main/board/config/CustomAuthenticationEntryPoint.java b/board/src/main/java/com/main/board/config/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..9b870eb --- /dev/null +++ b/board/src/main/java/com/main/board/config/CustomAuthenticationEntryPoint.java @@ -0,0 +1,27 @@ +package com.main.board.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + // 인증 실패 시 호출되는 메소드 + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + + // 401 Unauthorized 응답 상태 코드 설정 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + // 클라이언트에게 반환할 메시지 설정 + response.getWriter().write("로그인 실패: 인증이 필요합니다."); + } +} diff --git a/board/src/main/java/com/main/board/config/CustomUsernamePasswordAuthenticationFilter.java b/board/src/main/java/com/main/board/config/CustomUsernamePasswordAuthenticationFilter.java new file mode 100644 index 0000000..37eef97 --- /dev/null +++ b/board/src/main/java/com/main/board/config/CustomUsernamePasswordAuthenticationFilter.java @@ -0,0 +1,67 @@ +package com.main.board.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.main.board.login.DTO.LoginRequest; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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 org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private static final String LOGIN_URL = "/auth/login"; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public CustomUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { + super(authenticationManager); + super.setFilterProcessesUrl(LOGIN_URL); + setSecurityContextRepository(new HttpSessionSecurityContextRepository()); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { + try { + LoginRequest loginRequest = new LoginRequest(); + loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class); + String email = loginRequest.getEmail(); + String password = loginRequest.getRawPassword(); + + //@valid 어노테이션 사용불가 validation을 직접 구현하기엔좀 단점같음 + //직접구현 + if (email == null || email.isEmpty()) { + throw new IllegalArgumentException("이메일을 입력해주세요."); + } + if (password == null || password.isEmpty()) { + throw new IllegalArgumentException("비밀번호를 입력해주세요."); + } + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(email, password); + return this.getAuthenticationManager().authenticate(token); + } catch (IOException e) { + throw new RuntimeException("Failed to parse authentication request body"); + } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException { + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("로그인 성공!"); // 로그인 성공 + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException failed) throws java.io.IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("로그인 실패!" + failed.getMessage()); // 로그인 실패 + } + + +} diff --git a/board/src/main/java/com/main/board/config/SecurityConfig.java b/board/src/main/java/com/main/board/config/SecurityConfig.java index ac9517b..7d0ebfa 100644 --- a/board/src/main/java/com/main/board/config/SecurityConfig.java +++ b/board/src/main/java/com/main/board/config/SecurityConfig.java @@ -14,19 +14,22 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration // 스프링부트에게 이 클래스가 설정파일임을 알려줌 (빈등록) @EnableWebSecurity // Spring Security 활성화 기본보안 필터체인이 적용된다 public class SecurityConfig { - private UserDetailService userDetailsService; + private final UserDetailService userDetailsService; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - public SecurityConfig(UserDetailService userDetailsService) { + public SecurityConfig(UserDetailService userDetailsService, CustomAuthenticationEntryPoint customAuthenticationEntryPoint) { this.userDetailsService = userDetailsService; + this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; } @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //final HttpSecurity http 이점이있는가? + public SecurityFilterChain filterChain(HttpSecurity http, CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter) throws Exception { http .csrf((auth) -> auth.disable()) // csrf 비활성화 (REST API등 비상태 통신에서는 CSRF토큰이 필요하지 않을수있다) .httpBasic((auth) -> auth.disable()) // httpBasic 비활성화 @@ -34,6 +37,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //f .authorizeHttpRequests((auth) -> auth .requestMatchers("/", "/member/signup", "/auth/login").permitAll() // "/" 경로는 모든 사용자에게 허용 .anyRequest().authenticated()) + .addFilterAt(customUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .logout(logout -> logout + .logoutUrl("/auth/logout") + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID") + ) + .exceptionHandling(authenticationEntryPoint -> authenticationEntryPoint.authenticationEntryPoint(customAuthenticationEntryPoint)) // 인증 실패 시 커스텀 EntryPoint 사용 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) //세션정책설정 (인증이 필요할때만 생성) /* @@ -45,8 +55,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //f 사용자가 로그인하거나 인증할 때 기존 세션을 폐기하고 새로운 세션을 생성합니다. 이로 인해 기존 세션 ID가 무효화되며, 세션 고정 공격을 방지합니다. */ - .sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession)//세션고정공격방지 - .maximumSessions(1) // 동시세션수 제한 (하나의 사용자계정이 유지할수있는 세션의 수를 제한) + .sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::migrateSession)//작성하지않아도되지만 의도를알기위해 작성 ); return http.build(); diff --git a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java index 89fa2d7..1e941b1 100644 --- a/board/src/main/java/com/main/board/login/DTO/LoginRequest.java +++ b/board/src/main/java/com/main/board/login/DTO/LoginRequest.java @@ -7,9 +7,6 @@ @Getter @Setter public class LoginRequest { - @NotEmpty(message = "이메일을 입력해주세요.") private String email; - - @NotEmpty(message = "비밀번호를 입력해주세요.") private String rawPassword; } diff --git a/board/src/main/java/com/main/board/login/controller/LoginController.java b/board/src/main/java/com/main/board/login/controller/LoginController.java deleted file mode 100644 index cd12514..0000000 --- a/board/src/main/java/com/main/board/login/controller/LoginController.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.main.board.login.controller; - -import com.main.board.login.DTO.LoginRequest; -import com.main.board.login.service.LoginService; -import jakarta.servlet.http.HttpSession; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.SecurityContextHolder; -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 //JSON을 받기위한 어노테이션 -@RequestMapping("/auth") -public class LoginController { - - - private final LoginService loginService; - - public LoginController(LoginService loginService) { - this.loginService = loginService; - } - - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest) { - loginService.login(loginRequest); - return ResponseEntity.ok("Login successful"); - } - - @PostMapping("/logout") - public ResponseEntity logout(HttpSession session) { - session.invalidate(); //로그아웃시에 세션을 무효화하여 저장된 모든 데이터를 삭제한다 "SPRING_SECURITY_CONTEXT"해당 키 삭제 - SecurityContextHolder.clearContext(); //SecurityContextHolder의 인증정보를 삭제한다 - return ResponseEntity.ok().build(); // build()는 바디없이 빈 응답을 생성한다 - } -} diff --git a/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java b/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java deleted file mode 100644 index f2d083b..0000000 --- a/board/src/main/java/com/main/board/login/exception/LoginExceptionHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.main.board.login.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ProblemDetail; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -/* - @RestControllerAdvice 와 @ControllerAdvice 는 예외처리를 전역적으로 처리하기 위한 어노테이션 (spring 3.2부터 지원 Rest는 4.3부터 지원) - 두개의 차이는 Rest즉 @ResponseBody가 유무 차이로 응답이 Json으로 내려주는지 아닌지의 차이이다 - @ResponseBody는 메소드의 반환값을 Http 응답 바디에 직접 넣어주겠다는 의미이다 - */ -@RestControllerAdvice(basePackages = "com.main.board.login.controller") -public class LoginExceptionHandler { - - /* - @ExceptionHandler 어노테이션은 특정 예외가 발생했을 때 메소드가 처리하도록 하는 어노테이션이다 - 여기서는 BadCredentialsException 예외가 발생했을 때 handleBadCredentialsException 메소드가 처리하도록 한다 - ProblemDetail은 Spring(6부터)에서 제공하는 클래스로 예외처리시 상태코드와 상세정보를 담아서 반환할 수 있다 - 기존방식으로 처리하게되면 ResponseEntity> 형식으로 직접 관리를 해야하지만 ProblemDetail을 사용하면 편리하게 처리할 수 있다 - */ - - //로그인 실패 (비밀번호 불일치) 예외처리 - // UsernameNotFoundException은 필요없다 (BadCredentialsException로 spring security에서 처리) - @ExceptionHandler(BadCredentialsException.class) - public ProblemDetail handleBadCredentialsException(BadCredentialsException e) { - return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "아이디or비밀번호가 일치하지 않습니다"); - } - - //유효성 검증 실패 처리 - @ExceptionHandler(MethodArgumentNotValidException.class) - public ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - // 첫 번째 오류 메시지 추출 - FieldError fieldError = e.getBindingResult().getFieldErrors().get(0); // 첫 번째 오류만 처리 - String field = fieldError.getField(); - String defaultMessage = fieldError.getDefaultMessage(); - - // 이메일 오류 - if ("email".equals(field)) { - return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); - } - - // 비밀번호 오류 - if ("rawPassword".equals(field)) { - return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, defaultMessage); - } - return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "유효성 검증 실패"); - } - - -} diff --git a/board/src/main/java/com/main/board/login/service/LoginService.java b/board/src/main/java/com/main/board/login/service/LoginService.java deleted file mode 100644 index 4a5575a..0000000 --- a/board/src/main/java/com/main/board/login/service/LoginService.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.main.board.login.service; - -import com.main.board.login.DTO.LoginRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class LoginService { - - private final AuthenticationManager authenticationManager; - - public void login(LoginRequest loginRequest) { - /* - 인증 요청 - 토큰이라고 해서 토큰방식을 사용하는것이 아니고 - Spring Security에서 토큰 기반의 인증을 수행하는 객체이다 - Spring Secutiry 내부 에서 사용자 인증 정보를 담기위한 객체이다 - */ - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getRawPassword()); //토큰생성 - /* - 1. authenticationManager가 Provider에게 인증을 위임 - 2. config에 설정한 내용대로 UserDetailsService를 통해 유저정보를 가져온다 - 3. 입력된비밀번호와 가져온정보의 비밀번호를 비교하여 인증에 성공하면 Authentication 객체를 생성하여 리턴 - */ - Authentication authentication = authenticationManager.authenticate(token); // AuthenticationManager를 통해 인증을 시도한다 - // 세션에 인증 정보 저장 - /* - SecurityContextHolder는 SpringSecurity의 인증 정보를 저장하고 조회하는 컨텍스트 - 1. 인증이 성공하면 Authentication 객체를 SecurityContextHolder에 저장 - 2. 이후 클라인언트의 요청은 사용자 인증상태를 유지하게끔 한다 - 3. SPRING_SECURITY_CONTEXT는 SpringSecurity에서 사용하는 세션의 기본키이다 - */ - SecurityContextHolder.getContext().setAuthentication(authentication); - - } - -} diff --git a/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java b/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java deleted file mode 100644 index f6f2093..0000000 --- a/board/src/main/java/com/main/board/member/DTO/SignUpResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.main.board.member.DTO; - -import com.main.board.member.Member; -import lombok.Getter; - -import java.time.LocalDate; -import java.time.LocalDateTime; - -@Getter //Jackson 라이브러리는 객체를 JSON으로 변환할 때 기본적으로 getter 메서드를 사용 -public class SignUpResponse { - private final String email; - private final LocalDateTime createAt; - private final String message; - - public SignUpResponse(Member member) { - this.email = member.getEmail(); - this.createAt = LocalDateTime.now(); - this.message = "회원가입이 완료되었습니다."; - } -} diff --git a/board/src/main/java/com/main/board/member/controller/MemberController.java b/board/src/main/java/com/main/board/member/controller/MemberController.java index deb6ad6..b72dc67 100644 --- a/board/src/main/java/com/main/board/member/controller/MemberController.java +++ b/board/src/main/java/com/main/board/member/controller/MemberController.java @@ -1,13 +1,11 @@ package com.main.board.member.controller; import com.main.board.member.DTO.SignupRequest; -import com.main.board.member.DTO.SignUpResponse; import com.main.board.member.service.MemberService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,8 +20,9 @@ public class MemberController { @PostMapping("/signup") - public ResponseEntity signup(@Valid @RequestBody SignupRequest signupRequest) { - return ResponseEntity.ok(memberService.signUp(signupRequest)); + public ResponseEntity signup(@Valid @RequestBody SignupRequest signupRequest) { + memberService.signUp(signupRequest); + return ResponseEntity.ok().build(); } diff --git a/board/src/main/java/com/main/board/member/service/MemberService.java b/board/src/main/java/com/main/board/member/service/MemberService.java index e6265e4..f0b6702 100644 --- a/board/src/main/java/com/main/board/member/service/MemberService.java +++ b/board/src/main/java/com/main/board/member/service/MemberService.java @@ -1,11 +1,10 @@ package com.main.board.member.service; import com.main.board.member.DTO.SignupRequest; -import com.main.board.member.DTO.SignUpResponse; import com.main.board.member.Member; import com.main.board.member.exception.EmailDuplicatedException; import com.main.board.member.repository.MemberRepository; -import com.main.board.util.BcryptEncoder; +import com.main.board.util.PassWordEncoder; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,19 +14,17 @@ public class MemberService { private final MemberRepository memberRepository; - private final BcryptEncoder bcryptEncoder; + private final PassWordEncoder passWordEncoder; @Transactional - public SignUpResponse signUp(SignupRequest signupRequest) { + public void signUp(SignupRequest signupRequest) { if(memberRepository.existsByEmail(signupRequest.getEmail())) { throw new EmailDuplicatedException("이미 가입된 이메일입니다."); } - String encryptPwd = bcryptEncoder.encrypt(signupRequest.getRawPassword()); + String encryptPwd = passWordEncoder.encode(signupRequest.getRawPassword()); Member entity = signupRequest.toMemberEntity(encryptPwd); memberRepository.save(entity); - SignUpResponse response = new SignUpResponse(entity); - return response; } } diff --git a/board/src/main/java/com/main/board/util/BcryptEncoder.java b/board/src/main/java/com/main/board/util/BcryptEncoder.java deleted file mode 100644 index 53a318e..0000000 --- a/board/src/main/java/com/main/board/util/BcryptEncoder.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.main.board.util; - -import org.springframework.security.crypto.bcrypt.BCrypt; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - -//PasswordEncoder는 시큐리티에서 비밀번호 암호화를 적용하기위해 상속 -@Component -public final class BcryptEncoder { - - /* - 작업팩터 동적할당 (작업팩터는 Bcrypt의 해시작업을 얼마나 복잡하게 할지 결정하는 값이다. 4~31사이의 값이 가능하다) - 작업팩터가 높을수록 해시작업이 복잡해지고 그만큼 시간이 오래걸린다. (기본값은 10) - 작업팩터가 높을수록 보안이 높아지지만 그만큼 시간이 오래걸린다 - */ - private static final int WORK_FACTOR = 12; // 12로 설정 - - public String encrypt(String password) { - return BCrypt.hashpw(password,BCrypt.gensalt(WORK_FACTOR)); - } - - -} diff --git a/board/src/main/java/com/main/board/util/PassWordEncoder.java b/board/src/main/java/com/main/board/util/PassWordEncoder.java index 46ec80a..5dd8f1b 100644 --- a/board/src/main/java/com/main/board/util/PassWordEncoder.java +++ b/board/src/main/java/com/main/board/util/PassWordEncoder.java @@ -2,7 +2,9 @@ import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +@Component public class PassWordEncoder implements PasswordEncoder { private static final int WORK_FACTOR = 12;