gi_dor

회원가입 - SpringSecurity , @Valid , MySQL , MyBatis 본문

Back_End/SpringBoot

회원가입 - SpringSecurity , @Valid , MySQL , MyBatis

기돌 2024. 4. 12. 17:51
728x90


SpringSecurity - SecurityConfig

@Configuration // 빈 설정
@EnableWebSecurity // 스프링 시큐리티 웹보안 활성화
@EnableMethodSecurity(prePostEnabled = true , securedEnabled = true)
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain (HttpSecurity http) throws Exception{
        // csrf 공격을 방지하는 기술 비활성화
        http.csrf(csrf -> csrf.disable());
        http.authorizeHttpRequests(authorizeHttpRequest -> authorizeHttpRequest.requestMatchers("/**").permitAll());


        // 폼기반 로그인을 활성화
        // 사용자가 로그인 한다면 /user/login 경로로 이동하게되는데 로그인 성공시 ("/") 로 리다이렉트
        http.formLogin(formLogin -> formLogin.loginPage("/user/login").defaultSuccessUrl("/"));


        // 로그아웃
        // HTTP 세션을 무효화해서 로그인 상태를 제거한다
        http.logout(logout -> logout.logoutUrl("/user/logout").logoutSuccessUrl("/").invalidateHttpSession(true));
        return http.build();
    }


    // 비밀번호 암호화
    // salt 사용
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

http.formLogin(formLogin -> formLogin.loginPage("/user/login").defaultSuccessUrl("/"))

로그인 페이지를 "/user/login"으로 지정하며, 로그인 성공 시 기본적으로 홈 페이지("/")로 redirect .

http.logout(logout -> logout.logoutUrl("/user/logout").logoutSuccessUrl("/").invalidateHttpSession(true))

로그아웃을 활성화하고, "/user/logout" 경로로 로그아웃을 수행하며, 로그아웃 후에는 HTTP 세션을 무효화하여 사용자의 로그인 상태를 제거합니다.

@Bean PasswordEncoder passwordEncoder()

PasswordEncoder 인터페이스의 구현체를 빈으로 등록하는 메서드
BCryptPasswordEncoder는 강력한 해시 알고리즘을 사용하여 비밀번호를 안전하게 저장하고 인증하는 데 사용됩니다.

 


@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private Long no;
    private String id;
    private String password;
    private String name;
    private String email;
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;
    private String tel;
    private String zipCode;
    private String address;
    private String addressDetail;
    private String delYn;
}

 

DB의 User 테이블과 매핑하는 객체
사용자의 데이터를 담는 용도로 사용할 예정입니다
주로 사용자 정보를 가져오고 저장하는데 사용

 


 

@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserSignupForm {
// 회원 가입 양식을 처리한다 , 사용자 등록할 때 쓰인다

    @NotBlank(message = "ID는 필수 입력값 입니다")
    @Pattern(regexp = "^[a-z0-9]{5,20}$", message = "아이디는 영어 소문자와 숫자만 사용하여 5~20자리여야 합니다.")
    private String id;

    @NotBlank(message= "비밀번호는 필수 입력 값 입니디")
    @Size(min = 8 , message = "비밀번호는 8글자 이상 입니다")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\d~!@#$%^&*()+|=]{8,16}$", message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
    private String password;

    // @Pattern(regexp = "$expression") 정규식검증
    @NotBlank(message = "이름은 필수 입력 값입니다")
    @Pattern(regexp="^[가-힣]{2,}$", message = "이름은 한글 2글자 이상")
    private String name;

    @NotBlank(message="이메일은 필수 입력 값입니다")
    @Email(message = "유효한 이메일 형식이 아닙니다")
    private String email;

    @NotBlank(message = "전화번호는 필수 입력 값입니다")
    @Pattern(regexp="^\\d{2,3}-\\d{3,4}-\\d{4}$" , message = "유효한 전화번호 형식이 아닙니다")
    private String tel;

    private String zipCode;
    private String address;
    private String addressDetail;


    public User toEntity(PasswordEncoder passwordEncoder) {
        User user = new User();

        user.setId(id);
        user.setPassword(passwordEncoder.encode(password));
        user.setName(name);
        user.setEmail(email);
        user.setTel(tel);
        user.setZipCode(zipCode);
        user.setAddress(address);
        user.setAddressDetail(addressDetail);

        return user;
    }
}

사용자의 회원 가입할 때 입력하는 폼을 처리하기 위해 만든 클래스 입니다

사용자가 브라우저에서 회원가입시에 입력하는 정보들을 담고 있으며
사용자로부터 입력받은 데이터들을 유효성 검사하고 , 이것으로 객체를 생성해 DB에 저장하는데 사용됩니다

 

 

@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserSignupForm {
// 회원 가입 양식을 처리한다 , 사용자 등록할 때 쓰인다


    @Pattern(regexp = "^[a-z0-9]{5,20}$" , message ="알맞은 형식이 아닙니다")    // message = "아이디는 영어 소문자와 숫자만 사용하여 5~20자리여야 합니다."
    private String id;

    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\d~!@#$%^&*()+|=]{8,16}$"  , message ="알맞은 형식이 아닙니다")
    private String password;

    // @Pattern(regexp = "$expression") 정규식검증
    @Pattern(regexp="^[가-힣]{2,}$" ,  message ="알맞은 형식이 아닙니다")
    private String name;


    @Email(message = "유효한 이메일 형식이 아닙니다")
    private String email;


   // @Pattern(regexp="^\\d{2,3}-\\d{3,4}-\\d{4}$" , message = "유효한 전화번호 형식이 아닙니다")
    private  String tel;

    private String zipCode;
    private String address;
    private String addressDetail;


    public User toEntity(PasswordEncoder passwordEncoder) {
        User user = new User();

        user.setId(id);
        user.setPassword(passwordEncoder.encode(password));
        user.setName(name);
        user.setEmail(email);
        user.setTel(tel);
        user.setZipCode(zipCode);
        user.setAddress(address);
        user.setAddressDetail(addressDetail);

        return user;
    }
}

 

유효성 검증 메세지가 너무 지저분하게 나와서 변경했습니다

 


 

@Setter
public class UserDetailsImpl implements UserDetails {
// 사용자의 인증 정보를 내타내는데 사용되며  사용자가 제공한 자격이 클래스의 정보와 일치하는지 확인한다

    private String id;
    private String password;
    private  Collection<? extends GrantedAuthority> authorities;

    // 권한 반환
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return id;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 


 

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

    private final UserMapper userMapper;
    private final PasswordEncoder passwordEncoder;


    /*
    사용자의 id 를 기반으로 사용자의 정보를 로드한다
    해당 사용자의 정보를 UserDetails 객체로 반환하는데 , 실제 사용자의 정보를 포함하며
    Spring Security 는 사용자의 인증 , 권한 부여를 처리한다
     */
    /*
    사용자 이름을 기반으로 사용자를 찾는다.
    실제 구현에서는 검색이 대소문자를 구분하거나 구분하지 않을 수 있으며, 이는 구현 인스턴스가 구성된 방식에 따라 다를 수 있다.
    사용자 이름을 식별하는 사용자의 데이터가 필요하다
    사용자 레코드가 완전히 채워진 사용자 레코드를 반환. 반환된 객체는 null이 아니다
    사용자를 찾을 수 없거나 사용자에게 부여된 권한이 없는 경우 UsernameNotFoundException을 발생
     */

    /**
     * 주어진 사용자 아이디를 기준으로 사용자의 데이터를 가져와 UserDetails 객체로 반환합니다.
     * @param id 사용자 아이디
     * @return UserDetails 객체
     * @throws UsernameNotFoundException 주어진 아이디에 해당하는 사용자를 찾을 수 없는 경우 발생합니다.
     */
    @Override
    public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {

        // 사용자 아이디를 기준으로 데이터베이스에서 사용자 정보를 가져옵니다. 이 정보는 user 객체에 저장
        User user = userMapper.selectUserById(id);

        // 데이터베이스에서 가져온 사용자 정보가 없다면(null이면) 예외를 발생시킵니다.
        if(user == null) {
            throw new UsernameNotFoundException("Id 찾을수 없습니다 : " +id);
        }

        // UserDetailsImpl 클래스의 객체를 생성합니다. 이 객체는 사용자의 인증 및 권한 정보를 제공하기위해 사용한다
        UserDetailsImpl userDetails = new UserDetailsImpl();

        // 객체에서 가져온 사용자 아이디와 비밀번호를 userDetails 객체에 설정
        userDetails.setId(user.getId());
        userDetails.setPassword(user.getPassword());

        userDetails.setAuthorities(List.of(new SimpleGrantedAuthority("ROLE_USER")));

        return userDetails;
    }


    /**
     * 주어진 회원가입 폼 UserSignupForm 으로 사용자를 등록한다
     * @param form 사용자 회원가입 폼 UserSignupForm
     * @return 등록된 사용자
     * @throws RuntimeException 이미 존재하는 아이디나 이메일일 경우 발생
     */
    public User registerUser(UserSignupForm form) {

        if (userMapper.selectUserById(form.getId()) != null) {
            throw new RuntimeException("이미 존재하는 아이디입니다: " + form.getId());
        }

        if (userMapper.selectUserByEmail(form.getEmail()) != null) {
            throw new RuntimeException("이미 존재하는 이메일 입니다: " + form.getEmail());
        }

        User user = form.toEntity(passwordEncoder);
        userMapper.insertUser(user);

        return user;
    }

    /**
     * 주어진 번호에 해당하는 사용자를 데이터베이스에서 선택
     * @param no 사용자 번호
     * @return 선택된 사용자
     * @throws RuntimeException 주어진 번호에 해당하는 사용자를 찾을 수 없는 경우 발생
     */
    public User selectUserByNo(Long no) {
        User user = userMapper.selectUserByNo(no);

        if (user == null) {
            throw new RuntimeException("해당 번호에 해당하는 사용자를 찾을 수 없습니다: " + no);
        }
        return user;
    }

    /**
     * 주어진 아이디에 해당하는 사용자를 데이터베이스에서 선택
     * @param id 사용자 아이디
     * @return 선택된 사용자
     * @throws RuntimeException 주어진 아이디에 해당하는 사용자를 찾을 수 없는 경우 발생
     */
    public User selectUserById(String id) {
        System.out.println(id);
        User user = userMapper.selectUserById(id);
        if (user == null) {
            throw new RuntimeException("해당 아이디에 해당하는 사용자를 찾을 수 없습니다: " + id);
        }
        return user;
    }

    /**
     * 주어진 이메일에 해당하는 사용자를 데이터베이스에서 선택
     * @param email 사용자 이메일
     * @return 선택된 사용자
     * @throws RuntimeException 주어진 이메일에 해당하는 사용자를 찾을 수 없는 경우 발생
     */
    public User selectUserByEmail(String email) {
        User user = userMapper.selectUserByEmail(email);
        if (user == null) {
            throw new RuntimeException("해당 이메일에 해당하는 사용자를 찾을 수 없습니다: " + email);
        }
        return user;
    }


    /**
     * id 중복 체크
     * @param id DB에 저장되어있는  아이디
     * @return 0  또는 1
     */
    public int idCheck(String id) {
        int cnt = userMapper.idCheck(id);
        System.out.println("IdCnt : "+cnt);
        return cnt;
    }


    /**
     * email 중복 체크
     * @param email DB에 저장되어있는  이메일
     * @return 0 또는 1
     */
    public int emailCheck(String email) {
        int cnt = userMapper.emailCheck(email);
        System.out.println("EmailCnt : " + cnt);
        return cnt;
    }

}

 


 

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


    private final UserService userService;

   @GetMapping("/register")
    public String registerForm(Model model) {
        model.addAttribute("userSignupForm", new UserSignupForm());
        return "user/registerForm";
    }


    @PostMapping("/join")
    public String userJoin(@ModelAttribute("userSignupForm")
                           @Valid UserSignupForm form ,
                           BindingResult errors ) {
        // BindingResult객체에 오류가 있다면 , 유효성 체크를 통과하지 못한 것임으로 ,
        // 회원가입 폼으로 내부 이동 시킨다.
        if (errors.hasErrors()) {
            return "user/registerForm";
        }

        try {
            User user = userService.registerUser(form);
            return "redirect:/user/completed?id=" + user.getId();

        } catch (RuntimeException ex) {
            String message = ex.getMessage();

            if("id".equals(message)) {
                errors.rejectValue("id", null,"사용 할 수 없는 아이디");
            } else if( "email".equals(message)) {
                errors.rejectValue("email", null, "사용 할 수 없는 이메일");
            }

            return "user/registerForm";
        }


    @GetMapping("/completed")
    public String completed(String id , Model model) {
        User user = userService.selectUserById(id);
        model.addAttribute("user",user);

        return "user/completed";
    }

    }

 

 

https://dev-coco.tistory.com/123

 

[Spring Boot] Validation 적용, @Valid로 유효성 검사하기

이전에는 spring-boot-starter-web 의존성 내부에 validation이 있었지만, spring boot 2.3 version 이상부터는 아예 모듈로 빠져 validation 의존성을 따로 추가해줘야 사용할 수 있다. 1. validation 의존성 추가 impleme

dev-coco.tistory.com

 

728x90