Search
📒

8-3. Login 커스텀

.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 함수 하나만 구성되어 있습니다. 이 함수는 위의 AuthenticationManagerBuilderUserDetailsService 를 전달할때, 사용자가 로그인 요청을 보내게 되면, 아이디를 전달하고 해당 아이디와 연관된 정보를 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 이어야 계정이 사용가능합니다.
UserDetailsUserEntity 기반으로 직접 구현하거나, 이미 구현된 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 를 받아와 사용할 수 있습니다.