[SNS클론코딩] 2. 로그인, 회원가입

지윤·2021년 8월 20일
0

Spring

목록 보기
7/7

로그인

로그인 페이지로 이동 signinForm()

@Slf4j
@Controller
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    //로그인 페이지로 이동
    @GetMapping("/auth/signin")
    public String signinForm() {
        return "auth/signin";
    }

    //회원가입 페이지로 이동
    @GetMapping("/auth/signup")
    public String signupForm() {
        return "auth/signup";
    }

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

        User user = signupDto.toEntity();
        authService.join(user);
        return "auth/signin"; //로그인 페이지로 이동
    }
}

로그인 요청(POST: "/auth/signin")이 오면 SecurityConfig 설정에 의해 스프링 시큐리티가 로그인을 대신 수행한다. 이는 아래의 스프링 시큐리티를 통해 알 수 있다.

스프링 시큐리티

아래는 스프링 시큐리티 설정파일 정보이다. SecurityConfig.class

@RequiredArgsConstructor
@EnableWebSecurity //SecurityConfig 이 파일로 시큐리티를 활성화 시킨다.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final OAuth2DetailsService oAuth2DetailsService;

    @Bean //SecurityConfig가 IOC될 때 같이 빈으로 등록된다.
    public BCryptPasswordEncoder encode() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //기존 super 삭제 -> 기존 시큐리티가 가지고 있는 기능이 모두 비활성화
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/", "/user/**", 
                "/image/**", "/subscribe/**", 
                "/comment/**", "/api/**")
                .authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/auth/signin") 
                .loginProcessingUrl("/auth/signin") 
                .defaultSuccessUrl("/")
        	.and()
        	.oauth2Login()
        	.userInfoEndpoint()
        	.userService(oAuth2DetailsService);
    }
}
  1. 스프링 시큐리티는 SecurityConfig에서 설정한 loginProcessingUrl 값으로 POST 요청이 들어오는지 계속 확인한다.

  2. 요청이 들어오면 요청을 가로채서 HTTP Body Data의 username 값을UserDetailsService을 상속 받은 PrincipalDetailsService의 loadUserByUsername()의 파라미터로 넘겨주며 호출한다.

    • 파라미터로 username만 넘겨주기 때문에 해당 유저가 존재하는지 DB에 확인하는 작업만 하면 된다.
    • password는 시큐리티가 내부적으로 확인 작업을 자동으로 진행한다.
    • 또한, 유저가 존재하면 유저 정보를 스프링 시큐리티 세션에 자동으로 담아준다.
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) 
    	throws UsernameNotFoundException {
	//사용자가 존재하는지 DB 확인
        User userEntity = userRepository.findByUsername(username);

        if (userEntity == null) {
            return null;
        } else {
            return new PrincipalDetails(userEntity);
        }

    }
}

이때 loadUserByUsername()의 리턴 타입은 UserDetails이다.
따라서 UserDetails를 구현한 클래스를 만들고 그 클래스에 유저 정보를 담는 클래스를 담아서 사용하면 된다.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private static final long serialVersionUID = 1L;

    private User user;
    private Map<String, Object> attributes;

    public PrincipalDetails(User user) {
        this.user = user;
    }

    public PrincipalDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    //@Override 메서드는 생략...

}

회원가입


@Controller
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    //회원가입 페이지로 이동
    @GetMapping("/auth/signup")
    public String signupForm() {
        return "auth/signup";
    }

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

        User user = signupDto.toEntity();
        authService.join(user);
        return "auth/signin"; //로그인 페이지로 이동
    }
}
@Service 
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Transactional
    public User join(User user) {
        
        String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword);
        user.setPassword(encPassword);
        user.setRole("ROLE_USER");
        User userEntity = userRepository.save(user);
        
        return userEntity;
    }
}

값 검증

회원가입을 할 때 입력되는 정보에 대해 클라이언트, 서버 모두 값에 대한 검증을 해야한다. 회원가입 이외에도 값 검증이 필요한 곳은 많다.

값을 검증하는 방법은 아래와 같다.
1. JSP에서 required 속성 값 또는 Javascirpt를 이용하는 방법(프론트)
2. @Valid와 BindingResult 이용(유효성 검사)
3. ResponseEntity 응답 코드 이용(유효성 검사)
4. 서버에서 값 관련 예외 발생 시 핸들러를 사용한 처리(서버)

컨트롤러에서 @Valid와 BindingResult를 사용하여 유효성 검사를 할 수 있다. 이부분에 대해 알아보자

  • @Valid가 붙은 파라미터 객체에서 값 검증 관련 오류가 발생하면 BindingResult 객체에 오류가 담긴다. 값 검증 대상 객체는 항상 BindingResult 객체 앞에 위치해야한다.
  • 오류가 있다면 CustomValidationException을 발생시킨다.@ControllerAdvice를 이용하여 ControllerExceptionHandler에서 모든 CustomValidationException를 처리하도록 하였다.
  • @Valid vs @Validation 차이는 참고
    @PostMapping("/auth/signup")
    public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) {

        //@Valid 걸린 객체에서 에러가 발생되면 bindingResult로 오류가 담긴다.
        //파라미터 설정 시 무조건 해당 객체를 BindingResult 앞에 기재
        //이 부분은 추후 AOP를 이용하여 처리하고 코드를 제거했다.
        if (bindingResult.hasErrors()) {
            Map<String, String> errorMap = new HashMap<>();

            for (FieldError error : bindingResult.getFieldErrors()) {
                errorMap.put(error.getField(), error.getDefaultMessage());
            }
            throw new CustomValidationException("유효성 검사 실패", errorMap);
        } else {
            User user = signupDto.toEntity();
            authService.join(user);
        }
        
        User user = signupDto.toEntity();
        authService.join(user);
        return "auth/signin"; //로그인 페이지로 이동
    }

값 검증 대상인 SignupDto 클래스를 살펴보자.

@Data
public class SignupDto {

    @Size(min = 1, max = 20, message = "20자 이하로 작성해야 합니다.")
    @NotBlank
    private String username;
    @NotBlank
    private String password;
    @NotBlank
    private String email;
    @NotBlank
    private String name;

}

javax.validation의 애노테이션을 이용하여 값을 검증한다. 애노테이션에 설정된 정보를 토대로 값이 맞지 않으면 오류를 발생시키고 해당 오류를 컨트롤러의 BindingResult 파라미터 객체에 담는다.

데이터 전처리, 후처리

DB에 연결 전과 후로 나눈다.

전처리는 validation(유효성검사) - username 길이 확인
후처리는 exceptionHandler - username 중복 확인

회원가입 로직을 핵심 기능이라고 한다면 회원가입을 위해 필요한 전처리, 후처리를 공통 기능이라고 한다. 핵심 로직을 위해 필요한 공통 기능을 AOP(관점 지향 프로그래밍)라고 한다.

공통 기능은 핵심 로직(회원가입 로직)에 넣는 것이 아니라 별도로 분리해서 관리한다. 이렇게 해야하는 이유와 함께 AOP를 사용하여 컨트롤러단에서의 예외 관련 처리 방법에 대해서는 이후 게시물에 정리할 예정이다.

profile
헬로🙋‍♀️

0개의 댓글