🔗 참고자료

  • 유튜브 Amigoscode 채널 => 링크
  • [10분 테코톡] 작은곰의 Spring Security => 링크
  • <Spring Security 란?> 블로그 망나니개발자 => 링크
  • <JWT(Json Web Token)란?> 블로그 망나니개발자 => 링크
  • <JWT란 무엇인가?> 벨로그 hahan => 링크
  • JJWT 라이브러리 관련 공식문서 정리 및 변역된 블로그 => 링크
  • 블로그 <삽질중인 개발자> => 링크

 

✍ 공부하게 된 계기

프로젝트를 진행하면서 스프링 시큐리티는 대부분 이미 구현되어 있는 것을 가져다 사용하는 방식을 했습니다.

그래서 인증과 인가가 어떻게 이뤄지고, 어떤 필터를 타서 인증이 진행되는지 모르는 결과를 초래하게 되었습니다.

구현을 했는데 내부적으로 어떻게 돌아가는지 대략적으로도 모르고, 결국에 구현 후 나에게 남는 게 많이 없을 것 같다고 생각돼서 이렇게 조금이라도 깊게 공부하게 되었습니다.

그리고 공부를 하면서 나오는 다양한 개념(CSRF, JWT, Basic Auth 등등)들이 있어서, 개발자로서 간단한 보안에 대한 시각도 넓혀주고 도움이 될 것이라고 생각해서 이렇게 글을 작성하게 됐습니다.

 

 

 

 

 

1. 인증(Authentication)과 인가(Authorization)

  • 인증(Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차
  • 인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차

스프링 시큐리티는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며,
인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인을 하게 됩니다.

스프링 시큐리티에서는 이러한 인증과 인가를 위해

Principal을 아이디로,

Credential을 비밀번호로 사용하는

Credential 기반의 인증 방식을 사용한다.

 

  • Principal(접근 주체) : 보호받는 Resource에 접근하는 대상
  • Credential(비밀번호) : Resource에 접근하는 대상의 비밀번호

 

 

 

2. 스프링 시큐리티(Spring Security)란?

  • 스프링 기반의 애플리케이션 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다.
  • '인증'과 '권한'에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
    • Filter는 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 URL 요청을 받지만,
      Interceptor는 Dispatcher와 Controller 사이에 위치한다는 점에서 적용 시기의 차이가 있다.
    • 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이
      보안관련 로직을 작성하지 않아도 된다는 장점이 있다.

 

 

 

3. JWT(Json Web Token)란?

  • Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
  • 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다.

 

 

4. JWT의 구성요소

JWT는 헤더(Header), 페이로드(Payload), 서명(Signature) 세 파트로 나눠져 있다.

 

  • 헤더 (Header)
    어떠한 알고리즘으로 암호화 할 것인지, 어떠한 토큰을 사용할 것 인지에 대한 정보가 담겨있다.

  • 정보 (Payload)
    전달할려는 정보(사용자 id나 다른 데이터들, 이것들을 크렘이라고 부른다)가 들어있다.
    payload에 있는 내용은 수정이 가능하여 더 많은 정보를 추가할 수 있다.
    그러나 노출과 수정이 가능한 지점이기 때문에 인증이 필요한 최소한의 정보만을 담아야 한다.
    (아이디, 비밀번호 등 개인정보가 아닌 이 토큰을 가졌을 때 권한의 범위나 토큰의 발급일과 만료일자 등)
  • 서명 (Signature)
    가장 중요한 부븐으로 헤더와 정보를 합친 후 발급해준 서버가 지정한 secret key로 암호화 시켜 토큰을 변조하기
    어렵게 만들어준다.
    한가지 예를 들어보자면 토큰이 발급된 후 누군가가 payload의 정보를 수정하면 payload에는 다른 누군가가
    조작된 정보가 들어가 있지만 signature에는 수정되기 전의 payload 내용을 기반으로 이미 암호화 되어있는
    결과가 저장되어 있기 때문에 조작되어 있는 payload와는 다른 결과값이 나오게 된다.
    이러한 방식으로 비교하면 서버는 토큰이 조작되었는지 아닌지를 쉽게 알 수 있고,
    다른 누군가는 조작된 토큰을 악용하기가 어려워진다.

 

 

 

 

5. 일반 토큰 기반과 클레임 토큰 기반 차이

  • 일반 토큰 기반 인증
    • 검증할 때 필요한 관련 정보들을 서버에 저장해두고 있다.
      => 인증 시 DB에 접근하는 과정이 추가된다.
    • session 방식 또한 저장소에 저장해두었던 session ID를 찾아와 검증하는 절차를 가져야 한다.
  • 클레임 토큰 기반
    • 클레임 토큰 기반 중 하나인 JWT는 사용자 인증에 필요한 정보를 토큰 자체에 담고 있다.
      => 별도의 인증 저장소가 필요 없다.
    • 분산 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는다.
      => 일반 토큰 기반 인증에 비해 편리한 인증 절차

 

 

 

6. JWT 인증 절차

  1. [클라이언트] 자격 증명에 필요한 정보 전송
    - 로그인 시 유저의 아이디, 비밀번호와 같은 자격 증명에 필요한 정보 전송
  2. [서버] 전송받은 자격 증명 정보를 확인(검증)
  3. [서버] 검증 완료된 정보를 기반으로 토큰 생성 및 서명
  4. [서버] 생성된 토큰을 클라이언트에게 전송
  5. [클라이언트] 전송받은 토큰을 기반으로 리퀘스트를 보낼 때 마다 토큰을 전송

 

 

 

 

7. JWT 라이브러리

https://github.com/jwtk/jjwt

 

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

 

  • JJWT는 JVM과 안드로이드에서 쉽게 JWT를 생성하고 확인(검증) 할 수게 하는 라이브러리입니다.
  • JJWT는  Apache 2.0 라이선스에 따라 JWT, JWS, JWE, JWK, JWA RFC 명세 및 오픈소스를
    독점적으로 기반하는 순수 Java 구현이다.

 

Dependencies

 

<Maven>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

 

<Gradle>

dependencies {
    compile 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtime 'io.jsonwebtoken:jjwt-impl:0.11.5',
    // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
    //'org.bouncycastle:bcprov-jdk15on:1.70',
    'io.jsonwebtoken:jjwt-jackson:0.11.5' // or 'io.jsonwebtoken:jjwt-gson:0.11.5' for gson
}

 

String secretKey = "securesecuresecuresecuresecuresecuresecuresecuresecure";

String token = Jwts.builder()
        .setSubject(authResult.getName())
        .claim("authorities", authResult.getAuthorities())
        .setIssuedAt(new Date())
        .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(1)))
        .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
        .compact();
  • 위와 같이 간단하게 JWT 토큰을 만들 수 있다.
  • 대부분의 Header와 Payload에 대한 정보를 이미 JJWT 라이브러리에서 제공한다.
  • JWT 토큰의 Header에 들어가는 알고리즘에 대한 정보는 어떤 알고리즘을 사용하느냐에 따라서 JJWT가 알아서 헤더에 추가해준다.

 

 

8. Spring Security 모듈

 

SecurityContextHolder

  • 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.
  • SecurityContext를 SecurityContextHolder가 관리한다.
  • 기본적으로 ThreadLocal을 사용한다.
    - ThreadLocal : 한 쓰레드 내에서 사용하는 공용 저장소
    - ThreadLocal을 사용해서 'Authentication'을  한 쓰레드 내에서 공유가 가능하다.
    - 쓰레드가 달라지면 제대로 된 인증 정보를 가져올 수 없다.
  • 즉 SecurityContextHolder란 Authentication을 담고 있는 Holder라고 정의할 수 있다.
  • 'Authentication' 자체는 인증된 정보이기에 'SecurityContextHolder'가 가지고 있는 값을 통해 인증이 되었는지
    아닌지 확인 할 수 있다. => Authentication.isAuthenticated();

 

SecurityContext

  • Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.

 

Authentication

  • 현재 접근하는 주체의 정보와 권한을 담는 인터페이스입니다.
  • SecurityContext에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고,
    SecurityContext를 통해 Authentication에 접근할 수 있습니다.

 

 

 

 

 

 

9. 구현 소스코드

JwtUserNamePasswordAuthenticationFilter 코드

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Date;

@RequiredArgsConstructor
public class JwtUserNamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    @Value("${jwt.secretKey}")
    private String secretKey;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response
    ) throws AuthenticationException {

        try {
            UsernamePasswordAuthenticationRequest authenticationRequest = new ObjectMapper()
                    .readValue(request.getInputStream(), UsernamePasswordAuthenticationRequest.class);

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    authenticationRequest.getUsername(),
                    authenticationRequest.getPassword()
            );
            return authenticationManager.authenticate(authentication);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    // attemptAuthentication이 정상적으로 작동되면 아래 메서드가 작동되게 된다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult
    ) {
        String token = Jwts.builder()
                .setSubject(authResult.getName())
                .claim("authorities", authResult.getAuthorities())
                .setIssuedAt(new Date())
                .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(1)))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .compact();

        response.addHeader("Authorization",  "Bearer" + token);
    }
}

 

  • request안에서 username, password 파라미터를 가져와서 UsernamePasswordAuthenticationToken을 생성 후
    AuthenticationManager을 구현한 객체에 인증을 위임한다.
    => ObjectMapper()를 사용해서 JSON 컨텐츠를 Java 객체로 deserialization 한다.
    * ObjectMapper는 Java 객체를 deserialization 하거나 Java 객체를 JSON으로 serialization 할 때 사용하는
    Jasckson 라이브러리의 클래스이다. ObjectMapper는 생성 비용이 비싸기 때문에 bean/static으로 처리하는 것이 좋다

 

UsernamePasswordAuthenticationFilter 란?

  • 요청정보를 받아서 정보 추출을 하여 인증객체를 생성한다.
  • Form based Authentication 방식으로 인증을 진행할 때 아이디, 패스워드 데이터를 파싱하여 인증 요청을 위임하는 필터이다.
  • 유저가 로그인 창에서 Login을 시도할 때 보내지는 요청에서 아이디(username)와 패스워드(password) 데이터를
    가져온 후 인증을 위한 토큰을 생성 후 인증을 다른 쪽에 위임하는 역할을 하는 필터입니다.

 

UsernamePasswordAuthenticationFilter 로직을 순서

 

 

UsernamePasswordAuthenticationToken란?

  • Authentication을 구현한 AbstractAuthenticationToken의 하위 클래스로 usernamePrincipal의 역할을 하고,
    passwordCredential의 역할을 합니다.
  • 첫 번째 생성자는 인증 전의 객체를 생성하고, 두 번째 생성자는 인증이 완료된 객체를 생성해줍니다.

 

AuthenticationManager란?

  • 인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리됩니다.
  • 인증이 성공하면 isAuthentication-true인 객체를 생성하여 SecurityContext에 저장합니다.
  • 인증 상태를 유지하기 위해 세션에 보관하며, 실패할 경우에는 AuthenticationException을 발생시킵니다.

 

 

JwtTokenVerifier 코드

import com.google.common.base.Strings;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class JwtTokenVerifier extends OncePerRequestFilter {

    @Value("${jwt.secretKey}")
    private String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain
    ) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        if (Strings.isNullOrEmpty(authorizationHeader) || authorizationHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authorizationHeader.replace("Bearer ", "");
        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
                    .build()
                    .parseClaimsJws(token);

            Claims body = claimsJws.getBody();

            String username = body.getSubject();

            var authorities = (List<Map<String, String>>) body.get("authorities");

            Set<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
                    .map(m -> new SimpleGrantedAuthority(m.get("authority")))
                    .collect(Collectors.toSet());

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    username,
                    null,
                    simpleGrantedAuthorities
            );

            SecurityContextHolder.getContext().setAuthentication(authentication);

        } catch (JwtException e) {
            throw new IllegalStateException(String.format("Token %s cannot be trust", token) );
        }
    }

}

 

  • 클라이언트에게서 넘어온 JWT토큰을 한번만 인증하면 되기 때문에 OncePerRequestFilter를 사용한다.
  • jjwt 라이브러리를 사용해서 토큰을 쉽게 파싱합니다.
    => 파싱한 토큰 내의 데이터를 authentication 객체에 저장 후 SecurityContextHolder에 set 합니다.

 

OncePerRequestFilter란?

  • 모든 서블릿에 일관된 요청을 처리하기 위해 만들어진 필터
  • 한 요청당 반드시 한 번만 실행된다.

 

 

위 소스코드에서 FilterChain이란?

  • 톰캣은 등록된 필터들의 클래스를 모두 객체화해서 내부 저장합니다.
    필터가 초기화될 때 필요한 부분들 또한 마찬가지입니다.
    이 객체들은 설정된 대로 순서를 가지게 되는데,
    이 순서 정보를 가진 "FilterChain" 인터페이스 객체르 메소드의 파라미터로 넘겨줍니다.
  • 모든 필터 클래스는 "Filter" 인터페이스를 상속받아야 하고, 구현해야 하는 세 개의 메소드 중 실제 필터의 기능을 담당하는 doFilter 메소드를 구현할 때 FilterChain 객체를 넘겨주게 됩니다.

 

UsernamePasswordAuthenticationRequest 코드

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class UsernamePasswordAuthenticationRequest {
    private String username;
    private String password;
}

 

 

 

PrincipalDetails 코드

import com.example.bbakmemo.vo.UserVo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;


@Builder
@AllArgsConstructor
public class PrincipalDetails implements UserDetails {

    private final String accountId;
    private final String password;
    private final boolean enabled;
    private final List<? extends GrantedAuthority> getAuthorities;
    private final UserVo userVo;

    public UserVo getUserVo() {
        return userVo;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return getAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return accountId;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

UserDetails란?

  • 인증에 성공하여 생성된 UserDetails 객체는 UsernamePasswordAuthenticationToken을 생성하기 위해 사용됩니다.
  • UserDetails 인터페이스의 경우 직접 개발한 User 엔티티나 UserDto에 UserDetails를 구현하여 처리할 수 있습니다.

 

 

PrincipalDetailsService 코드

import com.example.bbakmemo.mapper.UserMapper;
import com.example.bbakmemo.vo.UserVo;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final UserMapper userRepository;

    @Override
    public UserDetails loadUserByUsername(String accountId) throws UsernameNotFoundException {

        UserVo userVo =  userRepository.findUserByAccountId(accountId)
                .orElseThrow(
                        () -> new UsernameNotFoundException("유효하지 않은 로그인 정보입니다.")
                );

        return PrincipalDetails.builder()
                .accountId(userVo.getAccountId())
                .password(userVo.getPassword())
                .enabled(userVo.isEnabled())
                .build();
    }
}

UserDetailsService란?

  • DB에서 유저 정보를 불러오는 중요한 메소드가 있는 인터페이스이다.
    => loadUserByUsername() 메소드
  • DB에서 유저의 정보를 가져와서 리턴해준다.

 

 

WebSecurityConfig 코드

import com.example.bbakmemo.config.security.jwt.JwtTokenVerifier;
import com.example.bbakmemo.config.security.jwt.JwtUserNamePasswordAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;


/* WebSecurityConfigurerAdapter란?
    스프링 시큐리티의 웹 보안 기능 초기화 및 설정
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // BCryptPasswordEncoder는 Spring Security에서 제공하는 비밀번호 암호화 객체
    // 비밀번호를 BCryptPasswordEncoder로 암호화 안하면 Spring Security 자체에서 경고를 하게 된다.
    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }


    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    @Override
    public void configure(WebSecurity web) {
        // h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)
        web.ignoring()
                .antMatchers("/h2-console/**")
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .headers().frameOptions().disable().and()
                .csrf().disable()
                .formLogin().disable()

                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .addFilter(new JwtUserNamePasswordAuthenticationFilter(authenticationManager()))
                .addFilterAfter(new JwtTokenVerifier(), JwtUserNamePasswordAuthenticationFilter.class)
                .authorizeRequests()

                // 회원 관리 처리 API 전부를 login 없이 허용
                .antMatchers("/user/signup").permitAll()
                .anyRequest()
                .authenticated();
    }
}

 

 

 

❓ 세션기반 인증과 토큰기반 인증의 차이

  • 세션의 경우 Cookie 헤더에 세션 ID만 실어 보내면 되므로 트래픽을 적게 사용한다.
  • JWT는 사용자 인증 정보와 토큰의 발급시각, 만료시각, 토큰의 ID 등 담겨있는 정보가 세션 ID에 비해 비대하므로
    세션 방식보다 훨씬 더 많은 네트워크 트래픽을 사용한다.
  • 세션의 경우 모든 인증 정보를 서버에서 관리하기 때문에 보안 측면에서 조금 더 유리하다.
    => 설령 세션 ID가 해커에게 탈취된다고 하더라도, 서버측에서 해당 세션을 무효 처리하면 된다.
    => 토큰의 경우 서버가 트래킹하지 않고, 클라이언트가 모든 인증정보를 가지고 있다.
    따라서 토큰이 한번 해커에게 탈취되면 해당 토큰이 만료되지 전까지는 속수무책으로 피해를 입을 수 밖에 없다.
    * JWT 토큰의 보안 문제 때문에 Access Token과 Refresh Token을 사용해서 보안 대책을 세우는 방법도 있다.
    (리프레쉬 토큰 관련 참고링크로 이동)

 

 

JWT가 여러 문제가 있음에도 사용하는 이유

  • 확장성이 좋다.
    일반적으로 웹 애플리케이션의 서버 확장 방식은 수평 확장을 사용한다. 즉, 한대가 아닌 여러대의 서버가 요청을 처리하게 된다. 이때 별도의 작업을 해주지 않는다면, 세션 기반 인증 방식은 세션 불일치 문제를 겪게 된다.
    이를 해결하기 위해서 Sticky Session, Session Clustering, 세션 스토리지 외부 분리 등의 작업을 해주어야 한다.

    하지만, 토큰 기반 인증 방식의 경우 서버가 직접 인증 방식을 저장하지 않고, 클라이언트가 저장하는 방식을 취하기 때문에 이런 세션 불일치 문제로부터 자유롭다. 이런 특징으로 토큰 기반 인증 방식은 HTTP의 비상태성(Stateless)를 그대로 활용 할 수 있고, 따라서 높은 확장성을 가질 수 있다.

  • 서버의 부담이 줄어든다.
    세션 기반 인증은 서비스가 세션 데이터를 직접 저장하고 관리하지만 토큰 인증 방식은 클라이언트가 인증 데이터를 직접 가지고 있다. 따라서 유저의 수가 얼마나 되던 서버의 부담이 증가하지 않는다.

 

 

❓ Authorization header란

Authorization 헤더는 인증 토큰(JWT든, Bearer 토큰이든)을 서버로 보낼 때 사용하는 헤더입니다.

API 요청같은 것을 할 때 토큰이 없으면 거절당하기 때문에 이 때 Authorization을 사용하면 됩니다.

 

 

 

 

❓ 헤더에 Bearer을 적는 이유

JWT 혹은 OAuth에 대한 토큰을 인증 타입을 명시하는 것이다.(RFC 6750)

결국 인증 시스템 구축을 위한 약속된 틀입니다.

다른 키워드를 넣어서 개발 할 수는 있지만 약속된 틀을 깨는 것이기 때문에 차 후에 문제가 발생할 수 있습니다.

반응형

+ Recent posts