Restful API 구현을 위한 Spring Security 설정해보기 #3 (인증 Filter)

안녕하세요.!

 

지난 시간에 UserDetails 와 UserDetailsService 인터페이스를 구현하여 인증대상객체와 인증 서비스 로직을 구현해보았습니다.

 

지난 포스팅을 먼저 확인해주시고 이번 내용을 따라와주시면 더 이해가 잘 될것 같습니다.

https://sas-study.tistory.com/359

 

Restful API 구현을 위한 Spring Security 설정해보기 #2 (UserDetails, UserDetailsService 인터페이스 구현)

안녕하세요. 오늘은 이전 포스팅에서 다뤘던 Java 설정파일에 이어서 인증로직을 구현해보겠습니다. 흐름을 파악하시기 위해서 이전 포스팅 내용을 확인 후 진행하시는 편을 추천드립니다.! https

sas-study.tistory.com

 


 

지난 두개의 스프링 시큐리티 포스팅을 보셨다면 아마 /auth에서 아직은 인증절차를 거치지 않아 아직 403(Forbidden)만 Response로 내려올 것입니다.

 

이는 UserDetailsService를 구현한 인증로직을 만들었지만 아직 이를 호출하지 않아서 인증컨텍스트에 존재하는 인증여부 boolean 값이 변경되지 않아서인데요. 이는 막바지에 스프링 시큐리티 내부 클래스를 까보면서 확인해보겠습니다.

 

스프링 시큐리티가 인증을 확인하는 단위는 매 Request 요청이 일어날 때입니다. REST API 이기 때문에 매 요청마다 인증이 필요하게 됩니다. 그렇게 되면 인증토큰이 필요하게 되지만 이번에는 무조건 인증되는 방식으로 해보겠습니다.

 

매 요청마다 확인하기 위해서는 Controller 로직을 수행하기 이전에 로직인 Filter를 이용하면 좋습니다. Filter는 Request 요청마다 한번씩 호출하는 OncePerRequestFilter을 상속합니다.

 


 

프로젝트 구조

 

SecurityAuthenticationFilter.java

public class SecurityAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
                                            throws ServletException, IOException {

		//아무 값이나 집어넣음.
        UserDetails authentication = customUserDetailsService.loadUserByUsername("test"); 
        UsernamePasswordAuthenticationToken auth =
                //여기있는 super.setAuthenticated(true); 를 타야함.
                new UsernamePasswordAuthenticationToken(authentication.getUsername(), null, null);
        SecurityContextHolder.getContext().setAuthentication(auth);
        filterChain.doFilter(request, response);

    }
}

필터는 OncePerRequestFilter를 상속받아 doFilterInternal 메소드를 재정의해줍니다.

 

매 Request 단위마다 Controller를 타기 이전에 해당 필터를 수행할 것이고 SecurityContextHolder에 있는 Context 객체의 authentication 여부에 따라 인증여부가 결정되게 됩니다.

 

그리고 UsernamePasswordAuthenticationToken 객체를 살펴보면

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;
    private final Object principal;
    private Object credentials;

//첫번째 생성자
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

//두번쨰 생성자
    public UsernamePasswordAuthenticationToken(Object principal, 
    										Object credentials, 
                                            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

두번째 생성자를 통해 인스턴스를 생성하면 super.setAuthenticated(true)를 통해 인증여부를 true로 처리하는 로직이 존재합니다. UsernamePasswordAuthenticationToken 클래스가 상속하고있는 AbstractAuthenticationToken 클래스는

 결국 Authentication 객체를 상속하는 것이기 때문에 Spring Security Context 객체에 필터에서 생성한 auth 변수가 setAuthentication(auth);

로 인증객체가 되어 들어갈 수 있게 됩니다.

 


 

OncePerRequestFilter 자체는 필터역할이고 요청 전/후 호출여부를 필터자체에서 등록하는 것이 아닌 Spring Security 설정파일에서 등록할 수 있는 것입니다. 따라서 이를 등록해주는 코드를 SecurityConfig.java에 추가해줍니다.

 

SecurityConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Bean
    public SecurityAuthenticationFilter securityAuthenticationFilter() {
        return new SecurityAuthenticationFilter();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .authorizeRequests()
                    .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                	.antMatchers("/api/v1/test/permit-all").permitAll()
                	.antMatchers("/api/v1/test/auth").authenticated()
                	.antMatchers("/**").authenticated()
                	.anyRequest().permitAll()
                    .and()
                .formLogin().disable()
        ;
        
        http
            .addFilterBefore(securityAuthenticationFilter(), 
            				 UsernamePasswordAuthenticationFilter.class);
    }
}

빈 등록

@Bean
public SecurityAuthenticationFilter securityAuthenticationFilter() {
	return new SecurityAuthenticationFilter();
}

빈등록 후 configure 에서 addFilterBefore로 등록해주면 아래와 같은 결과를 얻을 수 있습니다.!

 

 

결과!!

 

성공적으로 두 url 모두 통과되는 모습을 볼 수 있습니다.!

댓글

Designed by JB FACTORY