본문 바로가기
TIL(Today I Learned)

JWT를 이용한 인증, 인가 구현 과정 기록

by kkkdh 2023. 3. 2.
728x90

지난 글에서는 jwt를 생성하는 JwtProvider 구현까지 진행했었는데, 이번에는 비밀번호 암호화 + jwt 생성 구현물을 이용해서 SpringSecurity를 이용한 특정 자원에 대한 인증과 인가 조건을 구현해보려 합니다.

 

사이드 프로젝트 JWT 적용 과정 기록

오늘은 주말을 맞아 jwt를 공부할겸, 여러 기술 블로그와 강의를 참고해서 jwt를 공부하고 사이드 프로젝트에 적용해 봤고, 이 내용을 간단하게 기록해보려 합니다. JWT에 대해 잘 정리된 글들과

kkkdh.tistory.com


토큰 기반 인증 구현

Spring Security를 처음 공부해서 그런지 우선 WebSecurityConfigurer를 상속 받는 설정 파일부터 이해하기가 힘들었습니다.

 

일단 이것부터 제대로 공부해보니까 WebSecurityConfigurer를 상속받은 후 configure method를 overriding 하는 방식 자체가 deprecated 되었더라 고요..

 

그래서 다음 방식으로 설정 해줘야 합니다.

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
    // 생성자 DI
    // 기존의 configure를 overriding하고, WebSecurityConfigurerAdapter를 상속받는 방식 대신
    // 수동 빈 등록을 통한 자동 진행 과정을 변경됨
    private final JwtProvider jwtProvider;

    //PasswordEncoder interface의 구현체가 BCryptPasswordEncoder 임을 수동 빈 등록을 통해서 명시한다.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        http
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                    // 글 작성시에는 ROLE_USER 권한이 있어야 한다.
                    .antMatchers("/login", "/sign-up", "/loginerror").permitAll()
                    .anyRequest().authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt 사용하는 경우
                .and()
                .addFilterBefore(new JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

코드에도 적혀있듯이 configure method overriding 방식이 직접 Bean을 등록하는 방식으로 변경되었습니다.

 

여러 가지 블로그도 찾아보고 공식 문서를 찾아보니 filterchain 설정에서 loginForm과 logout method chaining을 해서 기본적으로 Spring Security에서 제공하는 로그인과 로그아웃 방식을 설명하고 있었는데

 

UserDetails, UserDetailsService 등의 객체들을 overriding 하는 방식을 참고해서 해봤는데, 도저히 해결이 안되어서 filter를 하나 직접 등록해서 인증을 처리하는 방식을 선택했습니다.

 

addFilterBefore method를 이용해서 UsernamePasswordAuthenticationFilter 앞에 직접 구현한 필터를 추가할 수 있었습니다.

그게 바로 이 부분입니다.

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    @Autowired
    private MemberService memberService;
    private final JwtProvider jwtProvider;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        final String authorization = request.getHeader("Authorization");
        logger.info("authorization : " + authorization);

        if(authorization == null || !authorization.startsWith("Bearer ")){
            logger.error("authorization is null");
            filterChain.doFilter(request, response);
        }

        // Authorization에서 token 추출
        String token = authorization.split(" ")[1];

        // token 유효성 검증
        boolean expired = jwtProvider.isExpired(authorization);
        if(expired){
            logger.error("token is expired");
            filterChain.doFilter(request, response);
        }

        String userName = (String)jwtProvider.parseJwtToken(authorization).get("userEmail");

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userName, null, List.of(new SimpleGrantedAuthority("USER")));

        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // filter chain의 다음 filter를 호출
        System.out.println("end of authorization with jwt filter");
        filterChain.doFilter(request, response);
    }
}

JwtFilter를 위와 같이 따로 구현했습니다.

 

사실 제가 다른 분들의 글을 보며 느꼈듯이, 위의 코드도 처음보면 알아보기가 너무 빡셀 것 같기는 합니다.

 

그래도 간단히 설명하자면, OncePerRequestFilter를 상속받아 filter를 구현하고, doFilterInternal method를 overriding 해서 filter의 내부 동작을 정의했다고 보면 됩니다.

 

OncePerRequestFilter는 이름에서 알 수 있듯이, 사용자의 요청마다 딱 한 번만 실행되는 필터를 만들기 위해 사용하는 추상 클래스라고 합니다. (나중에 더 공부해야 하지 않을까 싶습니다..)

 

이 Filter chain 개념도 처음에는 무슨 소린지 몰랐는데, 이 부분은 다음 링크를 참고하면 좋을 것 같습니다.

 

 

Architecture :: Spring Security

Spring Security’s Servlet support is based on Servlet Filters, so it is helpful to look at the role of Filters generally first. The following image shows the typical layering of the handlers for a single HTTP request. The client sends a request to the ap

docs.spring.io

 

그래서 이렇게 요청에 맞는 servlet을 연결해주는 DispatcherServlet 이전에 filter chain을 실행하고, 그 중간에 jwt를 이용한 인증 과정을 정의한 JwtFilter가 동작해서 SecurityContextHolder에 인가 정보(Authentication)를 저장하고, 인가 정보가 있어야 특정 자원에 접근이 가능한 것이라고 정리할 수 있었습니다.

 

사실 Authentication 정보를 보낼때, Authority를 만드는 과정에서 원래는 Member entity에 등록된 auth 정보를 모두 전달해야 하는데, USER라는 권한만 부여되도록 설정했습니다.

 

이 부분도 개선해야 하고, Spring Security에 몇일 매달려서 공부하기는 했는데, 당장 필요한 기능들만 간당간당하게 이해한 것 같아서 프로젝트를 진행하면서 더 공부하고, 정리해야 될 것 같습니다..

 

정석대로 사용자 정보를 토큰을 통해 인증하고 전달하려면, Authentication 객체에 UserDetails 객체를 이용해 사용자 정보를 담아 저장하고, 이렇게 만들어낸 Authentication에 올바른 권한을 부여해서 controller component에서 사용하게 구현하는 것이 정석인 것 같습니다.

 

→ 알고보니 Spring Security에서 기본적으로 제공하는 인증과 인가는 UserDetails, UserDetailsService 인터페이스의 구현체를 만들어 아이디와 비밀번호를 기반으로한 방식이기 때문에, 이렇게 구현하는 방법이 맞았습니다!

 

그래도 이렇게 구현한 결과 일단 적용하고 싶었던, 로그인 과정에서 jwt를 만들어 cookie로 전달하고 사용자의 request message의 header를 검사해서 jwt의 유효성을 확인하는 과정 구현에는 성공한 것 같아서 여기까지 공부하고 넘어가려고 합니다.

 

jwtProvider를 구현할때 까지만 해도 금방하겠다 싶었는데, 이렇게 Spring Security에 방대한 내용이 있을 줄 몰랐습니다..

이 영상을 참고해서 Spring Security 이용이 그래도 수월했던 것 같아 남겨봅니다!

728x90

댓글