sm 기술 블로그

[스프링 시큐리티] 회원가입 본문

스프링부트

[스프링 시큐리티] 회원가입

sm_hope 2022. 7. 23. 10:38

DB

CREATE TABLE site_user(
    id INT(20) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(30) NOT NULL UNIQUE,
    `password` VARCHAR(150) NOT NULL,
    email VARCHAR(50) NOT NULL UNIQUE
);

# 테스트 케이스
INSERT INTO site_user SET
username = "유저1",
`password` = "1234",
email = "test@test.com";

SELECT * FROM site_user;

password 길이를 적게하면 암호화 도중 에러가 발생한다.

SiteUser 엔티티

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class SiteUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    @Column(unique = true)
    private String email;
}

중복 허용을 방지하기 위해서 name과 email에 unique를 주었다.

User 리포지터리 작성

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<SiteUser, Long> {
}

SecurityConfig 작성

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.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").permitAll()
            .and()
                .csrf().ignoringAntMatchers("/h2-console/**")
            .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
            ;
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder 는 BCrypt 해싱 함수(BCrypt hashing function)를 사용해서 비밀번호를 암호화한다.
또한 Bruteforce attack(하나씩 비교) 이나 Rainbow table attack(변환 가능한 모든 해시 값을 저장 시켜 놓은 표) 과 같은 Password Cracking에 대한 저항력을 높이기 위해 의도적으로 느리게 설정되어 있다.

예를들어 암호화를 하면 아래와 같이 보여준다.

  • PasswordEncoder는 BCryptPasswordEncoder 에서 제공하는 인터페이스 이다.

PasswordEncoder는 다음과 같이 구성되어 있다.

public interface PasswordEncoder {
	
  // 비밀번호를 단방향 암호화
  String encode(CharSequence rawPassword);
	
  // 암호화되지 않은 비밀번호(raw-)와 암호화된 비밀번호(encoded-)가 일치하는지 비교
  boolean matches(CharSequence rawPassword, String encodedPassword);
	
  // 암호화된 비밀번호를 다시 암호화하고자 할 경우 true를 return하게 설정
  default boolean upgradeEncoding(String encodedPassword) { return false; };
}

User 서비스 작성

import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public SiteUser create(String username, String email, String password) {
        SiteUser user = new SiteUser();
        user.setUsername(username);
        user.setEmail(email);
        user.setPassword(passwordEncoder.encode(password));
        this.userRepository.save(user);
        return user;
    }
}

passwordEncoder는 @Bean으로 등록했기 때문에 UserService에서 다음과 같이 사용이 가능하다.

회원가입 폼

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserCreateForm {
    @Size(min = 3, max = 25)
    @NotEmpty(message = "사용자ID는 필수항목입니다.")
    private String username;

    @NotEmpty(message = "비밀번호는 필수항목입니다.")
    private String password1;

    @NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
    private String password2;

    @NotEmpty(message = "이메일은 필수항목입니다.")
    @Email
    private String email;
}

회원가입 폼에서 데이터를 받을 때 사용한다.

  • @Size(min = , max = , message = ) 최소,최대 크기를 제한함.
  • @NotEmpty(message = ) 비어 있다면 message를 bindingResult에 담아 출력함
  • @Email 이메일 형식이어야 한다.

회원가입 컨트롤러

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping("/signup")
    public String signup(UserCreateForm userCreateForm) {
        return "signup_form";
    }
    
    @PostMapping("/signup")
    public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
        //UserCreateForm 자체에서 유효성을 검사 했을 때 에러가 발생하면 이 조건에 들어온다.
            return "signup_form";
        }

        if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
        // 패스워드 1과 패스워드 2가 다르다면 이 조건에 들어온다.
            bindingResult.rejectValue("password2", "passwordInCorrect", 
                    "2개의 패스워드가 일치하지 않습니다.");
                    
        // 위의 문법은 rejectValue(@Nullable String field, String errorCode, String defaultMessage); 다음과 같다.
        // 참고로 에러 코드만 제대로 쓰면 되기 때문에 bindingResult.rejectValue("","passwordInCorrect","2개의 패스워드가 일치하지 않습니다."); 다음과 같이 바꿔 쓸 수 있다.
        
        
            return "signup_form";
        }

        try {
        // 정상적으로 회원이 생성 된다.
            userService.create(userCreateForm.getUsername(), 
                    userCreateForm.getEmail(), userCreateForm.getPassword1());
        }catch(DataIntegrityViolationException e) {
        // 중복이 발생할 경우 발생하는 에러
            e.printStackTrace();
            // 시스템 에러를 출력해준다.
            
            bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
            // bindingResult.rejectValue 와 비슷하다.(일치한다.)
            
            return "signup_form";
        }catch(Exception e) {
        // 그외 예상치 못한 에러들
            e.printStackTrace();
            // 시스템 에러를 출력해준다.
            
            bindingResult.reject("signupFailed", e.getMessage());
           // bindingResult.rejectValue 와 비슷하다.(일치한다.)
                        
            return "signup_form";
        }

        return "redirect:/";
    }
}

유효성 검사에서 할 수 있는 일은 많지 않다.(표 보러가기)
따라서 비밀번호가 맞지 않는 경우, 중복이 발생할 경우등은 직접 bindingResult를 해주어 사용자에게 알려주는 것이다.

Comments