티스토리 뷰

https://digda.tistory.com/36

 

Spring Security 흐름

Spring Security Stream 스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다. 인증 - '누구인지 증명하는 과정' 인가(허가) - '권한이 있는지 확인하는 과정' 모든 HTTP..

digda.tistory.com

해당 예제의 github : https://github.com/digda5624/spring_security_study

 

GitHub - digda5624/spring_security_study: 서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발

서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발. Contribute to digda5624/spring_security_study development by creating an account on GitHub.

github.com

의 login package 를 참고하자.

 

포스팅기준으로는 JWT Login 을 작성했으나 예제에는 Session 로그인 또한 존재한다.


기존에 spring security 의 흐름을 살펴보았다.

spring security filter 의 요소들을 살펴보면 다음과 같은 구조이다.

 

그림에서도 알 수 있듯이 인증과 관련된 내용은 USernamePasswordAuthenticationFilter에서 진행한다고 생각하면 된다.

 

따라서 우리는 AuthenticationManager, SuccessHandler 와 FailerHandler 를 재정의를 하면 되겠다.

 

spring security 의 경우 특별한 것이 있는 것이 아니고 요청에 따라서 요청 내용을 검사 가공해준다. 서블릿 필터와 기능이 동일하다는 것이다.

 

Spring Security 를 사용하게 되면 default 로 Login Form 을 사용해서 인증을 하게 되는데 이를 해지하고 나만의 AuthenticationFilter 를 만들자.

 

따라서 인증 단계에서 핵심적으로 필요한 내용은 다음과 같다.

1. 자신이 정한 로그인 정책에 맞는 Authentication Filter 작성

2. Authentication Filter 가 인증을 위임할 Authentication Manager

3. 실질적인 인증을 맡아줄 Authentication Provider (Manager 가 다시 한번 위임한다.)

 

클래스 (Class) 역할
AuthenticationFilter (인증 필터 관련) 인증이 필요한 객체 생성 및 인증을 Authentication Manager 에게 위임
AuthenticationManager 실질적으로 인증을 담당하는 AuthenticationProvider 를 관리
AuthenticationProvider 실질적인 인증을 담당
Authentication 인증완료 된 객체, 혹은 인증 대상 객체
Principal (의미론적) 쉽게 말해 User (인증을 받게 될 주체)
credentials (의미론적) 쉽게 말해 비밀번호
UserDetailsService UserInformation 가공을 위한 Supporting class
UserDetails UserInformation 을 정의

 

기존에 프로젝트에서는 JWT 를 사용하기에 JWT방식의 로그인을 demo project에 적용했다.

JWT Authentication Filter 를 작성할 때 UsernamePasswordAuthenticationFilter 를 상속 받아 사용했고, spring security 에서 제공하는 다른 AuthenticationFilter 를 사용해도 된다.

 

결국 AutheticationFilter 들의 핵심은 attemptAuthentication() 이고 이를 @override 하자.

 

코드는 다음과 같다.

 

@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 인증을 위한 객체를 만든다.
        // 전송이 오면 AuthenticationFilter로 먼저 요청이 오게 되고, 아이디와 비밀번호를 기반으로 Token 을 발급해야한다.
        // 현재 principal 을 loginId 로 놓고 유효성을 검사하게 된다.
        log.info("JwtAuthenticationFilter {} {}", request.getMethod(), request.getRequestURL());
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            final User user = objectMapper.readValue(request.getInputStream(), User.class);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
            return getAuthenticationManager().authenticate(authRequest);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

JWT 필터에서 하는 역할은 요청 request 의 Body를 분석하여 인증을 해야할 객체를 만들고 이 객체를 앞서 말한 Manager 에게 인증을 위임하게 된다. 해당 코드에서 살펴보면 UsernamePasswordAuthenticationToken 이 인증 객체이고 이를 Manager 에게 넘기는 것을 확인할 수 있다.

 

핵심 부분은 getAuthenticationManager().authenticate() 부분이다. 또한 생성자에서 직접 set Manager 를 하게 되는데, 직접 설정하지 않으면 default 로 다른 매니저가 들어가게 된다.

 

결국 AuthenticationManager 의 경우에도 Provider 쪽에 인증을 위임하게 되지만 살펴볼 필요가 있다.

AuthentcationManager 는 interface 로 다양한 구현체가 있지만 사실상 ProviderManager 를 사용하게 된다.

결국 핵심은 ProviderManager 가 어떤식으로 AuthenticationProvider 에게 인증을 위임 받는지만 살피면 되겠다.

 

ProviderManager의 authenticate 을 살펴보면

 

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

	//...
	private List<AuthenticationProvider> providers = Collections.emptyList();

	public ProviderManager(AuthenticationProvider... providers) {
		this(Arrays.asList(providers), null);
	}
	//...
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		// ...
		for (AuthenticationProvider provider : getProviders()) {
			// AuthenticationProvider 의 정책과 맞는지 확인 하는 부분
			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;
				}
			}
			// ...
		}
	}
}

 

즉 ProviderManager 는 그대로 사용하되 AuthenticationProvider 를 사용자 정의 하여 ProviderManager 에게 제공을 하면 된다.

 

ProviderManager 의 경우 실질적인 인증을 담당하게 되므로 이곳에서 dataBase 접근을 통한 id, password 확인이 이루어 지게 된다.

@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {

	// repository 접근 및 인증완료 객체 생성을 위한 userDetailsService
    private final UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String userName = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        MyUserDetails userDetails = (MyUserDetails) userDetailsService.loadUserByUsername(userName);

        if(!userDetails.getPassword().equals(password)){
            throw new RuntimeException("password 틀립니다");
        }

        return new UsernamePasswordAuthenticationToken(userDetails.getUser(), null, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
    	// 넘어올 인증 객체에 대한 Class 정보 true 이어야지 해당 클래스의 인증을 작업한다.
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

인자로 넘어온 Authentication 객체 즉, 인증을 받아야하는 객체에서 id(principal), password(credentials) 을 넘겨 받은 후

비밀번호 아이디 확인 작업이 끝난 후 인증이 완료된 객체인 UserPasswordAuthenticationToken 을 발급하게 된다.

 

UserDetailsService 의 경우 정말 말그대로 User에 대한 정보를 로딩을 Support 해주는 클래스이다. 살펴보면

다음과 같음을 알 수 있다. 우리는 UserRepository 를 접근해서 User 에 대한 내용을 UserDetails 객체에 담은 후 return 하면 된다.

 

@RequiredArgsConstructor
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public MyUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Exception 을 Handling 할 수도 있지만 예제에서 생략
        User user = userRepository.findByName(username)
                .orElseThrow(() -> new RuntimeException("존재하지 않는 유저"));
        return new MyUserDetails(user);
    }

}

실제로도 UserRepository 에서 User 조회후 해당 user 정보를 통해 UserDetails 를 만드는 것을 살필 수 있다.

/**
 * userDetails 를 바탕으로 인증을 하게 되므로 MyUserDetails를 구현하여야 한다.
 */
@Getter
@RequiredArgsConstructor
public class MyUserDetails implements UserDetails {

    private final User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        GrantedAuthority grantedAuthority = () -> user.getRole().toString();
        authorities.add(grantedAuthority);
        return authorities;
    }

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

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

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

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

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

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

자 결국 이를 통해서 UserDetails 의 정보를 통해 인증을 완료하게 되면, 앞서 본 AuthenticationProvider 에서 인증이 완료된 객체인 UsernamePasswordAuthenticationToken을 발급하게 된다.

 

이 인증 정보를 통해서 Filter 에서는 Login Success Handler 를 호출 하게 되는데 이는 다음 포스팅에서 알아보자.

 

결국 Spring Security 의 인증 흐름은 다음과 같다.

1. Spring Security Filter Chain 에서 인증 필터(로그인 필터) 로의 접근

2. 인증 필터는 인증을 AuthenticationManager 를 통해 AuthenticationProvider 에게 위임하고 인증이 필요한 객체를 생성해 넘김

3. 넘어온 인증필요 객체를 UserDetailsService 를 통해 UserDetails 를 작성하고 Principal, Credentials 와 비교하여 올바른 유저인지 확인 한다. 올바른 유저가 맞다면 인증 완료 객체를 발급하고 다시 인증 필터에게 넘겨준다.

4. Exception 이 없었다면 필터는 LoginSuccessHandler를 호출하고 실패하게 될 경우 별도의 Exception Handler 를 호출하게 된다.

 

주의 할 점은 이 부분들은 서블릿 Filter 단에서 일어나는 작업이므로 Exception Handler 를 통한 Listening 은 불가하다.

 

실제로 이런 저런 제약사항으로 인해서 팀원 이모씨께서는 Login 은 Filter를 사용하지 않았다...

댓글
11-21 15:30
Total
Today
Yesterday
링크