Spring Security UsernamePasswordAuthenticationFilter

배세훈·2021년 8월 8일
1

Spring

목록 보기
7/38

UsernamePasswordAuthenticationFilter는 Spring Security에서 formLogin을 할 때 사용할 수 있는 Filter 입니다.

UsernamePasswordAuthenticationFilter 핵심 내용

  • UsernamePasswordAuthenticationFilter는 자신이 처리할 url에 요청이 들어올 경우, 기본적으로 다음 필터로 동작을 넘기지 않습니다.

  • UsernamePasswordAuthenticationFilter는 다음 필터로 동작을 넘기더라도 아직 Context가 설정되지 않은 상태에서 동작이 전달되므로 인증 후 정보를 활용할 수 없습니다.

  • UsernamePasswordAuthenticationFilter를 상속해서, 해당되는 부분 재 구현하면 됩니다.

  • 대체 Filter를 만든다면, 성공/실패에 따른 응답 반환은 필수입니다.

  • OAuth에 jwt를 재사용 하는 등, 인증과 Jwt 생성 사이의 유연성을 보장하고 싶다면 로그인 필터와 jwt 필터를 따로 만들어야 합니다.

UsernamePasswordAuthenticationFilter의 핵심 동작 방식

UsernamePasswordAuthenticationFilter는 Spring Security에서 '기본적인' Form Login을 지원하기 위해 미리 구현된 Filter입니다.

흔히, Spring Security 설정에서 http.formLogin()과 같은 설정을 하게 되면, 이 필터가 등록됩니다.

UsernamePasswordAuthenticationFilter는 먼저, request로부터 username, password를 추출하여 인증이 완료되지 않은 UsernamePasswordAuthenticationToken을 만든 후, 이 token을 ProviderManager(AuthenticationManager의 구현체)에게 넘겨 인증 책임을 위임합니다.

ProviderManager는 자신이 가진 Provider들 중에서, Parameter로 들어온 Authentication을 인증할 수 있는 Provider를 찾은 후, 인증 책임을 위임합니다.

이 중 DaoAuthenticationProvider가 이 유형의 Authentication을 처리할 수 있습니다. DaoAuthenticationProvider는 Bean으로 등록된 UserDetailsService로부터 해당 username을 가진 user를 찾아옵니다.

그 후 비밀번호 일치 등의 검증을 수행합니다.

검증이 성공적이었다면, 검증여부가 성공으로 체크된 Authentication 객체를 새로 생성해 반환해줍니다.

UsernamePasswordAuthenticationFilter 의 유의할 특징

  1. UsernamePasswordAuthenticationFilter는 지정한 url에서만 동작하도록 되어있다.

UsernamePasswordAuthenticationFilter의 부모 클래스인 AbstractAuthenticationProcessingFilter는 Filter 작업 맨 처음에 Authentication이 필요한지를 체크합니다.

-> 미리 지정된 url 이름과 현재 들어온 url이 일치하는지 체크합니다. 만약 지정된 경로가 아니었을 경우, UsernamePasswordAuthenticationFilter는 인증작업을 수행하지 않고 바로 다음 필터들을 호출합니다.

  1. UsernamePasswordAuthenticationFilter는 기본적으로 동작이 완료된 후 이후 다른 Filter를 동작시키지 않는다.

  1. UsernamePasswordAuthenticationFilter는 기본적으로 동작이 완료된 후 바로 Response를 반환한다.

UsernamePasswordAuthenticationFilter는 성공적으로 인증을 완료했을 경우, Context를 저장하고, successHandler를 호출하도록 되어있습니다.

UsernamePasswordAuthenticationFilter에 등록된 SuccessHandler는 SimpleUrlAuthenticationSuccessHandler로, 지정된 경로로 redirect를 시키도록 되어 있습니다. (default "/")

이 redirect가 호출되면 redirect 경로에 해당하는 내용의 페이지가 Response로 반환되기 때문에, 이후 추가 응답을 보낼 수 없습니다.

  1. UsernamePasswordAuthenticationFilter는 다른 필터를 실행하더라도, 아직 context가 설정되어 있지 않은 상태이다.

continueChainBefore.. 속성을 통해, 다른 필터를 먼저 동작시킬 수 있습니다.

하지만 다른 필터가 실행된 후에야 Context에 Authentication이 저장되므로, 다른 후속 필터가 실행되는 시점에서는 이 Authentication을 사용할 수 없습니다.

예시)

web.xml 파일

<listener>
	<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

xml 파일

<security:http 
    	pattern="/admin/**"  
    	auto-config="false" // (1)
    	use-expressions="true" // (2)
    	disable-url-rewriting="true" // (3)
    	entry-point-ref="adminLoginEntryPoint"
    	>
		<security:intercept-url pattern="/favicon.ico" access="permitAll" />
        <security:intercept-url pattern="/index.html" access="permitAll" />
        <security:intercept-url pattern="/admin/login" access="permitAll" />
        <security:intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
        <security:custom-filter position="FORM_LOGIN_FILTER" ref="gptwrAuthenticationFilter" />
		<security:logout logout-url="/logout"  success-handler-ref="logoutSuccessHandler" invalidate-session="true" />
		<security:session-management session-authentication-strategy-ref="sas"/>
    </security:http>
    
    // SessionInformation 인스턴스의 레지스트리를 유지 관리합니다.
    <bean id="sessionRegistry"  class="org.springframework.security.core.session.SessionRegistryImpl" />
    
    // 한 사용자의 최대 연결 가능 수 지정
    <bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
		<constructor-arg name="sessionRegistry" ref="sessionRegistry"/>
		<property name="maximumSessions" value="1"/>
	</bean>
    
    // 로그아웃 성공 후 처리로직
    <bean id="logoutSuccessHandler" class="com.web.auth.LogoutSuccessHandler" />
     
     // 로그인 인증 실패시
 	<bean id="adminLoginEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<property name="loginFormUrl" value="/admin/login"/>
	</bean>
	<bean id="siteAdminLoginEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<property name="loginFormUrl" value="/"/>
	</bean>
	    
        // 로그인 검증을 할 authenticationManger 등록
	<security:authentication-manager  alias="authenticationManager">
        <security:authentication-provider ref="adminAuthenticationProvider"/>
    </security:authentication-manager>
    
    // 로그인 검증 필터 등록
	<bean id="authenticationFilter" class="com.web.auth.authenticationFilter">
		<property name="sessionAuthenticationStrategy" ref="sas" />
		<property name="authenticationManager" ref="authenticationManager"/>
		<property name="authenticationFailureHandler" ref="failureHandler" />
        <property name="authenticationSuccessHandler" ref="successHandler" />
    </bean>
    
    // 로그인 인증 성공 후 처리 로직
 	<bean id="successHandler" class="com.web.auth.LoginSuccessHandler" />
    // 로그인 인증 실패 후 처리 로직
    <bean id="failureHandler" class="com.web.auth.LoginFailureHandler" />
    

(1) auto-config='true'를 설정한것만으로 기본 로그인페이지 / HTTP 기본인증 / 로그아웃 기능등을 제공한다.

(2) use-expressions="true"는 SpEL을 사용한다는 의미

use-expression="true"를 설정하지 않으면 default 가 false이다. 이럴때는 SpEL을 사용하지 않는다.

ex) use-expression="false"인 경우

<http auto-config="true">
	<intercept-url pattern="..." access="ROLE_ANONYMOUS" />
    <intercept-url pattern="..." access="ROLE_USER" />
</http>

use-expression="true"로 설정하면 SpEL을 사용해서 작성을 해야한다. 그렇지 않으면 에러가 발생한다.
ex) use-expression="true"인 경우

<http auto-config="true" use-expressions="true">
	<intercept-url pattern="..." access="permitAll" />
    <intercept-url pattern="..." access="hasRole("ROLE_USER") />
</http>

(3) disable-url-rewriting: url에 jsessionId가 붙는것을 방지해준다.

로그인 인증 필터 구현

package com.web.auth;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter{

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    public static final String SPRING_SECURITY_CHECK = "security_check";
    
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private String filterProcessesUrl = SPRING_SECURITY_CHECK;
    
    private final static Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
    
    		
	public AuthenticationFilter() {
		super();
		setFilterProcessesUrl("/" + filterProcessesUrl); // 로그인 인증 필터에 로그인 검증 URL 등록
		setUsernameParameter(usernameParameter); // username custom 변수 등록
		setPasswordParameter(passwordParameter); // password custom 변수 등록
	}
	
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {

		if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);
        
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        
        username = username.trim();

        if (logger.isDebugEnabled()){
        	logger.debug(String.format("username=[%s],password=[%s] ", username,password));
        }
        AuthenticationToken authRequest = new AuthenticationToken(username, password);
        
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        Authentication authentication = this.getAuthenticationManager().authenticate(authRequest);
        
        if(!(authentication instanceof AuthenticationToken))
            throw new RuntimeException("Undesirable toke type");
        
        return authentication;
        
        
	}
	
    // 추가 변수가 필요하다면..
    protected String obtainCustomName(HttpServletRequest request) {
        return request.getParameter("custom_name");
    }
}

인증 토큰 구현

package com.web.auth;

import java.util.Collection;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

public class AuthenticationToken extends UsernamePasswordAuthenticationToken{

	private static final long serialVersionUID = 1L;
    
    private String customName = null;
	
	public AuthenticationToken(Object principal, Object credentials) {
		super(principal, credentials);
	}

	public AuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
		super(principal, credentials, authorities);
	}

	public AuthenticationToken(Object principal, Object credentials,String customName) {
		super(principal, credentials);
		this.customName = customName;
	}
	
	public AuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities,  String customName) {
		super(principal, credentials, authorities);
		this.customName = customName;
	}
	public String getCustomName() {
		return customName;
	}

	public void setCustomName(String customName) {
		this.customName = customName;
	}

	@Override
	public String toString() {
		return "AuthenticationToken [customName=" + customName + ", getName()=" + getName() + "]";
	}
	

}

인증 처리

package com.web.auth;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;

import com.service.auth.AdminAuth;
import com.util.Tool;


@Component
public class AdminAuthenticationProvider implements AuthenticationProvider{

	@Autowired
	AdminAuth adminAuth;
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		
		AuthenticationToken AuthenticationToken = (AuthenticationToken)authentication;
		WebAuthenticationDetails detail = (WebAuthenticationDetails)authentication.getDetails();
		String name = authentication.getName();
        String password = authentication.getCredentials().toString();
        String ipAddress =  detail.getRemoteAddress();
        String customName = AuthenticationToken.getCustomName();
        
        
        // 인증 처리 ..
        List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
	            grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
	            Authentication auth = new AuthenticationToken(name, password, grantedAuths);
        
	    return null;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return authentication.equals(AuthenticationToken.class);
	}

}
profile
성장형 인간

0개의 댓글