chae._.chae

JWT 토큰 서버 구축하기 - (1) 본문

스프링/시큐리티

JWT 토큰 서버 구축하기 - (1)

walbe0528 2022. 7. 28. 17:32
728x90
반응형

서버가 클라이언트의 인증을 확인하는 방식으로는 주로 쿠키, 세션, 토큰이 사용된다. 

💡 JWT란?

JWT(JSON Web Token) 는 디지털 서명이 되어있는, 인증에 필요한 정보들을 암호화시킨 JSON토큰이다.

토큰은 HTTP헤더에 담겨 이동하며, 서버가 클라이언트를 식별하는 용도로 사용된다. 

사용자의 인증작업을 처리하는데 주로 사용된다. 사용자가 JWT를 헤더에 담아 전송하면, 서버는 서명을 검증하고, 검증이 완료되면 요청한 응답을 돌려준다. 

 

JWT는 3가지 파트로 나누어지고, .(점)으로 구분한다.

 

  • Header : 토큰의 타입과 해시 암호화 알고리즘 방법으로 구성
  • Payload : 토큰에 담을 사용자 권한 정보와 데이터 정보가 들어 있다. 
  • Signature : Header, Payload에 해시함수를 적용하고, 개인키(Private Key, 서버만 알고 있는 값)로 서명한 전자서명이 담겨있다. (header + payload + secret(서버만 아는 값)의 형태)

 

 

🔥 흐름!!

  1. 클라이언트가 (아이디, 비밀번호)를 입력해 로그인 시도를 한다.
  2. 서버는 세션이 아니라, JWT를 만들어준다.
  3. 서버가 header, payload, signature를 만들어 각각을 base 64로 인코딩해서 클라이언트에게 토큰을 돌려준다. 
  4. 클라이언트가 권한이 필요한 요청(ex. 개인정보 알려줘)과 함께 JWT를 서버에게 보낸다. 
  5. 서버는 JWT를 신뢰할 수 있는 토큰(유효한 토큰인지)인지 검증하는 과정을 거친다. 
  6. 검증 -> header + payload + secret을 다시 암호화해본다. 값이 동일하면 인증이 끝난다. 
  7. payload에 있는 정보를 갖고 user에게 정보를 전달해준다. 

 

  • 세션

서버에서 유저의 정보를 세션에 저장하고 있어야한다. 메모리, 디스크, db에 이를 담는 방법으로 세션을 유지한다. 

유저가 많은 대형 서비스에서는 세션의 양이 많아지는 만큼 메모리 부하가 걸릴 수 있다. 

  • 토큰

토큰은 stateless하다. (클라이언트-서버의 통신이 끝나면 상태정보를 잊어버린다.) 유저의 정보를 세션에 담아두지 않기 때문에 서버의 메모리에 저장공간을 확보할 필요가 없다. 서버가 토큰을 만들어 클라이언트에게 보내주면, 클라이언트가 이 토큰을 보관하고 있다가 요청을 할때 헤더에 토큰을 실어 보낸다. 

 

 

1. JWT 토큰 사용을 위한 의존성 추가

//   https://mvnrepository.com/artifact/com.auth0/java-jwt
   implementation group: 'com.auth0', name: 'java-jwt', version: '3.19.0'

 

2. Security설정

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

    private final CorsConfig corsConfig;
    private final UserRepository userRepository;

    @Bean  // 비밀번호 암호화를 위해
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.addFilter(corsConfig.corsFilter());
        //http.addFilterBefore(new MyFilter1(), BasicAuthenticationFilter.class);
        // 모든 요청은 이 필터를 타게된다(cross-origin 요청이 와도 모두 허용이됨).
        // @CrossOrigin:인증이 없을때 걸어준다. 인증이 필요한 경우라면 시큐리티 필터를 걸어줘야함
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 세션 사용하지 않고 stateless서버로 만들겠다!
                .and()
               .formLogin().disable()  // jwt서버 사용. 폼로그인 사용X
               .httpBasic().disable()  // http 로그인 방식X
               
               .authorizeRequests()
               .antMatchers("/api/v1/user/**")  // user, admin권한이 있는 사용자만 접근 가능
               .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
               .antMatchers("/api/v1/admin/**")  // admin권한이 있는 사용자만 접근 가능
               .access("hasRole('ROLE_ADMIN')")
               .anyRequest().permitAll();  // 다른 요청은 권한 없이 접근 가능
    }
}

 

시큐리티 안에 내장된 필터말고, 사용자가 직접 필터를 만들어서 걸어줄 수 있다.

addFilterBefore, addFilterAfter를 사용해서 시큐리티 필터 시작 전이나 후로 걸어준다.

 

MyFilter1을 만들어준다.

import javax.servlet.*;
import java.io.IOException;

public class MyFilter1 implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("필터1");
        chain.doFilter(request, response);
    }
}

 

SecurityConfig에서 아래와 같이 걸어준다. 

순서는 MyFilter1을 BasicAuthenticationFilter 필터 실행 전으로 걸어주었다. 

http.addFilterBefore(new MyFilter1(), BasicAuthenticationFilter.class);

 

이렇게 시큐리티 필터에 걸어주지 않고, 내가 직접 설정해주는 방법도 있다. 

MyFilter1이 먼저 실행되고, 그 다음 MyFilter2가 실행된다. 

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<MyFilter1> filter1(){
        FilterRegistrationBean<MyFilter1> bean = new FilterRegistrationBean<>(new MyFilter1());
        bean.addUrlPatterns("/*");
        bean.setOrder(0);  // 낮은 번호가 가장 먼저 실행된다
        return bean;
    }
    @Bean
    public FilterRegistrationBean<MyFilter2> filter2(){
        FilterRegistrationBean<MyFilter2> bean = new FilterRegistrationBean<>(new MyFilter2());
        bean.addUrlPatterns("/*");
        bean.setOrder(1);  // 낮은 번호가 가장 먼저 실행된다
        return bean;
    }
}

 

시큐리티 필터가 내가 만든 필터보다 먼저 동작한다.

시큐리티 필터(SecurityConfig의 필터)가 먼저 동작하고, 그다음 MyFilter1, MyFilter2가 실행된다. 

 

=> 시큐리티 필터보다 먼저 동작하게 해주고 싶다면, SecurityConfig에 addFilterBefore로 걸어줘야 한다!

 

 

CorsConfig를 필터에 등록해서 걸어준다.  CrossOrigin요청이 오더라도 허용이 된다. 

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);  // 내서버가 응답할때 json을 js에서 처리할 수 있게 설정
        config.addAllowedOrigin("*");  // 모든 ip의 응답을 허용
        config.addAllowedHeader("*");  // 모든 header의 응답을 허용
        config.addAllowedMethod("*");  // 모든 post, get, delete, patch요청을 허용하겠다

        source.registerCorsConfiguration("/api/**", config);  // "/api/**"로 오는 모든 설정은 config설정을 따른다
        return new CorsFilter(source);
    }
}

 

 

 

728x90