[Spring] 안전한 회원가입 구현하기.

박제현·2023년 10월 19일
post-thumbnail

1. 엔티티 선언

@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String pwd;
    private String name;
    private String phone;
    private int gender;
    private String birth;
}

테이블로 생성하고자 하는 컬럼들을 선언한다.

2. 레포지토리 선언.

public interface UserRepository extends JpaRepository<User, Long> {
	// 이메일로 User 찾기.
    Optional<User> findByEmail(String email);
}

인터페이스 형태로 UserRepository 를 선언한다.

3. 서비스 등록.

@RequiredArgsConstructor
@Service
public class UserService {

	/** jwt 부분
    @Value("${jwt.secret}")
    private String secretKey;
    private Long expiredMs = 1000 * 60 * 60L;

    public String login(String userName, String password) {
        return JwtUtil.createJwt(userName, secretKey, expiredMs);
    } **/
    
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    public void join(UserJoinRequestDto requestDto) {
        // email 중복 체크
        userRepository.findByEmail(
                        requestDto.getEmail())
                .ifPresent(user -> {
                    throw new AppException(ErrorCode.USERNAME_DUPLICATED, requestDto.getEmail() + "는 이미 존재합니다.");
                });

        // 저장
        userRepository.save(
                User.builder()
                        .email(requestDto.getEmail())
                        .pwd(encoder.encode(requestDto.getPwd()))
                        .phone(requestDto.getPhone())
                        .name(requestDto.getName())
                        .birth(requestDto.getBirth())
                        .gender(requestDto.getGender())
                        .build());
    }
}

회원가입을 위한 서비스를 생성한다.
userRepository의 save 메소드로 저장한다.
필요한 예외처리를 해주고, User 도메인의 빌더를 통해 새로운 User 객체를 생성해서 저장한다.

암호화를 위한 BCrypPasswordEncoder 를 사용한다.

4. 컨트롤러 생성.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserApiController {

    private final UserService userService;

    @PostMapping("/join")
    public ResponseEntity<String> join(@RequestBody UserJoinRequestDto requestDto) {
        userService.join(requestDto);
        return ResponseEntity.ok("회원가입이 완료되었습니다.");
    }

}

POST 방식으로 UserJoinRequestDto 를 RequestBody 로 받아준다.
등록된 userService의 join 메소드를 통해 새로운 회원을 등록한다.

이렇게 하면 회원가입은 아주 쉽게 구현할 수 있다.

5. 패스워드 암호화.

@Configuration
public class EncoderConfig {

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
}

패스워드 암호화에 필요한 BCryptPasswordEncoder 를 Bean으로 등록한다.

6. Spring Security 설정.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final UserService userService;
	
    /** jwt 에 필요한 부분 
    @Value("${jwt.secret}")
    private String secretKey;
    **/

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        return httpSecurity
                .httpBasic(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(requests -> {
                    requests.requestMatchers("/api/users/login", "/api/users/join").permitAll();
                    requests.requestMatchers(HttpMethod.POST, "/api/**").authenticated();
                })
                .sessionManagement(
                        sessionManagement ->
                                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
                .build();
    }

}

암호화를 위해 Spring Security를 implements 했기 때문에, 이에 필요한 설정들을 위와 같이 작성한다.

7. 커스텀 예외처리 등록.

@RestControllerAdvice
public class ExceptionManager {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> runtimeExceptionHandler(RuntimeException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
    }

    @ExceptionHandler(AppException.class)
    public ResponseEntity<?> appExceptionHandler(AppException e) {
        return ResponseEntity
                .status(e.getErrorCode().getHttpStatus())
                .body(e.getErrorCode().name() + " " + e.getMessage());
    }
}

ExceptionMager를 통해서 서비스 동작 중 에러가 발생하는 부분을 처리해준다.

@AllArgsConstructor
@Getter
public class AppException extends RuntimeException{
    private ErrorCode errorCode;
    private String message;
}

AppException 이라는 새로운 RuntimeException을 생성한다.

@AllArgsConstructor
@Getter
public enum ErrorCode {
    USERNAME_DUPLICATED(HttpStatus.CONFLICT, "");

    private HttpStatus httpStatus;
    private String message;
}

ErrorCode 라는 Enum 을 생성하여 관리한다.

8. 결과


암호화된 패스워드가 DB 에 저장된 모습.

profile
닷넷 새싹

0개의 댓글