Spring Boot 3 & Spring Framework 6 - Section 17 : 풀스택 Application 실습 #3 - Spring Security 구현 및 JWT Token

이정수·2025년 3월 5일
0
  • Spring Security를 이용하여 Application에서 사전에 구축한 REST APIAuthentication 적용
    。Application에 Authentication을 구현한 이후에는 JWT를 사용하여 인증을 수행한 후 REST API를 사용.
    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    • 개발용 자격증명의 Username과 Password 변경
      application.properties에 다음 구문을 추가.
    spring.security.user.name=user123
    spring.security.user.password=pw456

    。해당 구문 추가 후 로그인 시 변경된 개발용 자격증명에 로그인이 가능.

    • SecurityFilterChain의 전반적인 Configuration 설정. SecurityFilterChain
      @Configuration를 선언한 Class에서 HttpSecurity instance를 매개변수로하여 SecurityFilterChain type의 Spring Bean instance를 생성 및 return하는 @Bean method를 구현.
      HttpSecurity instance에관련 Configuration을 설정 후 HttpSecurity객체.build()SecurityFilterChain instance를 생성
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.Customizer;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.web.SecurityFilterChain;
    @Configuration
    public class pgSecurityConfiguration {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            // 모든 HTTP Request 중 인증된 HTTP Request에 대해 승인처리 설정
           return http.authorizeHttpRequests(
                    auth->auth.anyRequest().authenticated()
            )
            // 인증되지않은 HTTP Request에 대해 Authorization Header를 요구.
            .httpBasic(Customizer.withDefaults())
            // Session을 stateless 상태
            .sessionManagement(
                    session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // CSRF 비활성화
            .csrf(csrf->csrf.disable())
            // SecurityFilterChain instance 생성
             .build();
        }
    }

    HttpSecurity instance에 대해 일일이 설정하지않고, 연쇄적으로 설정을 구현하여 return.

    인증이 된 모든 Http Request에 대해 승인
    Spring Application에 전달되는 모든 HTTP Request에 대해서 인증된 HTTP Request에 대해서만 승인.

    인증되지않은 HTTP Request에 대해 Authorization Header를 요구
    Authorization Header 요구 시 Basic Authentication 도출.
    Basic Authentication : 웹브라우저의 인증 팝업으로 Authorization HeaderID/PW 입력 기능 제공.

    Stateless Session 설정.
    Spring SecuritySession을 사용하지 않도록 Session Policy 설정.
    주로 JWT Token을 활용하는 REST API Authentication 구현 시 사용.

    CSRF protection 비활성화 설정
    Session이 존재하지 않는 경우( Stateless )에만 비활성화하며, Session이 존재하는 경우, 반드시 CSRF protection을 활성화해야한다.
    ▶ Spring Security에서는 default로 CSRF protection 활성화로 설정되어있음.



  • React에서 Spring ApplicationSpring Security로 보호된 REST API를 호출하도록 설정하기
    Front-End Application에서 API 호출 시 전송하는 HTTP RequestSpring에 접근할 수 있는 Authorization Header를 포함하도록 설정.

    React Application에서 호출하는 REST APISpring Security에 의해 보호되어있음.
    React Application에서 해당 REST API를 호출할 수 있도록 설정
    • API를 호출하는 Axios instance의 propertyAuthorization header를 포함하기 Axios
      axios객체.get("URL" , { property })
      { property }기본 URL , Header , timeout 등을 지정가능하며, 이는 사전에 axios.create({ property })를 통해 axios instance를 생성 활용 가능.
    const apiClient = axios.create({
      baseURL : "http://localhost:8080",
      // axios instance에 기본 Header 설정
      // axios instance를 이용해 API 호출시 해당 Header가 항상 포함.
      headers: {                         
        Authorization: "Basic dXNlcjEyMzpwdzQ1Ng==",
      }
    })
    export const retrieveBean = (pathvariable) => apiClient.get(`/pg/hello-world-bean/${pathvariable}`);

    headers에 정의된 { } 배열의 Authorization은 인코딩된 형태의 Spring Security에서 등록된 ID와 PW를 기입.
    headers : HTTP Requestheader 객체.
    Accept , Authorization , Content-Type , Origin 등의 HTTP Header를 포함
    Authorization : JWT Token , HTTP Basic Auth를 추가 시 사용.

    • Preflight RequestOPTIONS HTTP Method에 의해 발생하는 Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource 오류 해결



      API호출 시 검사 - Network 에서 OPTIONS Request MethodPreflight Request가 우선적으로 전송되는것을 확인 할 수 있으며, 이후 실제 HTTP Request가 전송되는것을 확인 가능.
      ▶ 해당 Preflight RequestOPTIONS HTTP Methodaccess control check를 통과하지 못해 오류가 발생.

    • Spring Application에서 SecurityFilterChain을 구현하는 @Bean MethodHttpSecurity instance 에서 모든 Client에게 OPTIONS HTTP Request에 대한 Access를 허용하도록 설정
    http.authorizeHttpRequests(
                    auth->auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                            .anyRequest().authenticated()
            )
    • 모든 URL PatternOPTIONS HTTP Method 방식의 인증된 HTTP Request는 모두 허용.
      ▶ 구현 시 Preflight RequestOPTIONS HTTP Method가 접근허용 설정되어 오류가 발생하지 않는다.
      • Preflight Request :
        。브라우저가 CORS Policy에 따라 Server에 실제 HTTP Request를 전달하기 전 사전에 확인용도로 OPTIONS Request Method를 사용하여 전달하는 Request. CORS
        Preflight RequestCORS Policy가 적용될때, 특정 조건을 만족 시 자동으로 발생.

        Preflight Request 발생 조건
        。다음 조건을 만족하는 경우 Preflight Request 발생.

        • Cross-Origin :
          。다른 URL의 출처( Origin )으로 요청 시 발생.

        • Simple Request에 해당하지 않는 경우.
          PUT , DELETE 등의 HTTP Method 또는 Authorization Header를 포함 시 Preflight Request 발생.

          Simple Request :
          GET, HEAD , POSTHTTP Method만 사용.
          Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나
          。Custom Header ( Authorization , X-Custom-Header )가 없는 경우


      • OPTIONS Request Method
        Server의 특정 Resource에서 허용하는 HTTP Method를 물어보는 용도의 Request.
        Client에서 해당 resource URL에서 사용할 수 있는 HTTP Method가 무엇인지 묻는 용도로 활용됨.

      • Access Control Check :
        。Client가 Application의 특정 Resource에 접근할 수 있는지를 확인하는 보안절차.
        ▶ 보안정책을 준수하고 권한이 없는 Client로부터의 Access를 차단하는 중요한 역할을 수행.


    • React Application에서 ID와 PW 입력 시 Spring에 Access가 가능한 유효한 자격증명인지 확인 후 인코딩된 형태의 Token을 생성
      React에서 ID , PW를 입력 후 Form 제출 시 생성된 TokenSpringAuthentication TEST 용도의 API로 HTTP Request에 첨부하여 전송하여 Spring에 Access가 가능한지 확인 후 React에서 활용.
      Authentication이 끝난 TokenReact Context에 보관하여 REST API 호출 시마다 Context로부터 Token을 가져오도록 설정.

      。기존의 HeadersAuthorization 하드코딩된 Token을 로그인 시 생성되는 Token으로 대체.

      。해당 과정은 HTTP Basic Authentication 방식을 사용하여 Base64로 생성한 Base64코드를 Token으로 임시활용.
      HTTP Basic Authentication는 보안성이 존재하지 않으므로, 사용 X
      • Spring에서 Authentication TEST 용도의 API 생성
        React의 로그인 시 생성된 Token을 통해 API를 호출하여 HTTP Requestheaders : { Authorization : "인코딩Token" }에 포함된 Token이 유효한지 확인하는 용도
      // Authentication 용도의 API
          @GetMapping(path="/basicauth")
          public String basicAuthCheck(){
              return "Success";
          }   

      。 생성된 Token을 포함한 HTTP Request를 전송하여 API호출 시 HTTP Response 200을 받을 경우, Token은 유효.
      ▶ 틀린 ID, PW를 입력하여 생성한 Token을 포함하여 전송 시 HTTP Response 401가 반환되어 Token이 유효하지 않은것을 확인 가능.
      401 : Resource에 유효한 자격증명이 없기 때문에 HTTP Request가 적용되지 않았음.

      • SpringAuthentication TEST 용도의 API호출을 위한 Axios method 생성
        React ContextAuthContext.js에서 로그인 후 생성된 Token을 전달받아서 Authorization Header로 포함하여 SpringAuthentication TEST 용도의 API를 호출.
       export const testApi = (token) => apiClient.get('/basicauth',{
        headers : {
          Authorization : token,
        }
      })

      tokenAxios HTTP Method 매개변수의 property로 Authentication Header를 설정

      • 기존 하드코딩 로그인 방식 대신 API를 활용해 Token 목적의 HTTP Basic Authentication를 통한 Base64코드를 생성하는 방식의 Authentication 기능을 구현하여 로그인하는 함수 작성하기
        React ContextAuthContext.js에서 로그인 후 usernamepassword를 전달받아서 Base64코드를 생성 후 Authentication Header로서 HTTP Request에 포함하여 SpringAuthentication TEST 용도의 API로 전송하여 HTTP Response 200을 받을 경우 해당 Token이 유효하다고 간주하여 React Context에 저장.

        。현재 과정에서 ID:PWBase64로 인코딩하여 앞에 Basic을 명시한 Base64코드 ( Basic Base64코드 )를 생성하여 Authorization Header에 포함하는 Basic Authentication 사용.
        ▶ 차후 JWT Token을 활용.

        Axios HTTP Method의 경우 Promise를 return하며 PromiseCallback Method가 전부 실행완료되기전에 비동기로 다음 구문을 실행하는 특징을 지님.
        ▶ 함수에 async를 선언 후 await을 사용하여 Callback Method를 모두 실행 한 후 다음 구문을 전개하도록 설정. Javascript - async/await

        변수 = await Promise인 경우, promise가 resolve되었을 때 값이 변수에 저장됨
        HTTP Response가 변수에 저장된다.
        // AuthContext.js
      const [token, setToken] = useState(null)
      // await을 사용할 수 있도록 함수에 async를 선언.
      async function logIn(username, password){
          // Base64로 인코딩된 ID와 PW 생성
          // Basic Base64코드 로 전달.
          const baCode = 'Basic ' + window.btoa(username + ':' + password)
          // try ~ catch 구문으로 오류발생 시 false를 반환하도록 설정.
          try{
                // 비동기를 통해 다음 구문을 실행하기 전에 Callback Method를 모두 실행하도록 await 선언. 
                // Axios HTTP Method를 수행할 경우 HTTP Resopnse를 return하여 response.status로 상태코드 조회 가능.
                const response = await testAuthenticateApi(baCode)
                if (response.status == 200){
                  setAuthState(true)
                  setUsername(username)
                  // 로그인이 된 경우 Context에 Token 저장
                  setToken(baCode)
                  return true
                } else {
                  // 로그인 실패 시 WET코드를 방지하기위해 구현된 Logoutfunction() 사용.
                  Logoutfunction()
                  return false
          }
          } catch{
            // 에러 발생시 구현된 Logoutfunction() 사용.
            Logoutfunction()
            return false
          } 
        }
        // Logout Logic 구현
        // 로그인에 실패하거나 로그아웃하는 경우우 Context의 Token을 Null로 설정
        function Logoutfunction(){
          setAuthState(false);
          setToken(null)
          setUsername(null)
        }
        return( 
          <AuthContext.Provider value={ {AuthState,logIn,Logoutfunction,username,token}}>
            {children}
          </AuthContext.Provider>
        )

      。입력한 ID와 PW를 통해 Base64로 생성한 TokenAuthentication Header로 포함하여 API 호출 후 return된 HTTP ResponseStatus Code200일때, 로그인이 되도록 구현.

      。추가로 try~catch 구문을 설정하여 오류 발생 시 false를 반환하도록 설정

      。로그인에 성공한 경우, 해당 유효한 TokenReact ContextToken 저장용도의 Global State로서 저장하며 로그인에 실패한 경우 Global StateNull로 설정하며 Logout 기능을 수행하는 Logoutfunction()에도 Token 저장용도의 Global StateNull로 설정.
      Context전역State로 저장된 Token<Context객체.Provider>을 통해 다른 Component로 전달하도록 설정.

      ▶ 로그인에 실패하는 경우, Context의 저장된 TokenNULL로 설정되어 검사- Network - Fetch/XHRHTTP Request에서 Authentication Header로 설정된 Token이 노출되지 않는다.

      • 해당 logIn 함수를 호출하고 있는 LoginComponent.jsxlogInStatus 함수에도 async / await을 선언
      async function logInStatus(){
          if (await AuthContext.logIn(username,password)){
            setShowSuccess(true)
            navigate(`/welcome/${username}`)   
          } else {
            setShowSuccess(false)
          }
        }


      Spring Application에서 등록된 ID와 PW를 입력 시 Base64로 인코딩된 유효한 Base64코드 ( Basic Base64코드 )이 생성된 후 Axios HTTP Method에서 Authentication Header로서 포함되어 Spirng Authentication TEST 용도의 API를 호출하여 HTTP Response 200 반환.

      검사- Network - Fetch/XHR에서 HTTP RequestURL Parameter( = basicauth )의 Headers를 확인할 경우 다음처럼 Authorization HeaderReact에서 전송한 Base64코드가 포함되어있음을 확인 가능.
      Fetch/XHR ( Fecth / XMLHttpRequest ) : Front-End Application에서 전송한 비동기 HTTP Request을 표시.

      。 잘못된 ID와 PW를 입력 시 유효하지 않은 Base64코드이 생성 및 전달되면서 HTTP Response 401 반환.
      401 : Resource에 유효한 자격증명이 없기 때문에 HTTP Request가 적용되지 않았음.

      • 최종적으로 Spring Application에서 등록한 ID와 PW를 입력하여 Base64로 생성한 유효한 Base64코드을 생성 후 Authentication Header로 포함하여 API 호출 후 return된 HTTP ResponseStatus Code200일때 로그인에 성공하면서 다음 Welcome Component를 반환.
        • Axios HTTP Method가 Return하는 Promiseasync/await 적용
          Axios HTTP Method의 경우 Promise를 return하며 PromiseCallback Method가 전부 실행완료되기전에 비동기로 다음 구문을 실행하는 특징을 지님.
        const baCode = 'Basic ' + window.btoa(username + ':' + password) 
            testAuthenticateApi(baCode)
            .then((response)=>{console.log("HTTP Response : "+response)}) // 
            .catch((error)=>{console.log(error)})
            .finally(()=>{console.log("실행완료")})
            console.log("Next Code")


        Promise의 비동기( AJAX ) 특징으로 Axios HTTP Method보다 다음 구문인 console.log("Next Code")를 먼저 실행된 후 then - finally 순으로 실행됨을 확인 가능.
        ▶ 함수에 async를 선언 후 await을 사용하여 Callback Method를 모두 실행 한 후 다음 구문을 전개하도록 설정.

        // await을 사용할 수 있도록 함수에 async를 선언.
        async function logIn(username, password){
            const baCode = 'Basic ' + window.btoa(username + ':' + password)
            // 비동기를 통해 다음 구문을 실행하기 전에 Callback Method를 모두 실행하도록 await 선언. 
            await testAuthenticateApi(baCode)
            .then((response)=>{console.log("HTTP Response : "+response)})
            .catch((error)=>{console.log(error)})
            .finally(()=>{console.log("실행완료")})
            console.log("Next Code")
          }



        • HTTP Basic Authentication
          HTTP에서 가장 간단한 Authentication 방식.
          ID:PWBase64로 인코딩하여 앞에 Basic을 명시한 Base64코드 ( Basic Base64코드 )을 HTTP RequestAuthorization Header로 포함하여 Authentication을 수행.

          window.btoa(id + ":" + pw)ID : PWBase64로 인코딩한 Base64코드를 생성한다.

          Base64 인코딩만 사용하여 암호화가 되지않아서 보안에 매우 취약.
          TLS/SSL을 사용하여 보안을 강화.

          session을 사용하지 않는 stateless 특징으로 HTTP Request마다 Authentication 정보를 포함해야한다.
        const baCode = 'Basic ' + window.btoa(username + ':' + password)
        testAuthenticateApi(baCode)
            .then((response)=>{console.log(response)})
            .catch((error)=>{console.log(error)})
            .finally(()=>{console.log("실행완료")})   
        export const testAuthenticateApi = (token) => apiClient.get('/basicauth',{
          headers : {
            Authorization : token,
          }
        })
        • base64
          Binary DataText format으로 변환하는 Encoding 방식
          데이터( JSON , XML , Binary Data 등 ) 전송에 특화.
          。단순 Encoding 방식이므로, 보안성이 없어 암호화 목적으로는 사용하지 않음.
          양방향변환으로서 Base64Encoding한 데이터는 다시 Decoding하면서 원본으로 복원 할 수 있다.

          Base64 활용

          • image, file Data의 binary dataTextEncoding 후 전송하여 다시 binary dataDecoding

          • HTTP Basic Authentication : Basic Base64인코딩ID:PW
            JavaScript에서 생성 시 window.btoa(문자열) 사용. ( Binary to ACSII )
            const ba = 'Basic ' + window.btoa(ID + ':' + PW)
            ▶ 현재는 JWT 사용.


        • Hashing
          。Data를 고정된 길이의 고유한 Hash로 변환하는 과정으로 복원이 불가능한 단방향 변환.
          주로 보안 및 무결성 검증에 사용

          단방향변환으로 원본 입력값으로 복원이 불가능
          。입력값 크기에 관계없이 고정된 길이의 Hash 생성. ( SHA-256 : 64자 )
          。충돌방지를 예방하기 위해 서로 다른 입력값으로 Hasing동일한 Hash를 갖지 않도록 설계.
          Bcrypt , SHA-256을 통한 암호화 알고리즘을 사용하여 보안성 존재.
          Spring Security - BCrypt hashing 활용

          Hashing 활용

          • 비밀번호 저장
            。비밀번호를 Hashing하여 Hash로 저장하여 로그인 시 입력된 비밀번호를 Hashing하여 비교하는 비교대상으로 사용.

          • 데이터 무결성 검증
            。파일이 변조되었는지 확인하는 용도로 사용

          • 디지털 서명 및 인증서


      • React Context에서 Global State로 저장된 유효한 Token을 활용하도록 Refactoring
        。각각의 Axios HTTP MethodpropertyAuthentication header에 중복적으로 Token을 설정하는 방식이 아닌, 특정 Axios instance가 API호출 수행 시 HTTP Request를 Intercepting하여 Authentication headerToken을 설정.
        • axios instance를 생성하는 axios.create() 함수를 정의하는 JS파일( = apiClient.js )을 생성.
              //apiClient.js
        import axios from 'axios'
        export const apiClient = axios.create({
          baseURL : "http://localhost:8080"
        })

        。 해당 함수를 통해 생성된 Axios instance를 통해 로그인 시 Axios Interceptor를 활용해서 인증된 TokenAuthorization Header로 추가하는 기능을 구현.
        。기존 Axios HTTP Method에서 해당 함수를 import하여 생성된 Axios instance를 사용하는걸로 refactoring을 수행.

        // TodoApiService.js
        import axios from 'axios'
        // Axios instance를 생성하여 반환하는는 함수를 import.
        import { apiClient } from './apiClient';
        export const retrieveDataApi = (username) => apiClient.get(`/pg/jpa/users/${username}/posts`);
        export const deleteDataByIdApi = (id) => apiClient.delete(`/pg/jpa/posts/${id}`); 
        export const retrievePostByPostIdApi = (username,id) => apiClient.get(`/pg/jpa/users/${username}/posts/${id}`)
        // todo가 HTTP Request Body로서 API 호출 수행.
        export const updatePostApi = (username, id, todo)=>apiClient.put(`/pg/jpa/users/${username}/posts/${id}` , todo)
        export const createPostApi = (username, todo)=>apiClient.post(`/pg/jpa/users/${username}/posts` , todo);
        export const testAuthenticateApi = (token) => axios.get('http://localhost:8080/basicauth',{
          headers : {
            Authorization : token,
          }
        })
        • 로그인하여 유효한 Token을 얻은 경우 Axios interceptor를 활용하여 API호출 시 Authorization Header를 추가하는 기능 구현하기
          apiClient.jsapiClient 함수를 통해 생성된 axios instaceAxios Request Interceptor를 설정 할 경우, 해당 apiClient를 통해 생성된 axios instance를 이용하여 Axios HTTP Method를 통한 모든 API호출을 Intercepting하여 Authorization Header를 설정.
            // AuthContext.js
        import { apiClient } from '../api/apiClient';
        		const response = await testAuthenticateApi(baToken)
                  if (response.status === 200){
                    setAuthState(true)
                    setUsername(username)
                    setToken(baCode)
            		// apiClient로 생성된 instance의 모든 API호출을 
            		// Intercepting하여 Authentication Header를 설정.
                    apiClient.interceptors.request.use(
                      (config)=>{
                        console.log('intercepting and adding a token')
                        config.headers.Authorization=baCode
                        return config
                      }
                    )
                    return true
                  } else {
                    Logoutfunction()
                    return false
                  }

        await을 통해 Promise에서 성공적으로 반환된 HTTP Responseconst response로 인가.

        apiClient로 생성한 Axios instance를 이용하여 API호출 시 해당 Axios Request InterceptorHTTP Request를 Server로 전송되기전에 Intercepting하여 Configuration객체를 Customize하여 HTTP Requestheaders 객체에 { Authorization : base64코드 } 설정.

        // ApiService.js
        import { apiClient } from './apiClient';
        import axios from 'axios';
        export const retrieveBean = (pathvariable) => axios.get(`/pg/hello-world-bean/${pathvariable}`);
        export const retrieveBean = (pathvariable) => apiClient.get(`/pg/hello-world-bean/${pathvariable}`);



        Axiosaxios instanceAxios HTTP Method를 통해 API호출 시 Authentication header가 존재하지 않아 Error가 발생.
        Axios Interceptor가 구현된 apiClientaxios instance를 통해 호출 시 정상적으로 기능.
        apiClientaxios instance를 통해 호출 시 Axios HTTP MethodProperty에 따로 Authentication header를 정의하지 않고 API호출을 수행하더라도, Axios Request Interceptor가 Intercepting하여 HTTP RequestAuthentication Header에 유효한 Token이 포함되도록 설정

        Postgres_Post 데이터 생성 시 오류가 발생할 경우, Postgres_User에 해당 username의 데이터가 존재하는지 확인.
        username를 참조하여 외래키로 연결되어있으므로 Postgres_User에 로그인한 계정의 데이터가 없는경우 Postgres_Post 생성 시 부모테이블의 기본키가 없으므로 외래키 제약조건 오류 발생.

        apiClient를 이용해 Authentication header없이 API호출을 수행하는 다른 React Component도 매번 API호출을 수행 시 Axios Interceptor가 정상적으로 작동됨을 관측 가능.

        Axios Interceptors
        AxiosAPI호출로 인한 HTTP Request or HTTP Response가 처리되기전에 Intercepting하여 Customize할 수 있는 기능.
        axios instance를 이용하여 Interceptor를 구현할 수 있다.

        。해당 기능을 사용하는 경우 headerToken 추가, Error Handling , HTTP Response 변환 등의 작업을 수행할 수 있다.

        • Request Interceptor : axios객체.interceptors.request
          。특정 Axios instance를 통해 API 호출HTTP RequestServer로 전송되기전에 Intercepting하여 Customize.

          axios객체.interceptors.request.use( callback ) 선언 시 callback의 매개변수로서 특정 Axios HTTP Request를 Intercepting하여 전달되는 Configuration 객체( = config )가 전달됨.
          ▶ 해당 Configuration 객체를 수정 시 Intercepting한 HTTP Request를 전송하기 전에 Configuration을 변경할 수 있다.

          Axios HTTP Request Configuration 객체 구조
          Javascript에서 config.headers.Authorization = token처럼 HTTP Request를 Intercepting하여 config을 통해 수정이 가능.

          {
            "url": "/users",
            "method": "get",
            "headers": {
              "Accept": "application/json, text/plain, */*"
            },
            "baseURL": "https://api.example.com",
            "timeout": 5000
          }
          • url : HTTP Request를 수행할 API의 URL Parameter

          • method : HTTP RequestHTTP Method

          • headers : HTTP Requestheader 객체.
            Accept , Authorization , Content-Type , Origin 등의 HTTP Header를 포함
            Authorization : JWT Token , HTTP Basic Auth를 추가 시 사용.

          • baseURL : 기본 URL

          • timeout : HTTP Request의 요청제한시간

          • params : GET HTTP RequestQuery Parameter

          • data : POST , PUT에서 전달할 데이터.


        • Response Interceptor : axios객체.interceptors.response
          。특정 Axios instance를 통해 API 호출HTTP Response를 전달받을 경우 Callback Method가 실행되기전에 Intercepting하여 Customize.
  • Spring Application에서 JWT Token 활용하도록 설정
    • JWT 구현 시 필요한 설정
      JWT 코드가 작성된 Class는 @SpringBootApplication가 선언된 Spring 진입점 Class의 Component Scan에 의해 식별되기위해 반드시 진입점 Class의 Sub Package에 위치해야한다.


      JWT 용도의 SecurityFilterChain을 새로 구현하게되므로 기존에 구현된 SecurityFilterChainConfiguration Class가 로딩되지 않도록 삭제하거나 @Configuration을 삭제하여 Component Scan에 의해 식별되지 않도록 설정

      Spring Security dependency 정의 시 개발용 자격증명을 임의로 설정하기위해 application.properties에서 작성한 다음 구문을 삭제.
    spring.security.user.name=user123
    spring.security.user.password=pw456
    • JWT사용 시 Spring에서 정의해야할 Dependency

      • spring-boot-starter-oauth2-resource-server
        Spring Boot ApplicationSpring Security와 통합하여 OAuth 2.0 Resource Server로 설정하는 Library.

        OAuth 2.0 Resource Server : Authenticated된 사용자만 접근할 수 있도록 보호된 API를 제공하는 Server.
        ▶ 기존 ApplicationREST API가 노출되어있으므로, ApplicationOAuth 2.0 Resource Server로 설정하여 보호된 REST API를 제공하도록 설정됨.

        。Dependency를 추가 시 OAuth 2.0 Token ( = JWT , Opaque Token 등 )을 검증하여 AuthenticatedHTTP Request만 허용하도록 설정됨.
      <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
      </dependency>
      • spring-boot-configuration-processor
        Spring Boot에서 @ConfigurationProperties가 선언된 Class를 자동으로 문서화를 수행하고 IDE에서 자동완성기능을 제공하는 Library.
        META-INF/spring-configuration-metadata.json 파일을 생성하여 문서를 자동화.
      <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-configuration-processor</artifactId>
      </dependency>


    • JWT Code
      。총 5개의 Class로 구성.
      • JwtTokenService
      import java.time.Instant;
      import java.time.temporal.ChronoUnit;
      import java.util.stream.Collectors;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.oauth2.jwt.JwtClaimsSet;
      import org.springframework.security.oauth2.jwt.JwtEncoder;
      import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
      import org.springframework.stereotype.Service;
      @Service
      public class JwtTokenService {
          private final JwtEncoder jwtEncoder;
          public JwtTokenService(JwtEncoder jwtEncoder) {
              this.jwtEncoder = jwtEncoder;
          }
          public String generateToken(Authentication authentication) {
              var scope = authentication
                      .getAuthorities()
                      .stream()
                      .map(GrantedAuthority::getAuthority)
                      .collect(Collectors.joining(" "));
              var claims = JwtClaimsSet.builder()
                      .issuer("self")
                      .issuedAt(Instant.now())
                      .expiresAt(Instant.now().plus(90, ChronoUnit.MINUTES))
                      .subject(authentication.getName())
                      .claim("scope", scope)
                      .build();
              return this.jwtEncoder
                      .encode(JwtEncoderParameters.from(claims))
                      .getTokenValue();
          }
      }
      • JwtTokenResponse
      public record JwtTokenResponse(String token) {}
      • JwtTokenRequest
      public record JwtTokenRequest(String username, String password) {}
      • JwtSecurityConfig
      import java.security.KeyPair;
      import java.security.KeyPairGenerator;
      import java.security.interfaces.RSAPrivateKey;
      import java.security.interfaces.RSAPublicKey;
      import java.util.UUID;
      import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.http.HttpMethod;
      import org.springframework.security.authentication.AuthenticationManager;
      import org.springframework.security.authentication.ProviderManager;
      import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
      import org.springframework.security.config.Customizer;
      import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
      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.configurers.AbstractHttpConfigurer;
      import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
      import org.springframework.security.config.http.SessionCreationPolicy;
      import org.springframework.security.core.userdetails.User;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.oauth2.jwt.JwtDecoder;
      import org.springframework.security.oauth2.jwt.JwtEncoder;
      import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
      import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
      import org.springframework.security.provisioning.InMemoryUserDetailsManager;
      import org.springframework.security.web.SecurityFilterChain;
      import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
      import com.nimbusds.jose.JOSEException;
      import com.nimbusds.jose.jwk.JWKSet;
      import com.nimbusds.jose.jwk.RSAKey;
      import com.nimbusds.jose.jwk.source.JWKSource;
      import com.nimbusds.jose.proc.SecurityContext;
      @Configuration
      @EnableWebSecurity
      @EnableMethodSecurity
      public class JwtSecurityConfig {
          @Bean
          public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, HandlerMappingIntrospector introspector) throws Exception {
              // h2-console is a servlet
              // https://github.com/spring-projects/spring-security/issues/12310
              return httpSecurity
                      .authorizeHttpRequests(auth -> auth
                              .requestMatchers("/authenticate").permitAll()
                              .requestMatchers(PathRequest.toH2Console()).permitAll() // h2-console is a servlet and NOT recommended for a production
                              .requestMatchers(HttpMethod.OPTIONS,"/**")
                              .permitAll()
                              .anyRequest()
                              .authenticated())
                      .csrf(AbstractHttpConfigurer::disable)
                      .sessionManagement(session -> session.
                              sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                      .oauth2ResourceServer(
                              OAuth2ResourceServerConfigurer::jwt)
                      .httpBasic(
                              Customizer.withDefaults())
                      .headers(header -> {header.
                              frameOptions().sameOrigin();})
                      .build();
          }
          @Bean
          public AuthenticationManager authenticationManager(
                  UserDetailsService userDetailsService) {
              var authenticationProvider = new DaoAuthenticationProvider();
              authenticationProvider.setUserDetailsService(userDetailsService);
              return new ProviderManager(authenticationProvider);
          }
          @Bean
          public UserDetailsService userDetailsService() {
              UserDetails user = User.withUsername("in28minutes")
                      .password("{noop}dummy")
                      .authorities("read")
                      .roles("USER")
                      .build();
              return new InMemoryUserDetailsManager(user);
          }
          @Bean
          public JWKSource<SecurityContext> jwkSource() {
              JWKSet jwkSet = new JWKSet(rsaKey());
              return (((jwkSelector, securityContext)
                      -> jwkSelector.select(jwkSet)));
          }
          @Bean
          JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
              return new NimbusJwtEncoder(jwkSource);
          }
          @Bean
          JwtDecoder jwtDecoder() throws JOSEException {
              return NimbusJwtDecoder
                      .withPublicKey(rsaKey().toRSAPublicKey())
                      .build();
          }
          @Bean
          public RSAKey rsaKey() {
              KeyPair keyPair = keyPair();
              return new RSAKey
                      .Builder((RSAPublicKey) keyPair.getPublic())
                      .privateKey((RSAPrivateKey) keyPair.getPrivate())
                      .keyID(UUID.randomUUID().toString())
                      .build();
          }
          @Bean
          public KeyPair keyPair() {
              try {
                  var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
                  keyPairGenerator.initialize(2048);
                  return keyPairGenerator.generateKeyPair();
              } catch (Exception e) {
                  throw new IllegalStateException(
                          "Unable to generate an RSA Key Pair", e);
              }
          }
      }
      • JwtAuthenticationController
      import org.springframework.http.ResponseEntity;
      import org.springframework.security.authentication.AuthenticationManager;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.web.bind.annotation.PostMapping;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RestController;
      @RestController
      public class JwtAuthenticationController {
          private final JwtTokenService tokenService;
          private final AuthenticationManager authenticationManager;
          public JwtAuthenticationController(JwtTokenService tokenService,
                                             AuthenticationManager authenticationManager) {
              this.tokenService = tokenService;
              this.authenticationManager = authenticationManager;
          }
          @PostMapping("/authenticate")
          public ResponseEntity<JwtTokenResponse> generateToken(
                  @RequestBody JwtTokenRequest jwtTokenRequest) {
              var authenticationToken =
                      new UsernamePasswordAuthenticationToken(
                              jwtTokenRequest.username(),
                              jwtTokenRequest.password());
              var authentication =
                      authenticationManager.authenticate(authenticationToken);
              var token = tokenService.generateToken(authentication);
              return ResponseEntity.ok(new JwtTokenResponse(token));
          }
      }

      。해당 JWT Java 파일들을 Spring 진입점 Class의 Sub package에 위치 지정 후 JWT Token을 생성하기 위해 Talend APIhttp://localhost:8080/authenticate로 다음 구문을 HTTP Request Body로 포함하여 POST HTTP Request를 전송.

      {
          "username" : "in28minutes",
          "password" : "dummy"
      }


      HTTP Request를 전송 시 Body에 입력된 usernamepassword를 토대로 JWT Token이 생성되어 HTTP Response Body로 반환.
      ReactAxios Http Method를 통해 usernamepassword를 전달하여 해당 API 호출 시 HTTPResponse.data.token으로 참조 가능.

      。생성된 JWT Token 코드jwt.io에 입력 시 다음처럼 header , payload , signature의 3개 구조로 사용자가 확인하기 용이하도록 표현.


      ApplicationREST APIOAuth 2.0 Resource Server로서 보호되므로, 생성된 JWT TokenHTTP RequestAuthorization Header를 설정하여 REST API를 호출해야한다.
      { Authorization : "Bearer JWT토큰" }으로 설정.

      Authorization Header를 설정하지않고 REST API 호출 시 OAuth 2.0 Resource Server에 의해 API가 보호되어 HTTP Response 401이 발생하지만, Authorization HeaderBearer JWT토큰을 설정 후 REST API 호출 시 정상적으로 Resource를 반환.



  • React Application에서 JWT Token 활용하여 Spring의 REST API에 접근하도록 설정
    React에서 JWT Token을 활용하여 Auhtentication을 수행하고 SpringOAuth 2.0 Resource Server로 보호된 REST API에 접근하는 상호작용을 정의.
    • Spring의 JWT Token을 생성하는 APIIDPW를 포함한 POST HTTP Request를 전달하여 JWT Token을 가져오는 Axios HTTP Method 구현.
      Spring"http://localhost:8080/authenticate"로 Mapping된 API에 Talend API TesterPOST HTTP RequestJSON format으로 IDPW를 포함하여 전송 시 JWT Token이 생성되어 반환된 것과 동일한 원리.
    // Spring의 "localhost:8080/authenticate" 로 Mapping된 API로 
    // ID와 PW를 Object type으로 포함하여 호출하여 `JWT Token`을 가져온다.
    export const JWTAuthenticationApi = (username,password) => apiClient.post('/authenticate', { username , password } )
    • AuthContext.js에서 해당 JWT Token을 반환하는 Axios HTTP Method를 이용하는 로그인 기능 구현
      。기존 HTTP Basic Authentication으로 구현된 로그인기능의 logIn() 함수를 Refactoring.

      Axios HTTP Method를 통해 JWT토큰코드를 생성하여 HTTP Response로 가져올 경우, response.data.token으로 참조 가능.
      await을 통해 Promise에서 성공적으로 반환된 HTTP Responseconst response로 인가.
      "Bearer " + response.data.token으로 JWT Token변수 설정.
      JWT Token변수React ContextGlobal State로서 저장.

      axios request interceptor를 구현하여 해당 Axios instance로 API호출 시 HTTP Request가 Server로 전달되기 전 Intercepting하여 Authorization headerJWT Token변수로 설정.
         // AuthContext.js
    async function logIn(username , password){
      try {
        // Axios HTTP method에 ID와 PW를 전달하여 JWT Token을 HTTP Response로 가져온다.
        const response = await JWTAuthenticationApi(username,password)
        if (response.status == 200){
          // "Bearer JWT토큰코드" JWT Token변수 설정.
          const jwtToken = 'Bearer ' + response.data.token;
          setAuthState(true)
          setToken(jwtToken)
          setUsername(username)
          // axios instance를 통해 API 호출 시 Intercepting하여 Authentication Header를 JWT Token으로 설정.
          apiClient.interceptors.request.use((config)=>{
            console.log('intercepting and adding a token')
            config.headers.Authorization=jwtToken
            return config
          })
          return true
        } else {
          Logoutfunction()
          return false
        }
      } catch {
        Logoutfunction()
        return false
      }
    }



    JWT Token을 사용하도록 다음 기능을 구현한 후 로그인 시 Axios Interceptor를 통해 apiClient로 모든 API호출을 Interceping하여 Authorization HeaderJWT Token을 설정하여 OAuth 2.0 Resource Server에 의해 보호된 모든 SpringREST API에 접근 가능.
    Fetch/XHRHTTP Request를 확인 시 로그인 시 입력한 ID, PWpayload로서 확인 및 Authorization header로서 포함된 JWT Token을 확인 가능.

    Fetch/XHR ( Fecth / XMLHttpRequest ) : Front-End Application에서 전송한 비동기 HTTP Request을 표시.

profile
공부기록 블로그

0개의 댓글