[security] Form Login 구현

yrok·2023년 12월 30일

spring security login

목록 보기
1/3
post-thumbnail

🤔 개요

스프링 시큐리티를 사용해 간단한 폼 로그인을 구현해보자.

전체 코드 : https://github.com/yryryr96/security

📄 흐름

코드에 들어가기 앞서 시큐리티에서 폼 로그인을 진행하는 흐름을 알아보자.
1. /login 주소로 들어오는 요청을 시큐리티에서 낚아채서 처리한다.
2. UserDetailsService의 loadUserByUser 메서드가 실행되고 UserDetails 객체를 반환한다.
3. 반환된 UserDetails 객체는 Authentication에 주입된다.
4. 이 Authentication은 SecurityContext에 주입된다.

즉, SecurityContext는 Authentication을 저장하고 Authentication은 UserDetails를 저장하므로 유저 정보를 가지고 있다.

💡 코드

SecurityConfig

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록된다.
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(CsrfConfigurer::disable);
        http.authorizeHttpRequests(authorize ->
                authorize
                        .requestMatchers("/user/**").authenticated()
                        .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
                        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
                        .anyRequest().permitAll()
        )
                .formLogin(
                        formLogin ->
                                formLogin
                                        .loginPage("/loginForm")
                                        .loginProcessingUrl("/login") // login 주소가 호출되면 시큐리티가 낚아채서 대신 로그인 진행
                                        .defaultSuccessUrl("/")
                );
        return http.build();
    }
}

폼 로그인을 사용하기위해 formLogin 설정을 해준다.

PrincipalDetails

UserDetails를 구현한 클래스이다. 결과적으로 로그인 요청이 들어오면 UserDetailsService에서 PrincipalDetails를 반환하고 Authentication 객체에 주입된다. 즉, PrincipalDetails는 유저 정보를 담고 있다.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private User user;

    // 일반 로그인
    public PrincipalDetails(User user) {
        this.user = user;
    }

    // 해당 User의 권한을 리턴하는 곳!!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        Collection<GrantedAuthority> collect = new ArrayList<>();

        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });

        return collect;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

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

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

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

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

principalDetails를 생성할 때 User 객체를 주입받아 User의 정보를 조회할 수 있도록 한다.

principalDetailsService

/login 주소로 로그인 요청이 들어오면 시큐리티에서 낚아채 자동으로 UserDetailsServiceloadUserByUsername 메서드를 실행시킨다. principalDetailsServiceUserDetailsService의 구현체이다.

// 시큐리티 설정에서 loginProcessingUrl("/login")
// /login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어있는 loadUserByUsername 함수가 실행
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    // 반환한 UserDetails가 Authentication 내부에 주입 -> Authentication이 시큐리티 session에 주입 (Security Context)
    // 해당 함수가 종료될 때 @AuthenticationPrincipal 어노테이션이 만들어진다.
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User findUser = userRepository.findByUsername(username);

        if (findUser != null) {
            return new PrincipalDetails(findUser);
        }

        return null;
    }
}

여기까지 시큐리티 폼 로그인을 적용시키기 위한 클래스는 모두 생성했고 폼 로그인을 진행하면 컨트롤러에 접근하여 비밀번호를 암호화 해야한다. 시큐리티에서 지원하는 기본 암호화 방식은 BCryptPasswordEncoder를 사용한다.
이를 간단하게 구현하기위해 BCryptPasswordEncoder를 빈에 등록하고 회원가입 요청을 보낼 때 컨트롤러에서 비밀번호를 암호화 해보자.

BCryptPasswordEncoder

@SpringBootApplication
public class Security1Application {
	@Bean
	public BCryptPasswordEncoder encodePwd() {
		return new BCryptPasswordEncoder();
	}

	public static void main(String[] args) {
		SpringApplication.run(Security1Application.class, args);
	}

}

Controller

@PostMapping("/join")
public String join(User user) {

    user.setRole("ROLE_USER");
    String rawPassword = user.getPassword();
    String encPassword = bCryptPasswordEncoder.encode(rawPassword);
    user.setPassword(encPassword);
    userRepository.save(user);
    return "redirect:/loginForm";
}
  • rawPassword : 사용자가 입력한 비밀번호
  • encPassword : BCryptPasswordEncoder를 사용해 암호화한 비밀번호

비밀번호를 암호화하고 DB에 저장하여 회원가입 완료

이후에 로그인을 시도하면 PrincipalDetailsServiceloadUserByUsername 메서드가 실행되며 로그인이 진행된다. (인증)

🤔 의문

폼 로그인을 구현하며 한가지 의문이 생겼다.

  1. 비밀번호를 abcd로 설정하고 회원가입을 진행하면 DB에는 암호화가 된 비밀번호가 저장되기에 DB의 비밀번호는 abcd가 아니다. 하지만, 로그인 요청을 진행할 때 abcd로 해도 로그인이 된다. 우리는 비밀번호 디코딩 로직을 작성하지 않았는데 어떻게 되는걸까?

    시큐리티 내부에서 디코딩하여 비밀번호를 검증한다.

📌 중요

  • /login 주소로 요청이 들어오면 시큐리티에서 낚아채 로그인을 진행한다.
  • SecurityContextHolder(시큐리티 세션)에는 SecurityContext가 있고 SecurityContext에는 Authentication 객체만 저장할 수 있다. 또한, Authentication 객체는 UserDetails를 들고있다. (유저 정보)
profile
공부 일기장

0개의 댓글