본문 바로가기
스프링 & Jpa/📌Spring Security 강좌

인프런 시큐리티 강좌 #2 - security 기본

by IMSfromSeoul 2022. 1. 21.

📌 JWT library

https://mvnrepository.com/artifact/com.auth0/java-jwt/3.18.2

📌 프로젝트 생성

plugins {
	id 'org.springframework.boot' version '2.6.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// https://mvnrepository.com/artifact/com.auth0/java-jwt
	implementation group: 'com.auth0', name: 'java-jwt', version: '3.18.2'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

test {
	useJUnitPlatform()
}

📌 security 관련 내용

▸ csrf

  • csrf란 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위를 하게 하는 것
    • get의 공격 경우, img 태그 안에 url과 얻고자 하는 parameter값을 넣어서 웹 페이지 로딩 시 특정 url로 요청을 보내도록 할 수 있다.
    • post의 경우 form tag를 hidden으로 사이트를 load하여서 사이트가 load되자 마자 특정 url로 요청을 보내도록 할 수 있다.
  • spring은 기본적으로 csrf protection을 제공한다.
    • api 서버로 사용할 경우 어차피 token인증 방식을 기본으로 하기 때문에 csrf protection을 disable()로 사용하는 것을 권장한다.

▸ cookie 와 jwt의 차이점

  • cookie - session
    • cookie는 사용자 정보를 client에 저장하는 방식이고, session은 사용자 인증 정보를 서버에 보관하는 방식이다.
      • cookie의 경우 key와 value값으로 이루어져 있는데, cookie는 무결성이 보장되지 않는 반면 jwt는 무결성이 보장된다.
  • cookie와 bearer token
    • 가장 큰 차이점은 cookie는 요청(request)을 보낼 때 자동적으로 보내진다는 것이다.
    • 반면에 bearer token방식은 http request에 명시적으로 추가해서 보내야 한다.
      • 자동적으로 보내기 때문에 csrf 와 같은 공격을 당해 사용자 정보가 탈취될 수 있다.
    • 또한 cookie는 browser기반이 아닌 안드로이드, 타블렛 앱 등이 서버의 api를 사용하는데 어렵다.

▸ bearer 란

JWT 혹은 OAuth에 대한 토큰을 사용한다는 의미이다.

📌 SecurityConfig 기본 설정

public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/api/v1/user/**")
                .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/manager/**")
                .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/admin/**")
                .access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll();
    }
}
  • http.sessionManageMent().sessionCreationPolicy(SessionCreationPolicy.SATELESS)
    • 세션을 사용하지 않겠다.
  • httpBasic().disable()
    • 원래는 기본으로 http basic auth 기반으로 로그인 창이 뜬다. 해당 기능을 사용하지 않겠다.
  • authorizeRequest()
    • 요청에 대한 권한을 지정할 수 있다.
    • 시큐리티 처리에 HttpServletRequest를 사용한다는 것을 의미한다.
  • antMatchers() , access()
    • 해당 경로를 access() 안에 있는 hasRole('') argument값만 허락한다.
  • anyRequest().permitAll()
    • 나머지 요청에 대해서는 모두 허락한다.

📌 CorsFilter 추가

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

    private final CorsFilter corsFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilter(corsFilter)
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/api/v1/user/**")
                .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/manager/**")
                .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/admin/**")
                .access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll();
    }
}

▸ CorsConfig.java

package com.jwtpractice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorConfig {
    @Bean
    public CorsFilter corsFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true); // server 가 client 에서 json 처리를 javascript 로 할 수 있게 허락
        config.addAllowedOrigin("*"); // 모든 ip 에 응답 허용
        config.addAllowedHeader("*"); // 모든 header 에 응답 허용
        config.addAllowedMethod("*"); // 모든 method 에 응답 허용
        source.registerCorsConfiguration("/api/**",config);
        return new CorsFilter(source);
    }
}

▸ http basic 방식

매 요청마다 id, password를 보내는 방식

📌 필터 걸기

▸ MyFilter1.java

package com.jwtpractice.filter;

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);
    }
}

servlet 밑에 있는 servlet을 상속 받아야 한다.

▸ SecurityConfig.java

package com.jwtpractice.config;

import com.jwtpractice.filter.MyFilter1;
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.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 MyFilter1(), BasicAuthenticationFilter.class); // 이 부분 추가
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilter(corsFilter)
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/api/v1/user/**")
                .access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/manager/**")
                .access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
                .antMatchers("/api/v1/admin/**")
                .access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll();
    }
}

위처럼 하면 필터가 걸린다.

그런데 필터가 여러 개면 위처럼 여러 개를 config에 작성하기에 좋지 않다.

그래서 FilterConfig class를 따로 뺀다.

▸ FilterConfig.java

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<MyFilter1> filter1(){
        FilterRegistrationBean<MyFilter1> bean = new FilterRegistrationBean<>(new MyFilter1());
        bean.addUrlPatterns("/*");
        bean.setOrder(0);
        return bean;
    }
}

만약 필터를 여러개를 걸고 싶으면 아래와 같이 필터를 새로 생성하고, FilterConfig에 필터를 다시 등록해주면 된다.

  • bean.setOrder()
    • 낮은 번호의 Order가 먼저 실행된다.

▸ FilterConfig.java

package com.jwtpractice.config;

import com.jwtpractice.filter.MyFilter1;
import com.jwtpractice.filter.MyFilter2;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@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;
    }
}
  • MyFilter2MyFilter1을 그대로 복붙했다.

▸ 필터 실행 순서

이 때, security filter chain이 내가 만든 기본 filter보다 먼저 동작하게 돼 있다. 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new MyFilter3(),BasicAuthenticationFilter.class); // <-- 이 부분 추가
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
		...
}

addFilterBefore 에 MyFilter3를 추가해보면 MyFilter3 -> MyFilter1 -> MyFilter2 순으로 호출되는 것을 볼 수 있다.

addFilterAfter는 security filter chian안에서 가장 나중에 동작하는 필터이다.

▸ SecurityFilterChian 그림

https://atin.tistory.com/590

 


bearer 란?
https://velog.io/@cada/%ED%86%A0%EA%B7%BC-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EC%97%90%EC%84%9C-bearer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

csrf
https://zzang9ha.tistory.com/341
https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80
https://wave1994.tistory.com/150

cookie vs jwt
https://stackoverflow.com/questions/37582444/jwt-vs-cookies-for-token-based-authentication

쿠키 세션 설명
https://www.youtube.com/watch?v=OpoVuwxGRDI

authorizeRequest()
https://velog.io/@jayjay28/2019-09-04-1109-%EC%9E%91%EC%84%B1%EB%90%A8

댓글