[Spring] refreshToken 존재 여부에 따른 인증처리 구현

hyng·2022년 1월 5일
0

문제 상황

Spring Security을 사용 한 token 기반 로그인을 구현하면서
로그인 페이지에서 로그인 성공 시 홈페이지로 리다이렉트 해주는 식으로 구현하였는데,
이때 accessToken을 지역변수에, refreshToken을 쿠키에 저장하였다.
하지만 리다이렉트시에 커스텀 쿠키나 헤더를 보낼 수가 없기 때문에 리다이렉트 페이지를 서버에 요청할 경우 토큰을 보낼 수 없어 인증처리가 되지 않아 로그인에 성공했지만 페이지에 접근이 안되는 문제가 발생했다.
해당 문제를 해결하기 위해 홈페이지로 접근 시 request에 유효한 refreshToken 쿠키만 있다면 인증처리해 주는 부분을 기존의 토큰 기반 인증 filter에 추가해 주었다.

Spring Security


출처

Spring Security의 Servlet Filter 지원은 FilterChainProxy에 의해 수행되며 FilterChainProxy는 Security Filter Chain을 통해 많은 작업을 security Filter 인스턴스에 위임한다.

Security Filter Chain에는 LogoutFilter, UsernamePasswordAuthenticationFilter .. 등이 존재하고 이를 순회하면서 필터링을 실시한다.

인증 관련 필터(UsernamePasswordAuthenticationFilter..)는 AuthenticationManager를 통해 인증을 수행한다.


위 아키텍처는 Username과 Password 인증 방식 아키텍처이고, 간략히 설명하면 아래와 같다.

AuthenticationFilter: 인증과 관련된 여러 filter들이 존재하는데

UsernamePasswordAuthenticationFilter도 그중 하나이다. 두 번째 사진에서 AuthenticationFilter는UsernamePasswordAuthenticationFilter을 가리킨다.
앞선 filter에서 이미 인증된 사용자의 경우는 이후 인증 filter에서 인증 과정을 거치지 않는다.
요청 정보를 이용해 인증 객체를 생성하고 AuthenticationManager에게 인증 객체를 전달한다.
이후 인증에 성공할 경우 Security Context에 전달받은 인증 객체를 저장한다.

UsernamePasswordAuthenticationToken: 이름, 권한, 인증 여부 등을 나타내는 인증 객체를 구현한 클래스이며, principal(접근 주체)을 id로 credentical(비밀번호)을 password로 사용한다.

AuthenticationManager: Spring Security에서 제공하는 인터페이스로, 가장 많이 사용되는 구현체로는 ProviderManager가 있다.

실제 인증 처리 로직을 가지는 AuthenticationProvider 리스트를 가지고 있으며 해당 인증 객체를 지원하는
AuthenticationProvider에게 인증 처리를 위임한다.

AuthenticationProvider: 인증 전의 인증 객체를 전달받아 인증이 완료된 객체를 반환하는 역할을 한다.
지정된 인증 유형을 지원하는지 확인할 수 있도록 supports메소드를 제공한다.
전달받은 사용자 정보를 UserDetailsService에 넘겨 준 후 UserDetailsService에서
반환하는 UserDetails 객체를 이용해 인증 과정을 거친다.
Authenticaion 타입을 지원하는 여러 AuthenticationProvider 중 하나라도 인증에 성공한다면
AuthenticationManager에 인증된 Authentication 객체(Authenticated==true)를 반환하고 이는
AuthenticationFilter에 전달된다.
인증에 실패한다면 AuthenticationException 을 발생시킨다.

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

UserDetailsService: UserDetails 객체를 반환하는 하나의 메서드만 가지는 인터페이스로,
전달받은 사용자 정보를 이용해 DB에서 찾은 사용자 정보를 기반으로 UserDetails 객체를 만들어 AuthenticationProvider에게 권한을 담은 인증 객체를 반환한다.

Spring Security를 이용해서 필자의 경우처럼 실질적인 인증 과정(아이디, 패스워드 비교)을 거치지 않고
refreshToken만 있다면 인증처리를 할 수 있도록 하기 위해선 아래처럼 두 개를 구현해 주어야 한다.

​AuthenticationFilter, Authentication

구현

config

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.    
                csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler())
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/", "/login", "/join","/refreshToken","/logout")
                .permitAll()
                .antMatchers(HttpMethod.POST, "/signup")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

SecurityAuthenticationFilter의 첫 번째 인증 필터인 UsernamePasswordAuthenticationFilter 앞 부분에
custom filter를 추가한다.

filter

@Slf4j
@RequiredArgsConstructor
@Component


public class JwtAuthenticationFilter extends GenericFilterBean {

    private final RefreshTokenService refreshTokenService;


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
   
        //login -> chatRoomList redirect시, refreshToken으로 접근가능하도록 하기 위함.
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        if(cookies != null) {

            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("refreshToken")) {
                    RefreshToken refreshToken = refreshTokenService.findByToken(cookie.getValue());
                    if(refreshToken != null && refreshTokenService.verifyExpiration(refreshToken))
                    {
                        RefreshAuthToken refreshAuthToken = new RefreshAuthToken(cookie.getValue());
                        refreshAuthToken.setAuthenticated(true);
                        SecurityContextHolder.getContext().setAuthentication(refreshAuthToken);

                        break;
                    }


                }
            }
        }

   
        chain.doFilter(request,response);

    }
}

GenericFilterBean을 상속받은 filter를 만든다.
filter에서 유효성을 검증하고 SecurityContextHolder에 인증 객체를 저장해 주었다.
단순히 refreshToken의 유효성만 판단하여 인증 처리할 것이기 때문에, AuthenticationManager, AuthenticationProvider.. 등 이후 과정은 구현하지 않았다.

Authentication

public class RefreshAuthToken extends AbstractAuthenticationToken {

    private final String principal;
    private Object credentials;

    public RefreshAuthToken(String principal){
        super(null);
        this.principal = principal;
        this.credentials = null;

    }



    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

AbstractAuthenticationToken 을 상속받아 인증 객체를 만든다.
단순 인증만을 위한 인증 객체였기 때문에 principal, credentials 모두 null로 두었다.

참고

https://imbf.github.io/spring/2020/06/29/Spring-Security-with-JWT.html
https://mangkyu.tistory.com/76
https://derekpark.tistory.com/101

profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글