
스프링 시큐리티를 사용해 간단한 폼 로그인을 구현해보자.
코드에 들어가기 앞서 시큐리티에서 폼 로그인을 진행하는 흐름을 알아보자.
1. /login 주소로 들어오는 요청을 시큐리티에서 낚아채서 처리한다.
2. UserDetailsService의 loadUserByUser 메서드가 실행되고 UserDetails 객체를 반환한다.
3. 반환된 UserDetails 객체는 Authentication에 주입된다.
4. 이 Authentication은 SecurityContext에 주입된다.
즉, SecurityContext는 Authentication을 저장하고 Authentication은 UserDetails를 저장하므로 유저 정보를 가지고 있다.
@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 설정을 해준다.
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의 정보를 조회할 수 있도록 한다.
/login 주소로 로그인 요청이 들어오면 시큐리티에서 낚아채 자동으로 UserDetailsService의 loadUserByUsername 메서드를 실행시킨다. principalDetailsService는 UserDetailsService의 구현체이다.
// 시큐리티 설정에서 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를 빈에 등록하고 회원가입 요청을 보낼 때 컨트롤러에서 비밀번호를 암호화 해보자.
@SpringBootApplication
public class Security1Application {
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(Security1Application.class, args);
}
}
@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에 저장하여 회원가입 완료
이후에 로그인을 시도하면 PrincipalDetailsService의 loadUserByUsername 메서드가 실행되며 로그인이 진행된다. (인증)
폼 로그인을 구현하며 한가지 의문이 생겼다.
시큐리티 내부에서 디코딩하여 비밀번호를 검증한다.