본문 바로가기

Spring

스프링-시큐리티 UsernamePasswordAuthenticationFilter

스프링에서 보안,인증,인가 등을 지원해주는 스프링 하위의 프레임워크이다.

일단 프레임워크이기때문에 어느정도 다 틀이 잡혀있어서 그에 맞게 설정 및 개발을 해줘야한다.

(자주 잊어먹어서 정리를 하는글이지.. 기초부터 내용은 없습니다.. )

 

기본적으로 서블릿필터가 서블릿 컨테이너에서 작동을 하고, 스프링 시큐리티는 스프링컨테이너에 있다.

그러면 어떻게 먼저 요청을 오는데 필터단에서 처리가 가능을 한 것일까..?

 

바로 서블릿 필터에서 스프링에게 요청을 위임을 해준다.

 

요청이 들어오면 먼저 서블릿컨텍스트의 필터단에서 설정된 필터가 차례대로 실행이 된다.

그때 DelegatingFilterProxy 클래스(요청을 위임하는거 빼고는 보안,인증,인가의 행위는 하지않는다.)를 이용을 해서 스프링 컨텍스트에 SPRINGSECURITYFILER라는 빈이름으로  등록된 FilterProxyChain에게 위임을 해준다.

 

필터체인이라고 하는데 필터가 체인처럼 서로 연결이 되어서 기본적으로 계속 요청마다 실행되는 필터가 있고,

반대로 요청의 경우에 따라 실행되는 필터가 있다. 이것은 나중에 뒤에 정리를 하겠습니다..

 

시큐리티 필터의 등록 방법은 mavan에 의존성을 각자 환경에 맞는 버전에 따라 설정을 해주며

등록 예

 

 

WebSecurityConfigurerAdapter

원래 예전에는 WebSecurityConfigurerAdapter를 상속받아서 등록을 하였는데 deprecated되었다.. 그래서 위의 방식으로 변경되었다 HttpSecurity를 파라미터로 받아 등록을 빌더패턴으로 해주어서 가독성이 좋아서 편하다.

 

위에 내용중에 antMachers는 Url패턴에 따라 접근할 수 있는 경우를 만들어준다.

내가 만든 경우는 기본적으로 프론트를 JSP을 사용하기 때문에 정적자원이랑 기본적인 회원가입 로그인 메인페이지는 PermitAll()라는 메서드로 인증,인가,권한 없이 요청이 가능하다 나머지는 authenticated()로 인증이 필요하다는 걸로 막아놨다.

그 이후에 밑에 사진에는 짤려서 안보이지만, Oauth2 (소셜로그인)등 세션 정책에 대해 설정이 가능하지만 이번 글에서는 다루지 않겠습니다...

 

이렇게 설정을 하면 알아서 작동이 잘된다... 알아서 해준다.

하지만  스프링 시큐리티가 프레임워크이기 때문에 위에서 말한 거 처럼 그에 설정에 맞게 해줘야한다.

JWT , FormLogin, HttpBasic, Ajax 등등으로 커스텀하여 할 수 있지만 나는 FormLogin으로 하였습니다(기본제공해줌.)

 

자 이제 인증객체에 대해서 알아본다. 

Authentication이라는 interface타입 객체가 시큐리티에 존재한다. 이것은 시큐리티가 인증,인가,권한에 대해 관리를 하기위해 제공되는 객체이다.

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

위에 같이 정의가 되어있다.principal : 사용자의 아이디 및 User객체가 들어있다.credentials: 사용자의 비밀번호가 들어있다.authorities: 인증이 완료된 사용자의 권한목록이다.(권한은 여러개가 있을 수 있다.)details: 인증의 부가정보가 들어있다.Authenticated:인증의 여부 

 

이렇게 들어있다. 이정도들은 SecurityContextHolder에 SecurityContext안에 Authentication으로 들어가 있다.

이것들은 ThreadLocal 로 각자의 스레드에 저장이 되기때문에 thread-safe하며 , session에 정보를 저장을 해주기 때문에 그 정보를 사용을 할 수 있다. (나는 쿠키/세션환경으로 개발을 하였다...)principal에는 usedetails interface타입을 넣을 수있다. 시큐리티가 이렇게 만들었다.

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

이렇게 만들 이유는... 뭐 개발자가 인증에 대한 필수적인 요소를 강제화하기 위해 만들어진 구조가 아닐까싶다.. 보면 다  필요한 필수 요소이다. 이것은 구현을 하여서 만들면 된다. 근데 좀 확실히 사용자의 정보라기엔 내용이 부실하다..그래서 구현클래스를 만들어서 커스텀한다. 필수요소는 강제화하며, 나머지는 알아서 추가를 해주면된다.

 

이제 로그인이 어떻게 진행이 되는지 봐보자.최초로 요청이 오면 위에 설명했다시피 필터에 걸린다.지금은 로그인에 대한 요청이기 때문에 로그인에 대한 시큐리티 필터가 걸릴 것이다. debug를 해보자.

 

AbstractAuthenticationProcessingFilter

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    if (!this.requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        /*로그인에 대한 요청인지 아닌지를 확인 하여 아닌 경우 다음 필터로 진행*/
    } else {
    	/*로그인에 대한 요청이면 실행*/
        try {
        	/*로그인 실행*/
            Authentication authenticationResult = this.attemptAuthentication(request, response);
            if (authenticationResult == null) {
                return;
            }

            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            this.successfulAuthentication(request, response, chain, authenticationResult);
        } catch (InternalAuthenticationServiceException var5) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
            this.unsuccessfulAuthentication(request, response, var5);
        } catch (AuthenticationException var6) {
            this.unsuccessfulAuthentication(request, response, var6);
        }

    }
}

로그인 시에는 UsernamePasswordAuthenticationFilter가 작동을 한다.

사실 그전에 부모인 AbstractAuthenticaionProcessingFilter가 작동을 해서 요청이 로그인 요청인지 확인 후에 맞는경우에는 로그인을 진행한다.

그러면 이제 UsernamePasswordAuthenticationFilter의 attempAuthenticaion이 작동한다.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        username = username != null ? username.trim() : "";
        String password = this.obtainPassword(request);
        password = password != null ? password : "";
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

이제 작동을 하게 되면 요청으로 부터 사용자가 요청한 username과 password 을 찾아서 UsernamePasswordAuthenticationToken에 객체를 생성한다. -> Authenticaiontion 타입이다.

UsernamePasswordAuthenticationToken 이 토큰을 기준으로 this.getAuthenticationManager().authenticate(authRequset)를 실행한다. 그럼 AuthenticationManager에게 처리요청을 위임한다.

 

AuthenticationManager는 interface이며, ProviderManager가 실제 구현하고 있다.

그럼 이제 ProviderManager가 이 인증에 대한 알맞는 AuthenticationProvider를 찾아 (for문으로  찾는다)요청을 위임한다.

Login에 대한 요청은 DaoAuthenticationProvider이다. AbstractUserDetailsAuthenticationProvider를 상속함.

위에 사진 처럼 찾고

찾은 AuthenticationProvider에게 인증을 시도한다. 그럼 부모객체인  AbstractUserDetailsAuthenticationProvider 의

authenticate()가 실행된다. 

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
        return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
    });
    String username = this.determineUsername(authentication);
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;

        try {
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        } catch (UsernameNotFoundException var6) {
            this.logger.debug("Failed to find user '" + username + "'");
            if (!this.hideUserNotFoundExceptions) {
                throw var6;
            }

            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {
        if (!cacheWasUsed) {
            throw var7;
        }

        cacheWasUsed = false;
        /*userDetails에서 만든 loadByusername 실행*/
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    }

    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

위에 코드는 AbstractUserDetailsAuthenticationProvider 의 authenticate이다.

템플릿 메서드 패턴으로 만들어졌다 여기서 this. 는 DaoAuthenticationProvider를 가르킨다.

그럼 위에 주석 처럼 retrieveUser에서 username에 대한 정보를 DB에서 가져오고...

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    this.prepareTimingAttackProtection();

    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    } catch (UsernameNotFoundException var4) {
        this.mitigateAgainstTimingAttack(authentication);
        throw var4;
    } catch (InternalAuthenticationServiceException var5) {
        throw var5;
    } catch (Exception var6) {
        throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
    }
}

 

DaoAuthenticationProvider에서 Password인코더를 사용하며,addtionalAuthenticationChecks에서 DB의 비밀번호와 UsernamePasswordAuthenticationToken의 password(즉 사용자가 작성한 비밀번호)를 비교 후에 맞으면 최종적으로

 

this.createSuccessAuthentication를 호출하여

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
    if (upgradeEncoding) {
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = this.passwordEncoder.encode(presentedPassword);
        user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    }

Authentication객체를 반환한다......

그럼 이제 반환을 하면.. 다시 최초로 먼저 받은

 

AbstractAuthenticationProcessingFilter

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    if (!this.requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
       
    } else {
    	
        try {
        	/*여태 내부적으로 인증을 요청하여 성공한 Authentication객체가 return된다*/
            Authentication authenticationResult = this.attemptAuthentication(request, response);
            if (authenticationResult == null) {
                return;
            }

그러고 나서 또 session 전략등등이 실행되는데 그 전략에 맞게 끝났으면

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }

    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }

    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

위에 successfulAuthentication이 실행되어 securityContextHolder에 넣어준다. 끝!

'Spring' 카테고리의 다른 글

스프링  (0) 2023.03.08
Spring 올바른(?)빈주입?  (0) 2023.02.06
스프링 aop  (0) 2022.12.13
Interceptor 예외처리  (0) 2022.12.11
스프링 배치  (0) 2022.11.23