고딩왕 코범석

JWT를 적용하기 전에 알아야 할 것들 본문

Language & Framework/Spring

JWT를 적용하기 전에 알아야 할 것들

고딩왕 코범석 2021. 3. 2. 21:02

안녕하세요 이번 시간에는 데어프로그래밍 님의 jwt 강의를 듣고 공부한 내용을 정리하기 위한 포스팅입니다. 그럼 바로 시작할게요!


짚고 넘어갈 개념


Filter

시큐리티는 우선 필터체인 방식으로 동작합니다. 기존의 시큐리티 프로젝트와는 다르게 formLogin과 httpBasic을 disable합니다.


1. formLogin을 사용하지 않는 이유

form으로 로그인 하는 경우는 javascript로 서버에 id, pw를 요청합니다.. 이 때, javascript로 요청할 경우 서버에 클라이언트가 갖고 있는 쿠키를 전송하지 못하는데요. 대부분의 서버는 httpOnly값이 true로 설정되어 있기 때문이죠.


하지만 false로 허용을 한다면 javascript로 요청시 보안 상 좋지 않기 때문에 formLogin을 사용하지 않습니다.


2. httpBasic을 사용하지 않고 Bearer 방식을 사용하는 이유

httpBasic방식은 요청헤더의 Authorization key에 id, pw를 그대로 노출합니다. 이 경우는 상당히 위험합니다! Bearer 방식은 Authorizatoin key의 value에 id, pw를 암호화한 토큰을 들고 요청하게 되며, 이 토큰이 우리가 적용해볼 Json Web Token이죠!


시큐리티 필터는 우리가 따로 설정한 필터들 보다 우선적으로 실행됩니다. 그렇다면 로그인을 시도할 때, 요청의 헤더 Key인 Authorization의 value를 시큐리티 필터가 실행되기 전에 필터를 통해 검증하게끔 해야하는데, 이 필터를 MyFilter3이라 했으며, 코드는 다음과 같습니다.


package com.example.jwt.config;

import com.example.jwt.config.jwt.JwtAuthenticationFilter;
import com.example.jwt.filter.MyFilter1;
import com.example.jwt.filter.MyFilter3;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
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.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CorsFilter corsFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new MyFilter3(),
                SecurityContextPersistenceFilter.class);    //시큐리티는 필터체인 방식이다. 이 필터가 시큐리티의 맨 처음이다!
        http.csrf()
                .disable();
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않음
            .and()
                .addFilter(corsFilter)  // @CrossOrigin(컨트롤러 클래스 레벨에 달아주는것)는 인증이 없는경우만 사용가능, 인증이 필요할때는 필터를 달아주어야 한다.
                .formLogin().disable()  // jwt를 사용하므로 생략
                .httpBasic().disable()  //
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))   //jwt 필터 달아주기, AuthenticationManager를 던져줘야함
                .authorizeRequests()
                    .antMatchers("/api/v1/user/**")
                        .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                    .antMatchers("/api/v1/manager/**")
                        .access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                    .antMatchers("/api/v1/admin/**")
                        .access("hasRole('ROLE_ADMIN')")
                    .anyRequest()
                        .permitAll();
    }
}

MyFilter3의 로직을 보겠습니다.


package com.example.jwt.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class MyFilter3 implements Filter {

    /**
     * 시큐리티 필터가 우선적으로 호출된다! before, after 모두!
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /**
         * 이 jwt 토큰은 시큐리티 필터가 적용되기 전에 적용되어야한다.
         */
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

//        토큰을 만들었다고 가정(cos)
//        id, pw를 입력받고 로그인이 완료되면 토큰은 만들어주고 응답해주기
//        요청할 때 마다 header에 Authorization key에 value로 토큰이 옴
//        넘어온 토큰을 내가 만든건지 검증하기
        if (req.getMethod().equals("POST")) {
            System.out.println("post 요청됨");
            String headerAuth = req.getHeader("Authorization");
            System.out.println("headerAuth = " + headerAuth);
            System.out.println("필터3");

            if (headerAuth.equals("cos")) {
                chain.doFilter(req, res);
            } else {
                PrintWriter out = res.getWriter();
                out.println("인증 안됨!");// 그 이후의 필터들이 적용되지 않는다.
            }
        }
    }
}

콘솔에 찍힌 print들을 보면 이 MyFilter3 필터 호출됩니다. 이후 요청 메서드가 post, header의 Authorization Key값이 서버에서 준 코드와 일치하다면 chain.doFilter 메서드를 통해 이후 시큐리티 필터에 걸린 메서드들이 동작하게끔 구현해줍니다.


이제 JwtAuthenticationFilter를 보겠습니다.

package com.example.jwt.config.jwt;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

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

/**
 * 스프링 시큐리티에서 UsernamePasswordAuthenticationFilter가 있음
 * 동작할 때 : /login 요청후 username, password를 post로 전송하면
 * 이 필터가 동작한다. 
 * 
 * formLogin()을 disable해서 동작하지 않기 때문에 시큐리티 필터에 이 필터를 등록하기
 */
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    /**
     * /login 요청시 login 시도를 위해 실행되는 메서드
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("JwtAuthenticationFilter 로그인 시도 중");

        /**
         * username, password 받아서
         * 정상인지 로그인 시도하기
         * AuthenticationManager로 로그인 시도를 하면
         * PrincipalDetailsService의 loadUserByUsername 메서드 호출 호출된다.
         *
         * PrincipalDetails를 세션에 담고 > 이걸 세션에 담지 않으면 권한 관리가 안된다.
         * > 이 때, 서버세션이 아니라 시큐리티 세션이다!
         *
         * JWT 토큰을 만들어서 응답해준다.
         */
        return super.attemptAuthentication(request, response);
    }
}

SecurityConfig를 보시면 .addFilter(new JwtAuthenticationFilter(authenticationManager())) 메서드가 있을겁니다. 이 의미는 JwtAuthenticationFilter를 시큐리티의 검증 필터로 등록하고 WebSecurityConfigurerAdapter가 빈으로 갖고 있는 authenticationManager 빈을 생성자를 통해 JwtAuthenticationFilter에 주입시켜 줍니다.


이후의 로직은 다음 포스팅에서 마저 이어가보도록 하겠습니다. 긴 글 읽어주셔서 감사하구 피드백은 항상 달게 받습니다!

출처

'Language & Framework > Spring' 카테고리의 다른 글

Service Layer 분리하기!  (0) 2021.05.05
비동기 이벤트를 테스트하는 방법 With JUnit  (0) 2021.04.10
Security 1. 기초 설정 ~ 로그인  (0) 2021.02.22
@Async, 비동기 기능  (0) 2021.02.16
ResponseEntity란?  (4) 2021.02.08