1. 인증 방식

 1) Session & Cookie 인증 방식

 

  • 사용자가 로그인 요청을 보내면 사용자를 확인 후 Session ID를 발급한다
  • 발급한 ID를 이용해 다른 요청과 응답을 처리하는 방식이다
  • 쿠키 인증은 쿠키에 사용자 정보를 담아 서버로 보내게 되는데 HTTP 방식 통신을 사용하는 경우 정보가 유출되기 쉽다
  • 세션 인증은 세션 ID를 보내므로 쿠키에 비해 보안성이 높지만 서버에서 처리를 해야하기 때문에 추가적인 데이터베이스 공간이 필요하므로 점점 커지면 부담이 될 수 있다

 

 2) Token 인증 방식

 

  • 저장소의 필요 없이 로그인 시 토큰을 발급한다
  • 데이터 요청 시에 발급받은 토큰을 헤더를 통해 전달하여 응답 받는 방식이다
  • 쿠키나 세션을 이용한 인증보다 보안성이 강하고 효율적인 인증 방법이다
  • 토큰은 데이터가 인코딩 되어 있어 누구나 디코딩하여 데이터가 유출될 수 있지만 서명 필드가 헤더와 페이로드를 통해 만들어져 데이터 변조 후 재전송을 막을 수 있다
  • statelsess 서버를 만들 수 있다
  • 인증정보를 OAuth로 이용할 수 있다
  • 일반적으로 토큰은 요청 헤더의 Authorization 필드에 담겨져 보내지게 된다
요청 헤더의 Authorization 필드 구조
Authorization: <type> <credentials>

 

 

2. Bearer 인증 테스트

  • Bearer 인증은 JWT or OAuth에 대한 토큰을 사용한다
  • 기초 환경 설정 코드를 작성한 token 폴더를 오픈한다

 1) Filter 생성

  • filter 패키지를 만들고 FirstFilter 클래스를 생성한다
package com.json.token.filter;

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

public class FirstFilter implements Filter {

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

 

 

  • SecurityConfig 클래스에 코드를 추가한다
    - addFilterBefore() 또는 addFilterAfter()를 사용해서 특정 필터 전/후로 적용될 수 있게 한다
    http.addFilterBefore(new FirstFilter(), BasicAuthenticationFilter.class); // 추가

 

 2) Token 적용

  • FirstFilter 클래스에 코드를 추가한다
    - HttpServletRequest
     : ServletRequest를 상속한다
     : Http 프로토콜의 request 정보를 서블릿에 전달하기 위한 목적으로 사용한다
     : Header 정보, Parameter, cookie, URI, URL 등의 정보를 읽어들이는 메서드를 가진 클래스이다
     : Body의 Stream을 읽어들이는 메서드를 가지고 있다

    - HttpServletResponse
     : ServletResponse를 상속한다
     : Servlet이 HttpServletResponse 객체에 Content Type, 응답코드, 응답 메세지 등을 담아서 전송한다

    - HttpServlerRequest, HttpServletResponse 는 http 요청을 할 때 요청 정보가 해당 객체에 있기 때문에 사용한다
package com.json.token.filter;

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

public class FirstFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        //System.out.println("FirstFilter");

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        res.setCharacterEncoding("UTF-8");
        if(req.getMethod().equals("POST")) {
            String headerAuth = req.getHeader("Authorization");

            if(headerAuth.equals("codestates")) {
                chain.doFilter(req, res);
            } else {
                PrintWriter writer = res.getWriter();
                writer.println("인증 실패");
            }
        }
    }
}

 

  • POST 테스트를 위해 RestApiController 클래스에 코드를 추가한다
    @PostMapping("/token")
    public String token() {
        return "<h1>token</h1>";
    }

 

3. 테스트

 1) postman 에 http://localhost:8080/token 으로 POST 요청을 한다

  • FirstFilter 클래스에 작성한 KEY : Authorization 와 VALUE : codestates 를 입력하고 전송한다
  • 'token' 값을 전송받는다

 2) postman 에 http://localhost:8080/token 으로 POST 요청을 한다

  • 임의의 KEY : Authorization 와 VALUE : kim 를 입력하고 전송한다
  • 허용하지 않은 값으로 Authorization이 올 경우에는 '인증 실패' 값을 전송받는다

 

4. 토큰 처리

  • ID, Password가 정상적으로 들어와 로그인이 완료되면 토큰을 만들어주고 응답으로 넘겨 준다
  • 이후 요청이 올 때 header에 Authorization에 있는 토큰 값만 가져와서 확인한다
  • 넘어온 토큰이 우리가 발행한 토큰이 맞는지만 검증하면 된다

 

※ 참조 링크 

▶ 필터 이해하기 : https://atin.tistory.com/590

 

[Spring Security] 필터 Filter, SecurityFilterChain 이해하기

Spring Security를 커스터마이징하기 위해서는 그리고 이해하기 위해서는 아래 필터 체인을 이해하는 것이 좋다. 아래 그림은 인터넷에 돌아다니는 Spring Security 호출 그림을 내가 다시 깔끔하게 그

atin.tistory.com

 

1. 토큰(Token)

  • 유저의 정보를 암호화한 상태로 저장한 파일이다
  • 암호화 되어 있어서 클라이언트에 저장하여도 보안이 형성될 수 있다

 1) 토큰기반 사용 목적

  • 세션기반 인증은 서버나 DB에 유저의 정보를 담는 방식이다
  • 정보를 요청할 때마다 세션을 생성해서 DB에 일치 여부를 확인한 후 정보를 전송한다
  • 이러한 불편함을 간소화 하기 위하여 토큰기반 인증을 사용한다
  • 대표적인 토큰기반 인증방식으로는 JWT(JSON Web Token)이 있다

 

2. JWT(JSON Web Token)

 1) JWT 구성 및 진행 절차

  • Json 포맷으로 사용자에 대한 속성을 저장하는 웹 토큰이다
  • Json 객체를 basse64 방식으로 인코딩하면 아래와 같이 토큰이 생성된다
    - Header, Payload, Signature 총 3개의 블록으로 구성되어 있다

JWT 구성

  • HMAC SHA256 알고리즘(암호화 방법 중 하나)을 사용한다면 Signature는 아래와 같은 방식으로 생성된다
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
  • 토큰기반의 인증 절차
    - 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보낸다
    - 서버는 아이디/비밀번호가 일치하는지 확인하고, 클라이언트에게 보낼 암호화 된 토큰을 생성한다
      : 액세스 / 리프레시 토큰을 모두 생성한다
      : 토큰에 담길 정보(payload)는 유저를 식별할 정보, 권한이 부여된 카테고리(사진, 연락처, 기타 등등)가 될 수 있다
      : 두 종류의 토큰이 같은 정보를 담을 필요는 없다
    - 토큰을 클라이언트에게 보내주면, 클라이언트는 토큰을 저장한다
      : 저장하는 위치는 Local Storage, Session Storage, Cookie 등 다양하다
    - 클라이언트가 HTTP 헤더(Authorization 헤더) 또는 쿠키에 토큰을 담아 서버에 보낸다
      : bearer authentication을 이용한다
    - 서버는 토큰을 해독하여 발급해 준 토큰이 맞으면, 클라이언트의 요청을 처리한 후 응답을 보내준다

 

 

 2) JWT 종류

  • 액세스 토큰(Acess Token)
    - 보호된 정보들(유저의 이메일, 연락처, 사진 등)에 접근할 수 있는 권한부여에 사용한다
    - 클라이언트가 처음 인증을 받게 될 때(로그인 시), 액세스 토큰과 리프레시 토큰 두 가지를 모두 받는다
    - 실제로 권한을 얻는 데 사용하는 토큰은 액세스 토큰이다
    - 액세스 토큰은 리프레시 토큰에 비해 탈취에 대비하여 짧은 유효기간을 가지고 있다
    - 유효기간 만료시에는 리프레시 토큰을 이용하여 신규 발급을 받는다
    - 신규 발급 후에는 별도의 로그인은 필요없다
  • 리프레시 토큰(Refresh Token)
    - 클라이언트가 처음 인증을 받게 될 때(로그인 시), 액세스 토큰과 리프레시 토큰 두 가지를 모두 받는다
    - 액세스 토큰에 비해 유효기간이 길다
    - 리프레시 토큰을 사용하지 않는 경우가 많다

 

 3) JWT 기반 인증의 장점

  • Statelessness & Scalability (무상태성 & 확장성)
    - 서버는 토큰의 신뢰성만 확인하면 되고 클라이언트의 정보를 저장하지 않아도 된다
    - 토큰을 헤더에 추가하는 것으로 인증절차를 완료한다
    - 하나의 토큰으로 여러 개의 서버에 사용이 가능하다
  • 안정성
    - 암호화 한 토큰을 사용하므로 안전하다
    - 함호화 키를 노출할 필요가 없다
  • 생성의 자율성
    - 토큰을 생성하는 서버가 토큰을 만들지 않아도 사용이 가능하다
    - 토큰만을 생성하는 서버나 외부 서버에서 생성한 토큰을 사용할 수 있다
  • 권한 부여에 용이
    - 토큰의 payload 안에 접근 가능한 정보의 권한을 지정할 수 있다
    - 연락처만 가능, 갤러리와 포토만 가능 등...

 

 4) JWT 기반 인증의 단점

  • Payload는 해독할 수 있다
    - Payload는 base64로 인코딩 된다
    - 토큰을 탈취하여 Payload를 해독하면 토큰 생성시 저장한 데이터를 확인할 수 있다
    - Payload에는 중요한 정보를 넣지 않도록 한다
  • 토큰의 길이가 길어지면 네트워크에 부하를 줄 수 있다
    - 토큰에 저장하는 정보의 양이 많아질 수록 토큰의 길이는 길어진다
    - 요청할 때마다 길이가 긴 토큰을 함께 전송하면 네트워크에 부하를 줄 수 있다
  • 토큰은 자동으로 삭제되지 않는다
    - JWT는 상태를 저장하지 않기 때문에 한 번 생성된 토큰은 자동으로 삭제되지 않는다
    - 토큰 만료 시간을 반드시 추가해야 한다
    - 토큰이 탈취된 경우 토큰의 기한이 만료될 때까지 대처가 불가능하므로 만료 시간을 너무 길게 설정하지 않는다
  • 토큰은 어딘가에 저장되어야 한다
    - 토큰은 클라이언트가 인증이 필요한 요청을 보낼 경우에 함께 전송할 수 있도록 저장되어 있어야 한다

 

3. JWT 인증 환경 구성

 1) 프로젝트 생성

 

 2) 환경 설정

  • Certificate > token > src 의 buiold.gradle 의 코드 구성을 확인한다
plugins {
	id 'org.springframework.boot' version '2.7.2'
	id 'io.spring.dependency-management' version '1.0.12.RELEASE'
	id 'java'
}

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

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	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 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
	useJUnitPlatform()
}
 
 
  • buiold.gradle에 jwt 의존성을 추가해 준다
    - gradle은 추가 작업 후에는 항상 재실행을 해 준다
implementation 'com.auth0:java-jwt:3.19.2'

 

  • Certificate > token > src > main > resources 에 H2 서버 구성을 위하여 application.yml을 추가한다
spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
 
 
  • Certificate > token > src > main > java > com.json.token  아래에 controller 패키지를 만들고 RestApiController 클래스를 생성한다
package com.json.token.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RestApiController {

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

 

  • 프로젝트 실행 및 웹 접속을 테스트 한다
localhost:8080/login localhost:8080/home
  
 
 
 3) JWT - security 설정
  • Certificate > token > src > main > java > com.json.token  아래에 model 패키지와 Member.java 를 생성한다
    -
    getRoleList는 역활을 ,로 구분하여 여러개를 넣기 때문에 사용한다
    - Role 모델을 추가하여 getRoleList를 대체할 수 있다
package com.json.token.model;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Data
@Entity
public class Member {

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

    public List<String> getRoleList() {
        if(this.roles.length() > 0) {
            return Arrays.asList(this.roles.split(","));
        }
        return new ArrayList<>();
    }
}
 
 
  • Certificate > token > src > main > java > com.json.token  아래에 config 패키지를 만들고 SecurityConfig 클래스를 생성한다
    - JWT를 사용하기 위한 기본 설정
     : JWT는 headers에 Authorization 값에 토큰을 보내는 방식이다. (⇒ Bearer)
     : 토큰 정보는 노출되면 안되지만 노출되게 되더라도 유효 시간을 지정해뒀기 때문에 큰 위험이 없다 : 

    - .http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
     : Web은 기본적으로 stateless인데 seesion이나 cookie를 사용할 수 있다
     : session / cookie를 만들지 않고 STATELESS로 진행하겠다는 의미이다


    - .formLogin().disable()
     : form Login을 사용하지 않는다

    - .httpBasic().disable()
     : http 통신을 할 때 headers에 Authorization 값을 ID, Password를 입력하는 방식이다
     : https를 사용하면 ID와 Password가 암호화되어 전달된다
     : http 로그인 방식을 사용하지 않는다
package com.json.token.config;

import org.springframework.context.annotation.Bean;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().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();
    }
}
 
 
  •  config 패키지에 CorsConfig 글래스를 생성한다
    - .setAllowCredentials()
     : 서버가 응답할 때 json을 자바스크립트에서 처리할 수 있게 설정해 준다

    - .addAllowedOrigin(”*”)
     : 모든 ip에 응답을 허용한다

    - .addAllowedHeader(”*”)
     : 모든 header에 응답을 허용한다

    - .addAllowedMethod(”*”)
     : 모든 post, get, patch, delete 요청을 허용한다
package com.json.token.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 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);
    }
}
 
 
  • SecurityConfig 클래스에 코드를 추가한다
@RequiredArgsConstructor //@ 추가

public class SecurityConfig {

    private final CorsFilter corsFilter; //코드 추가

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()

                .addFilter(corsFilter) // 추가
                
                .formLogin().disable()
 
 
  • 프로젝트를 재실행 후 웹에 접속한다
    - localhost:8080 , localhost:8080/login 등... 접속은 404 에러가 발생한다

      - localhost:8080/home 는 로그인 없이 접속된다

      - http://localhost:8080/api/v1/user, http://localhost:8080/api/v1/manager, http://localhost:8080/api/v1/admin 는 403에러가 발생한다

http://localhost:8080/api/v1/user http://localhost:8080/api/v1/manager http://localhost:8080/api/v1/admin

 

 

 

※ 참조 링크

▶ JWT : https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 Bearer token : https://learning.postman.com/docs/sending-requests/authorization/#bearer-token

 

Authorizing requests | Postman Learning Center

Authorizing requests: documentation for Postman, the collaboration platform for API development. Create better APIs—faster.

learning.postman.com

https://datatracker.ietf.org/doc/html/rfc6750

 

RFC 6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage

 

datatracker.ietf.org

 

 

+ Recent posts