.formLogin() 을 사용하면 간단하게 로그인 여부만 판단하는 기능은 쉽게 만들 수 있습니다. 하지만 현재 상태에선 누가 로그인 했는지, 회원가입등은 어떻게 진행하는지, 아직 정해진게 없습니다. 저희 서비스에 필요한데로 사용자 정보를 구성하고, 돌려주기 위해선 좀 더 작업이 필요합니다.
WebSecurityConfigurerAdapter (다시)
이 역시, 좀전에 만들었던 WebSecurityConfigurerAdapter 를 사용합니다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
...
}
Java
복사
Authentication을 구성하기 위한 configure() 함수입니다. 전달받은 auth 객체를 이용해 어떤 방식으로 Authentication을 진행할지를 결정할 수 있습니다. 먼저 테스트를 위해
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1")
.password(passwordEncoder().encode("user1pass"))
.roles("USER");
}
...
}
Java
복사
와 같이 작성해 봅시다. auth.inMemoryAuthentication() 함수를 사용하면, Spring Boot 내부에 Map 의 형태로 만들어진 단순한 유저 관리 객체를 사용하게 됩니다. 해당 설정을 적용하고 다시 실행하면, 구성하였던 HttpSecurity 의 로그인 페이지에서 user1 사용자로 로그인할 수 있게됩니다.
이때 사용하게 되는 객체는 UserDetailsService 의 구현체입니다. 이 UserDetailsService 인터페이스를 구현하여, auth 객체의 .userDetailsService() 를 통해 전달하면, 원하는 방식으로 유저를 관리할 수 있습니다.
UserDetailsService
우선 UserDetailsService 인터페이스를 살펴보면,
package org.springframework.security.core.userdetails;
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
Java
복사
loadUserByUsername 함수 하나만 구성되어 있습니다. 이 함수는 위의 AuthenticationManagerBuilder 에 UserDetailsService 를 전달할때, 사용자가 로그인 요청을 보내게 되면, 아이디를 전달하고 해당 아이디와 연관된 정보를 UserDetails 라는 인터페이스의 형태로 반환하도록 요구하게 됩니다. 여기에 UserEntity 의 CRUD를 담당하는 UserRepository 를 이용해 데이터를 데이터베이스에 저장할 수 있습니다.
...
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
if(!userRepository.existsByUsername(username)){
logger.warn(username);
throw new UsernameNotFoundException(String.format("username %s not found", username));
}
// TODO
}
...
Java
복사
다만 함수의 반환형은 UserDetails 로 고정되어 있습니다. 따라서 UserEntity 에서 받아온 데이터를 UserDetails 인터페이스에 맞는 형태로 조정해야 합니다.
UserDetails
UserDetails 는 Spring Security 내부에서 사용자 정보를 담기 위해서 정의된 인터페이스 입니다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
Java
복사
완전한 Custom을 위해선 해당 인터페이스에 정의된 함수들을, getter 의 형태로 제작하면 됩니다.
•
getAuthorities() : 사용자의 권한을 Collection 의 형태로 반환합니다.
•
isAccountNonExpired() : 사용자 계정이 현제 만료된지 여부를 반환합니다. 만료 되지 않으면 true 입니다. 만료된 계정은 인증에 사용될 수 없습니다.
•
isAccountNonLocked() : 사용자 계정의 잠금 여부를 반환합니다. 잠금된 계정은 인증에 사용될 수 없습니다. 장기 미사용으로 인한 잠금 여부에 사용할 수 있습니다.
•
isCredentialsNonExpired() : 사용자의 비밀번호의 만료 여부를 반환합니다. 만료된 비밀번호는 사용할 수 없으며, 인증에 사용할 수 없습니다.
•
isEnabled() : 사용자 계정의 사용 여부를 반환합니다.
이중 isAccountNonExpired() 와 isEnabled() 는 유사하게 생각할 수 있으나, isAccountNonExpired() 는 일반적으로 계정의 생성시에 유효기한을 생성하여 자동으로 만료되게 하는 용도로, isEnabled() 는 수동으로 계정의 사용을 멈추는 용도로 볼 수 있습니다. 또한, 여기서 반환하는 모든 boolean 변수가 true 이어야 계정이 사용가능합니다.
UserDetails 를 UserEntity 기반으로 직접 구현하거나, 이미 구현된 User 클래스를 활용하는 방법이 있습니다. User 클래스의 경우, 이미 완성된 UserDetails 인터페이스의, 계정 사용 여부에 관한 boolean 변수들을 다 true 로 반환하도록 간단히 생성하도록 만들거나, 상세한 내용을 설정하여 반환하게 할 수 있습니다.
public class User implements UserDetails, CredentialsContainer {
...
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
...
}
Java
복사
서비스 중에 사용자 정보 받아오기
로그인한 사용자의 정보는 어플리케이션의 비즈니스 로직중 어디에서든 활용할 수 있습니다. 먼저 Controller에서 받을 수 있는 방식 두가지를 살펴보겠습니다.
@GetMapping("/principal")
@ResponseBody
public String withPrincipal(Principal principal) {
return principal.getName();
}
@GetMapping("/authentication")
@ResponseBody
public String withAuthentication(Authentication authentication) {
return authentication.getName();
}
Java
복사
Spring IoC 컨테이너는 사용자의 요청에 정의된 인증 정보를 RequestMapping 에 자동으로 할당해 줍니다. 사용할 인자를 정의하면, 위에 보이는 바와 같이 가져와서 사용할 수 있습니다.
컨트롤러에서 비즈니스 로직에 전부 전달하는 것이 불편하게 느껴지면, SecurityContextHolder 를 사용할 수 있습니다.
@GetMapping("/context-holder")
@ResponseBody
public String withContextHolder(){
return SecurityContextHolder.getContext().getAuthentication().getName();
}
Java
복사
SecurityContextHolder 는 현재 요청을 처리중인 쓰레드에 할당된 인증 관련 정보를 SecurityContext 의 형태로 담고 있습니다. 여기서 getAuthentication() 함수를 사용하면, 상기의 예시의 Authentication 객체를 받아와서 사용할 수 있습니다. 이를 좀더 편리하게 사용하기 위해, 따로 @Component 로 구현하는것도 한가지 방법입니다.
@Component
public class AuthenticationFacade {
public Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
Java
복사
위와 유사하게 Bean을 만든다면, Spring Application의 어디에서든 손쉽게 SecurityContext 를 받아와 사용할 수 있습니다.