본문 바로가기
Spring

스프링부트 회원가입 구현하기 2

by 쌩욱 2021. 8. 12.

출처 : 스프링부트 SNS 프로젝트 - 포토그램 만들기 (최주호)

스프링부트 회원가입 구현하기 1 : https://ysu96.tistory.com/8

 

스프링부트 회원가입 구현하기

출처 : 스프링부트 SNS 프로젝트 - 포토그램 만들기 (최주호) 1. SecurityConfig 생성 public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity htt..

ysu96.tistory.com

 

4. 비밀번호 암호화(해시)

이전까지 구현한 회원가입 기능은 비밀번호를 암호화하지 않아 그대로 노출되는 문제가 있었습니다. 이를 암호화하기 위해 BCryptPasswordEncoder라는 클래스를 사용합니다. SecurityConfig 클래스에 @Bean 어노테이션을 사용해서 스프링 컨테이너가 해당 객체를 받을 수 있도록 합니다.

public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	//비밀번호 암호화
	@Bean  // 이 클래스가 IoC에 등록될 때 @Bean 어노테이션을 읽어서 이 함수를 리턴해 IoC가 들고있음 / 우리는 쓰기만 하면 됨
	public BCryptPasswordEncoder encode() {
		return new BCryptPasswordEncoder();
	}
    ...
}

 

 

그 후 서비스 클래스에서 BCryptPasswordEncoder 객체를 받아 회원가입 할 때 입력했던 비밀번호를 .encode() 함수로 암호화해 다시 비밀번호를 세팅하면 됩니다.

public class AuthService {
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bCryptPasswordEncoder; //비밀번호 암호화
	
	@Transactional //Write(Insert, Update, Delete) 할 때는 트랜잭션 처리
	public User 회원가입(User user) {
		//회원가입 진행
		String rawPassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawPassword); //패스워드 암호화 됨
		user.setPassword(encPassword);
		user.setRole("ROLE_USER"); // 권한 부여, 관리자 : ROLE_ADMIN
		
		User userEntity = userRepository.save(user); //데이터베이스에 저장하고 저장한 객체 반환
		return userEntity;
	}
}

 

 

결과는 성공적으로 암호화가 된 걸 볼 수 있습니다.

 


5. 유저 아이디 중복검사 / 길이 제한

회원가입 시 유저의 아이디가 이미 존재하는 아이디거나 길이가 초과하는 경우를 막아줘야 합니다.

 

아이디 길이 제한은 DB 앞단에서 전처리로 구현할 수 있으므로

@Valid 어노테이션을 사용해 먼저 SignupDto 부터 수정해 줍니다.

https://bamdule.tistory.com/35

 

[Spring Boot] @Valid 어노테이션으로 Parameter 검증하기

java.validation의 @Valid 어노테이션 사용법 정리 글입니다. Spring Boot 라이브러리에서 기본적으로 탑재된 기능이며 따로 dependency해 줄 필요가 없습니다. Spring Boot Version은 2.2.2.RELEASE 입니다. 1. j..

bamdule.tistory.com

 

 

우선 아이디 길이가 20을 초과하지 않도록 최대값을 20으로 설정하고 나머지 정보들에 꼭 값이 채워지도록 @NotBlank 어노테이션을 사용했습니다.

public class SignupDto {
	@Size(min=2, max=20)
	private String username;
	
	@NotBlank
	private String password;
	@NotBlank
	private String email;
	@NotBlank
	private String name;
    
    ...
 }

 

 

그리고 컨트롤러에서 해당 SignupDto를 받을 때 유효성 검사를 위해 @Valid 어노테이션을 붙이고 BindingResult라는 클래스도 추가로 사용합니다. 유효성 검사 시 오류를 발견하면 그 오류들을 모아 BindingResult 객체에 담아줍니다.

@PostMapping("/auth/signup")
public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) {

		//@Valid에서 오류가 발생하면 BindingResult에 오류를 다 모아줌 -> getFieldErrors 콜렉션에 다 모아줌
		if(bindingResult.hasErrors()) {
			Map<String, String> errorMap = new HashMap<>();
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(), error.getDefaultMessage());
			}
			throw new CustumValidationException("유효성 검사 실패함", errorMap);
			//유효성 검사 실패 -> BindingResult -> errorMap -> throw CustumValidationException -> ControllerExceptionHandler -> validationException함수 -> Script 리턴
		}
		else {
			//signupDto -> User로 만들기
			User user = signupDto.toEntity();
			//log.info(user.toString());
			
			User userEntity = authService.회원가입(user);
			
			return "auth/signin"; //회원가입 성공하면 로그인 페이지로
		}
}

 


6. 커스텀 예외처리 만들기

 

이제 예외가 발생하면 처리하기 위해 RuntimeException을 상속받아서 저만의 커스텀 예외를 만들겁니다.

RuntimeException을 상속받고 에러들을 담기 위한 Map을 만들어 줍니다.

public class CustumValidationException extends RuntimeException{
	//객체를 구분할 때 사용 , 사용자한텐 중요x 
	private static final long serialVersionUID = 1L;
	
	private Map<String, String> errorMap;
	
	public CustumValidationException(String message, Map<String, String> errorMap) {
		super(message);	
		this.errorMap = errorMap;
	}

	public Map<String, String> getErrorMap() {
		return errorMap;
	}
	
	
}

 

 

그리고 예외를 처리할 핸들러를 만들어줍니다.

@ControllerAdvice :exception이 발생 시 모든 exception을 낚아채 처리할 수 있게 해줍니다.

@ExceptionHandler : 해당 exception이 발생하면 처리합니다.

@RestController // 데이터 응답을 위해
@ControllerAdvice  //모든 exception을 낚아채 처리함
public class ControllerExceptionHandler {	
	@ExceptionHandler(CustumValidationException.class) //해당 exception이 발생하면 이 함수가 가로챔
	public  String validationException(CustumValidationException e) {
		return Script.back(e.getErrorMap().toString()); //스크립트 리턴
	}

}

 

 

이제 예외가 발생했을 때 사용자에게 보여줄 스크립트를 구현합니다.

해당 클래스의 back함수를 호출하면 메세지를 담은 경고창을 띄우고 다시 돌아가게 됩니다.

public class Script {
	public static String back(String msg) {
		StringBuffer sb = new StringBuffer();
		sb.append("<script>");
		sb.append("alert('"+msg+"');"); //경고창 띄우고
		sb.append("history.back();"); //뒤로 돌아가기
		sb.append("</script>");
		return sb.toString();
	}
}

 

 

아이디를 20자 넘게 입력하면 결과는 다음과 같습니다.

이렇게 회원가입 구현을 마치겠습니다.

 


7. 공통응답 DTO 만들기

 

지금까지 한 스크립트 방식의 응답은 클라이언트가 응답을 받을 때 효과적입니다.

개발자에게 응답을 해야하는 경우 공통응답 DTO를 사용하는게 효율적일 때가 있기 때문에 만들어줍니다.

자바의 제네릭을 사용해 여러 데이터 타입의 경우를 대비합니다.

//공통 응답 DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CMRespDto<T> { //generic 사용, 다른 데이터를 리턴해야 할 수도 있기 때문(다른 객체나 String..)
	private int code; // 1(성공) , -1(실패)
	private String message;
	private T data;	
}

 

 

스크립트 방식이 아닌 Dto를 리턴할 경우 커스텀 예외처리 핸들러를 다음과 같이 구현하면 됩니다.

리턴 타입인 CMRespDto<?>의 ? 는 어떤 타입이든 추론해서 변경해줍니다.

public class ControllerExceptionHandler {
	@ExceptionHandler(CustumValidationException.class) //해당 exception이 발생하면 이 함수가 가로챔
	public  CMRespDto<?> validationException(CustumValidationException e) {
		return new CMRespDto(-1, e.getMessage(), e.getErrorMap());
	}
	
	// CMRespDto, Script 비교
	// 1. 클라이언트에게 응답할 때는 Script가 좋음.
	// 2. Ajax통신 - CMRespDto (개발자가 js코드로 서버쪽으로 던져서 응답받는 것)
	// 3. Android 통신 - CMRespDto (응답을 안드로이드 앱에서 개발자가 해주는 것)
	// -> 응답 받는 쪽이 클라이언트면 Script, 개발자면 CMRespDto 가 좋음 
 }

 

 

아이디가 20자를 넘게 입력 후 회원가입을 하면 결과는 다음과 같습니다.

 


해당 프로젝트에서는 클라이언트 응답용 스크립트 방식을 사용합니다.