JWT(Json Web Token) - 1

수정이·2022년 8월 21일
0

security-jwt

목록 보기
5/7
post-thumbnail

JWT

JWT란 어떤 홈페이지에 사용자 인증을 할 때, 세션보다 간단하게 인증을 할 수 있는 기능이다.
왜 세션보다 간단하다고 말하냐면, 네이버를 예시로 들겠다.

네이버같은 큰 사이트는 방문객이 엄~청 많을 것이다. 동시 접속자 수도 많을 것이다.
네이버에 만약 서버가 한 개 있다고 생각하면, 서버 한 개에 세션이 한 개 있을 것이다.
네이버가 동시 접속자수가 100만명이라고 하고, 서버가 동시에 처리할 수 있는 양이 접속자 1000명이라고 가정하자.

그렇다면 사용자들은 엄청 기다려야할 것이다. 그래서 서버를 여러개를 구비하여 처리하면 서버가 한 개일 때보다 훨씬 수월하게 처리할 수 있을 것이다.

하지만, 사용자들이 접속할 때마다. 그 사용자가 세션에 저장된 사용자인지 아니면 없는 사용자인지 판단을 해야하는데, 서버마다 세션이 있어서 만약 사용자A가 처음 로그인할 때는 1번 서버를 이용해서 1번 서버의 세션에 저장되어 있는데, 사용자가 많아서 4번 서버를 이용했다고 하자.

그러면 사용자A의 정보는 1번 세션에 있고, 4번 세션에는 없다. 그러면 서버에서는 다시 세션에 저장하는 상황이 발생한다.

이런 상황을 해결해줄 수 있는 것이 바로 JWT 이다.
JWTxxxx.yyyy.zzzz 이렇게 .을 기준으로 3부분으로 나눠져있다. 이 3부분은 다음과 같다.

Header : {HS256 or RSA} -> 암호화 방식이 들어가 잇다.
Payload : {username : jeongsu} -> 정보가 들어가 있다.
Signature : {HS256(Header + Payload + secretKey)} -> Header + Payload + secretKey를 
			HS256으로 암호화한 값이 들어 있다.

참고로 Header, Payload, Signature는 각각 Base64로 인코딩한다.
위에서 secretKey는 서버밖에 모르는 비밀키이다.

이제 이것을 어떻게 사용하는가?

1. 사용자가 (ID : user, PS : 1234)로 로그인을 한다.
2. 서버에서 적합한 사용자이면 Header, Payload, Signature를 설정하고, 
   토큰으로 만들어 사용자에게 반환한다.
3. 사용자는 토큰을 가지고, 사이트를 돌아다니면서 요청을 보낼 때 토큰을 같이 보낸다.
4. 서버는 사용자에게 토큰을 받으면, Header와 Payload는 알고있으니 
   두 정보와 비밀키를 조합하여 Signature를 만들고, 
   서버에서 만든 값과 사용자에게 받은 값을 비교하여 맞으면 인증완료! 틀리면 인증이 안된 것이다.

토큰을 사용하면 위 그림과 같이 사용자 정보를 세션마다 저장할 필요가 없어진다.


프로젝트 설정

mvnrepository에서 jwt를 검색하여 Java JWT를 디펜던시에 추가한다.

그리고 application.yml을 다음과 같이 설정한다.

server:
  port: 8080
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
    username: cos
    password: cos1234
  jpa:
    hibernate:
      ddl-auto: create #create update none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true

그리고 테스트를 위해 RestApiController를 만들어 테스트 해본다.

@RestController
public class RestApiController {

    @GetMapping("/home")
    public String home() {
        return "<h1>home</h1>";
    }
}

Spring Security 설정

로그인에 사용될 User 클래스는 다음과 같다.

@Data
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String username;
    private String password;
    private String roles; // USER, ADMIN

    public List<String> getRoleList() {
        if (this.roles.length() > 0) {
            return Arrays.asList(this.roles.split(","));
        }
        return new ArrayList<>();
    }
}

JWT를 사용할 때는, 기존 우리가 설정한 것과는 다르게 설정한다. 코드를 보면서 설명하겠다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(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();
        return http.build();
    }

JWT를 사용할 때는 기존 시큐리티 설정과 3가지가 다르다. JWT를 사용하기 위한 기본 설정은 다음과 같다.

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
: 세션을 사용하지 않겠다는 의미이다.

formLogin().disable() : form 태그를 만들어서 로그인을 하지 않는다는 의미이다.

httpBasic().disable() : 기본적인 http 로그인 방식을 사용하지 않는다는 의미이다.

이 세가지가 JWT를 사용하기 위한 기본 설정이다.
그 다음 코드들은 이전에 배웠기 때문에 생략하겠다.

Cors 설정

다음은 cors 설정을 해준다.

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true); 
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/api/**", config);

        return new CorsFilter(source);
    }
}

config.setAllowCredentials(true) : 내 서버가 응답을 할 때, Json을 자바스크립트에서 처리할 수 있게 할지를 설정하는 것이다.

config.addAllowedOrigin("*") : 모든 IP에대한 응답을 허용하겠다는 의미이다.

config.addAllowedHeader("*") : 모든 header에대한 응답을 허용하겠다는 의미이다.

config.addAllowedMethod("*") : 모든 메소드 함수(Get, Post, Put, Patch, Delete)에 대한 요청을 허용하겠다는 의미이다.

위와 같이 설정한 클래스를 SecurityConfig에 등록시켜준다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor <--- 추가
public class SecurityConfig {

    private final CorsFilter corsFilter; <--- 추가

    @Bean
    public SecurityFilterChain securityFilterChain(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();
        return http.build();
    }
}

cors 설정을 하는 방법은 이 방법 말고도 @CrossOrigin 어노테이션을 붙여주는 방법이 있다.
하지만 @CrossOrigin 은 인증이 없는 URI 요청일 때 사용하는 것이고,
시큐리티 필터에 등록하는 방식은 인증이 필요할 때 사용하는 방법이다.


참고

스프링부트 시큐리티 & JWT 강의

0개의 댓글