전체적인 인증 흐름
Filter(username, password 토큰 생성) -> Manager(Provider 구분) -> Provider(실제 인증 처리부) ->
Service(username으로 DB에서 조회) -> Details(User정보 구성) ->
Provider(입력 token과 Details 비교 검증, 일치할 시 Authentication 리턴) ->
Filter(SpringContext에 Authentication 등록)
간단 요약
(입력된 username, password 토큰)을
(username으로 DB에서 조회한 유저정보)랑 비교 검증
일치하면 (Authentication 권한과 정보)를 SecurityContext에 등록
스택을 쌓아가며 인증처리를 한다.
AuthenticationFilter
인증의 처음과 마지막을 제어
처음
form 값으로 UsernamePassworAuthenticationToken을 만든다.
마지막
Security Session(Context)에 인터페이스 Authentication를 넣는다.
구현체 UsernamePassworAuthenticationToken는두개의 생성자를 갖고 있다. (인증 전, 인증 후)
전체 로직 처리는 AuthenticationManager에서 한다.
Provider 종류에 알맞게 인증 처리하는 곳 (Factory)
실제 인증은 AuthenticationProvider에서 처리
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
등록된 provider를 반복문으로 돌려 해당되는 provider를 찾는다.
Token(Username, password)를 DB정보(UserDetail)와 검증해서 authenticate를 통과하나 확인한다.
토큰과 userDetail이 일치하는 인증된 사용자이므로 break로 반복문 종료.
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
인증되면 Authenticaton 객체를 AuthenticationFilter에게 return
실질적인 인증 처리는 AuthenticationProvider에서 한다.
인증 전 Authentication 객체를 받아서 인증 후 객체로 만드는 곳
Provider는 여러개일 수 있다.
해당되는 Provider로 인증을 진행한다.
UserDetailService (Login 요청이 오면 loadUserByUserName 메서드 동작), 인터페이스이므로 구현
Username으로만 Repository를 이용해 DB에 조회한다.
유저가 있다면 UserDetails로 반환한다.
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> optionalUser = userRepository.findByUsername(username);
if(optionalUser.isPresent()){
return new PrincipalDetail(optionalUser.get());
}else{
return null;
}
}
}
UserDetails, 인터페이스이므로 구현해서 유저의 정보를 제공 (UserDetailService에서 생성된다.)
사용자의 정보 제공 목적으로 사용된다. (DB데이터, 로그인된 유저 Session)
유저명, 비밀번호, 유저 권한 등을 갖고 있다.
생성자에 User를 넣어준다.
public class PrincipalDetail implements UserDetails {
private User user;
public PrincipalDetail(User user){
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole().name();
}
});
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;
}
}
UserDetails 객체는 UsernamePasswordAuthenticationToken(Authentication)을 생성, 보관한다.
AuthenticationProvider에서
전달받은 UserDetails와 UsernamePassworAuthenticationToken(Authentication) 을 검증하여 인증처리
암호가 같다면 인증처리된 Authentication을 AuthenticationManager에게 반환
(이전에 password를 해시암호화 해야한다.)
AuthenticationManager에서 AuthenticationFilter에게 반환
Authentication를 SecurityContext에 저장
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean // 필터를 bean component 생성
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 원하는 필터를 골라서 커스텀 적용시킬 수 있다.
return http
// 권한 설정
.csrf().disable() // 해당 설정 안하면 csrf 토큰이 없어서, post 는 forbidden 이 나오므로 테스트에선 해제
.authorizeRequests()
.antMatchers("/user/**").hasRole("USER") // ROLE 이 필요한 URL 을 먼저 적용 시킨다.
.antMatchers("/manager/**").hasRole("MANAGER")
.antMatchers("/login").not().hasAnyRole("USER", "MANAGER") // 로그인 이후 접근 금지
.antMatchers("/**").permitAll()
// 로그인
.and()
.formLogin() // 해당 url 은 form 으로 이동
.loginPage("/login")
.loginProcessingUrl("/login") // 시큐리티가 로그인 대신 진행
.defaultSuccessUrl("/")
.failureUrl("/login")
// 로그아웃
.and()
.logout()
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.and().build();
}
}
'프로그래밍 > Security' 카테고리의 다른 글
웹 요청 정리 (0) | 2022.10.20 |
---|---|
[쿠키] Javascript로 쿠키에 직접 접근, HttpOnly, CORS (0) | 2022.10.19 |
[Web 보안] XSS, CSRF (0) | 2022.08.25 |
[인증과 인가] 쿠키와 세션, JWT (0) | 2022.06.23 |