HttpOnly ์ฟ ํ‚ค ๐Ÿช: ๊ฐœ๋…, ์ ์šฉ ๋ฐฉ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  ๋ณด์•ˆ์  ํŠน์ง• ๐Ÿ”’

๋‚˜๋‚˜'s Brainยท2024๋…„ 12์›” 17์ผ
0

๊ฐœ๋…Study

๋ชฉ๋ก ๋ณด๊ธฐ
10/21
post-thumbnail

๐Ÿš€ JWT ์ธ์ฆ ๋ฐ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐฉ๋ฒ• (HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ)

1. JWT ์ธ์ฆ์˜ ๊ธฐ๋ณธ ํ๋ฆ„ ๐Ÿƒโ€โ™‚๏ธ

์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๋ฉด ์„œ๋ฒ„๋Š” JWT(JSON Web Token)๋ฅผ ๋ฐœ๊ธ‰ํ•˜๊ณ , ์ด๋ฅผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ €์žฅํ•˜์—ฌ ์ธ์ฆ์„ ์œ ์ง€ํ•œ๋‹ค. JWT๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ localStorage๋‚˜ sessionStorage์— ์ €์žฅํ•˜์ง€๋งŒ, ๋ณด์•ˆ์ด ์ค‘์š”ํ•œ ์ƒํ™ฉ์—์„œ๋Š” HttpOnly ์ฟ ํ‚ค์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์•ˆ์ „ํ•˜๋‹ค. ๐ŸŽฏ


2. ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ ๐Ÿ›ก๏ธ

localStorage์— JWT๋ฅผ ์ €์žฅํ•˜๋ฉด XSS(ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์Šคํฌ๋ฆฝํŒ…) ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด, HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ๐Ÿ˜ฌ
ํ•˜์ง€๋งŒ... HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด XSS๋ฅผ ์™„์ „ํžˆ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, JavaScript๋กœ ์ฟ ํ‚ค ์ ‘๊ทผ๋งŒ ๋ง‰๋Š”๋‹ค๋Š” ์ ์„ ๋ช…ํ™•ํžˆ ํ•ด์•ผํ•œ๋‹ค.

XSS ์ž์ฒด๋ฅผ ์ฐจ๋‹จํ•˜๋ ค๋ฉด โ‰๏ธ : ์ฝ˜ํ…์ธ  ๋ณด์•ˆ ์ •์ฑ…(CSP) ์„ค์ •, ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ ๊ฐ•ํ™”๊ฐ€ ํ•„์š”

+ ๐Ÿ’กCSRF ๋ฐฉ์–ด๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ์„ค์ •
HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด CSRF ๋ฐฉ์–ด๋Š” ์ถฉ๋ถ„ํ•˜์ง€ ์•Š๋‹ค.
์ด๋ฅผ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด CSRF ํ† ํฐ๊ณผ ๊ฐ™์€ ์ถ”๊ฐ€ ๋ณดํ˜ธ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

์˜ˆ์‹œ:

  • CSRF ๋ฐฉ์ง€ ํ† ํฐ ์ƒ์„ฑ โ†’ ์ฟ ํ‚ค์™€ ํ•จ๊ป˜ ์ „์†ก
  • ์š”์ฒญ ์‹œ CSRF ํ† ํฐ์„ ํ—ค๋”๋‚˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•จ๊ป˜ ๋ณด๋‚ด๊ธฐ
  • ์„œ๋ฒ„์—์„œ ๊ฒ€์ฆ

3. HttpOnly ์ฟ ํ‚ค๋ž€? ๐Ÿช

HttpOnly ์ฟ ํ‚ค๋Š” JavaScript์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋Š” ์ฟ ํ‚ค์ด๋‹ค. ์ฆ‰, ํด๋ผ์ด์–ธํŠธ ์ธก ์Šคํฌ๋ฆฝํŠธ์—์„œ๋Š” ์ด ์ฟ ํ‚ค์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ XSS ๊ณต๊ฒฉ์„ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๊ณ , ์ž๋™์œผ๋กœ ์ฟ ํ‚ค๋ฅผ ์„œ๋ฒ„์— ์ „์†กํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ธ์ฆ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ„ํŽธํ•ด์ง„๋‹ค๋Š” ์žฅ์ ์ด ์กด์žฌํ•œ๋‹ค. ๐Ÿ˜Ž

4. ๊ตฌํ˜„ ๊ณผ์ • ๐Ÿ”ง

4-1. ์„œ๋ฒ„์—์„œ JWT๋ฅผ HttpOnly ์ฟ ํ‚ค์— ์ €์žฅ ๐Ÿ’ผ

๋กœ๊ทธ์ธ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ ํ›„, ์„œ๋ฒ„๋Š” JWT๋ฅผ ์ƒ์„ฑํ•˜์—ฌ HttpOnly ์ฟ ํ‚ค๋กœ ํด๋ผ์ด์–ธํŠธ์— ์ „์†กํ•˜๋ฉฐ ์•„๋ž˜์™€ ๊ฐ™์€ ์„ค์ •์„ ํ•œ๋‹ค :

// ์ฟ ํ‚ค ์ƒ์„ฑ
Cookie jwtCookie = new Cookie("token", token);
jwtCookie.setHttpOnly(true);  // JavaScript์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€
jwtCookie.setSecure(false);   // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” false, ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ๋Š” true
jwtCookie.setPath("/");       // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์— ์œ ํšจ
jwtCookie.setMaxAge(7 * 24 * 60 * 60);  // 7์ผ ๋™์•ˆ ์œ ํšจ
response.addCookie(jwtCookie);

+ ๐Ÿ’ก HttpOnly ์™ธ์— SameSite ์†์„ฑ ์ถ”๊ฐ€
SameSite ์†์„ฑ์€ ์ฟ ํ‚ค์˜ CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•œ๋‹ค. ์•Œ์•„์•ผํ• ๊ฒŒ ...๋๋„์—†๋‹ค ใ„นใ…‡...

SameSite=Strict โ†’ ์ฟ ํ‚ค๊ฐ€ ๊ฐ™์€ ์‚ฌ์ดํŠธ์—์„œ๋งŒ ์ „์†ก๋จ
SameSite=Lax โ†’ ๋Œ€๋ถ€๋ถ„์˜ ์š”์ฒญ์—์„œ ์ „์†ก๋˜์ง€๋งŒ, ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ์—์„œ ์ œํ•œ
SameSite=None โ†’ ๋ชจ๋“  ์š”์ฒญ์—์„œ ํ—ˆ์šฉ๋˜์ง€๋งŒ, ๋ฐ˜๋“œ์‹œ Secure ์†์„ฑ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์•ผ ํ•จ

4-2. ํด๋ผ์ด์–ธํŠธ์—์„œ HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ ๐Ÿ–ฅ๏ธ

HttpOnly ์ฟ ํ‚ค๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค. ํ•˜์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ, ์ž๋™์œผ๋กœ ์ฟ ํ‚ค๊ฐ€ ํฌํ•จ๋˜์–ด ์ „์†ก๋œ๋‹ค. ์ด๋กœ ์ธํ•ด, ์„œ๋ฒ„๋Š” ๋ณ„๋„์˜ ์ €์žฅ์†Œ ์—†์ด ์ธ์ฆ์„ ์ž๋™์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๐Ÿ”„


5. ๋กœ๊ทธ์ธ ํ›„ ์ƒํƒœ ๊ด€๋ฆฌ (Pinia ์‚ฌ์šฉ) ๐Ÿ“ฆ

๋‚˜๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ๋Š” ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Pinia์™€ ๊ฐ™์€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ๋กœ๊ทธ์ธ ํ›„ HttpOnly ์ฟ ํ‚ค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ , ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ ๋˜์–ด๋„ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๐Ÿ‘

const loginState = useLoginState(); // Pinia ์ƒํƒœ ๊ด€๋ฆฌ ์‚ฌ์šฉ

// ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ, ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๊ถŒํ•œ์„ ์ƒํƒœ์— ์ €์žฅ
loginState.isLoggedIn = true;
loginState.adminName = decodedToken.name;

์•„๋ž˜๋Š” ์œ„์—๋ฃฐ ์ฐธ๊ณ ํ•˜์—ฌ ๊ตฌํ˜„ํ•œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์˜ script ๋ถ€๋ถ„์ด๋‹ค.

<script setup>
import axios from 'axios';
import { jwtDecode } from 'jwt-decode';
import { ref } from 'vue';
import { useLoginState } from '@/stores/loginState';

// ํผ ๋ฐ์ดํ„ฐ
const formData = ref({
  adminCode: '',
  adminPassword: ''
});

// ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๊ด€๋ฆฌ
const errorMessage = ref('');
const loginState = useLoginState(); // ๋กœ๊ทธ์ธ ์ƒํƒœ ๊ด€๋ฆฌ Store

// ๋กœ๊ทธ์ธ ํ•จ์ˆ˜
const loginUser = async () => {
  // ํ•„์ˆ˜ ์ž…๋ ฅ ํ™•์ธ
  if (!formData.value.adminCode || !formData.value.adminPassword) {
    alert("์‚ฌ๋ฒˆ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.");
    return;
  }

  try {
    const response = await axios.post('http://localhost:5000/users/login', {
      admin_code: formData.value.adminCode,
      admin_password: formData.value.adminPassword
    }, {
      headers: {
        'Content-Type': 'application/json',
      }
    });

    console.log('HTTP ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ:', response.status);
    console.log('์ „์ฒด ์‘๋‹ต ๋ฐ์ดํ„ฐ:', response);

    // ์‘๋‹ต ํ—ค๋”์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    const token = response.headers.authorization;
    if (token) {

      // JWT ๋””์ฝ”๋”ฉ
      const decodedToken = jwtDecode(token);
      console.log('Decoded JWT Token:', decodedToken);

      // ๋กœ๊ทธ์ธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
      loginState.isLoggedIn = true;
      loginState.adminName = decodedToken.name; // JWT์—์„œ ์ด๋ฆ„ ์ถ”์ถœํ•˜์—ฌ ์ƒํƒœ ์„ค์ •

      // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
      alert(`${decodedToken.name}๋‹˜, ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค.`);

      // ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™
      window.location.href = '/';
    } else {
      errorMessage.value = '์ธ์ฆ ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ์‹คํŒจ';
      alert("์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.");
    }
  } catch (error) {
    console.error('๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
    errorMessage.value = '๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.';
    alert(errorMessage.value);
  }
};
</script>

6. ๋กœ๊ทธ์ธ ํ›„ ๋ณด์•ˆ ๊ฐ•ํ™” โš”๏ธ

  1. XSS ๊ณต๊ฒฉ ๋ฐฉ์ง€: JWT๋ฅผ HttpOnly ์ฟ ํ‚ค์— ์ €์žฅํ•˜์—ฌ, ํด๋ผ์ด์–ธํŠธ ์ธก ์Šคํฌ๋ฆฝํŠธ์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋„๋ก ํ•œ๋‹ค.
  2. ์ž๋™ ์ธ์ฆ ์ฒ˜๋ฆฌ: ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์˜ค๋Š” ์š”์ฒญ์—์„œ ์ž๋™์œผ๋กœ ์ฟ ํ‚ค๋ฅผ ํ™•์ธํ•˜์—ฌ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  3. CORS ์„ค์ •: ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ผ ๊ฒฝ์šฐ, ์„œ๋ฒ„์—์„œ CORS ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.
@Configuration
public class CorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("http://localhost:5173");  // ํด๋ผ์ด์–ธํŠธ URL
        configuration.addAllowedMethod("*");
        configuration.addAllowedHeader("*");
        configuration.setAllowCredentials(true);  // ์ฟ ํ‚ค ์ „์†ก ํ—ˆ์šฉ
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

7. ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ๐Ÿšช

๋กœ๊ทธ์•„์›ƒ ์‹œ์—๋Š” ์„œ๋ฒ„์—์„œ HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•˜๋ฉฐ, ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ์‚ญ์ œํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, ์„œ๋ฒ„์—์„œ ์ฟ ํ‚ค๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

Cookie jwtCookie = new Cookie("token", null);
jwtCookie.setHttpOnly(true);
jwtCookie.setSecure(true);
jwtCookie.setPath("/");
jwtCookie.setMaxAge(0);  // ์ฟ ํ‚ค ๋งŒ๋ฃŒ
response.addCookie(jwtCookie);

๋‚˜๋Š” ์—ฌ๊ธฐ์„œ refresh token์„ ๋ฐœํ–‰ํ•˜์—ฌ ๋ ˆ๋””์Šค์— ๋‹ด์•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ ˆ๋””์Šค์—์„œ๋„ ์บ์‹œ ์‚ญ์ œ๋ฅผ ํ•ด์ฃผ์—ˆ๋‹ค. ํ˜น์—ฌ๋‚˜ refresh token์„ Redis์— ์ €์žฅํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ•ด๋‹น token์„ Redis์—์„œ ์‚ญ์ œํ•ด์•ผ ๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ํ•ด๋‹น refresh token์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด access token์„ ๋ฐœ๊ธ‰๋ฐ›๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

Refresh Token์€ ํด๋ผ์ด์–ธํŠธ์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์„œ๋ฒ„์˜ ์•ˆ์ „ํ•œ ์ €์žฅ์†Œ(Redis)์— ์ €์žฅ
Refresh Token์ด ์‚ฌ์šฉ๋˜๋ฉด ์ฆ‰์‹œ ์‚ญ์ œ(์ผํšŒ์šฉ)

refresh token์„ ํ•œ ๋ฒˆ ์‚ฌ์šฉํ•˜๊ณ  ๋งŒ๋ฃŒ์‹œํ‚ค๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์œผ๋ฉฐ refresh token์„ ๋งค๋ฒˆ ์ƒˆ๋กœ ๋ฐœ๊ธ‰ํ•˜๊ณ , ์ด์ „ token์„ ์‚ญ์ œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ๋กœ๊ทธ์•„์›ƒ ์‹œ Redis์—์„œ refresh token์„ ์‚ญ์ œํ•œ ํ›„, 
์ƒˆ๋กœ ๋ฐœ๊ธ‰๋œ token์€ ์ด์ „์— ์‚ฌ์šฉ๋œ refresh token๊ณผ ๊ด€๊ณ„์—†์ด ๋‹ค์‹œ ๋ฐœ๊ธ‰๋˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

โš ๏ธ refresh token ์ผํšŒ์šฉ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒƒ์„ ์žŠ์ง€๋ง์ž !!!

Access Token์ด ๋งŒ๋ฃŒ๋จ โ†’ 401 ์—๋Ÿฌ ๋ฐ˜ํ™˜
ํด๋ผ์ด์–ธํŠธ๋Š” Refresh Token์„ ์„œ๋ฒ„๋กœ ์ „์†ก โ†’ ์ƒˆ Access Token ๋ฐœ๊ธ‰
Refresh Token์€ ์ฆ‰์‹œ ์‚ญ์ œํ•˜๊ณ  ์ƒˆ๋กœ์šด Refresh Token ๋ฐœ๊ธ‰


โš ๏ธ JWT ์ธ์ฆ ๊ตฌํ˜„ ์‹œ ์ฃผ์˜์‚ฌํ•ญ

JWT ์ธ์ฆ์„ ๊ตฌํ˜„ํ•  ๋•Œ HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์•ˆ์ „ํ•˜๊ณ  ํŽธ๋ฆฌํ•˜์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ์ฃผ์˜์‚ฌํ•ญ์ด ์žˆ๋‹ค. ์ด๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š์œผ๋ฉด ๋ณด์•ˆ์ด๋‚˜ ๊ธฐ๋Šฅ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜์˜ ์ฃผ์˜์‚ฌํ•ญ์„ ๋ฐ˜๋“œ์‹œ ์ฒดํฌ! โœ…

๐Ÿ”– 1. HTTPS ํ•„์ˆ˜! ๐Ÿ”’

cookie.setSecure(true)๋Š” HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ์ž‘๋™ํ•œ๋‹ค.
์ฆ‰, ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ secure ์†์„ฑ์„ true๋กœ ์„ค์ •ํ•˜๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ HTTPS๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” HTTP๋กœ๋„ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” HTTPS๋ฅผ ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. HTTPS๋Š” ๋ฐ์ดํ„ฐ ์ „์†ก ์ค‘ ์•”ํ˜ธํ™”๋ฅผ ๋ณด์žฅํ•˜๋ฉฐ, ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM)์„ ๋ฐฉ์ง€ํ•œ๋‹ค.
๋”ฐ๋ผ์„œ, ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ JWT ํ† ํฐ์„ HttpOnly ์ฟ ํ‚ค์— ์ €์žฅํ•˜๋ ค๋ฉด, HTTPS ์„ค์ •์ด ํ•„์ˆ˜์ด๋‹ค. ๐ŸŒ

์˜ˆ์‹œ:

Cookie jwtCookie = new Cookie("token", token);
jwtCookie.setHttpOnly(true); // JavaScript์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€
jwtCookie.setSecure(true);   // HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ์ ์šฉ
jwtCookie.setPath("/");
response.addCookie(jwtCookie);

๐Ÿ”– 2. JWT ํ† ํฐ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ โณ

JWT๋Š” ๋งŒ๋ฃŒ ์‹œ๊ฐ„(exp)์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋งŒ๋ฃŒ๋œ ํ† ํฐ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ธ์ฆ ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋”ฐ๋ผ์„œ, ํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

๋งŒ๋ฃŒ๋œ ํ† ํฐ ์ฒ˜๋ฆฌ ํ๋ฆ„:
ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญํ•  ๋•Œ, JWT ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ โžก๏ธ
๋งŒ์•ฝ ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด โžก๏ธ ํด๋ผ์ด์–ธํŠธ๋Š” ์ƒˆ๋กœ์šด ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•ด ์žฌ๋กœ๊ทธ์ธ์„ ํ•˜๊ฑฐ๋‚˜ โžก๏ธ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ(refresh token)์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ ํ† ํฐ์„ ๋ฐœ๊ธ‰

๐Ÿ’ก ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ์ƒˆ๋กœ์šด JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ์•ก์„ธ์Šค ํ† ํฐ์„ ๊ฐฑ์‹ ํ•˜๋Š” ์—ญํ• ๋งŒ ํ•˜๋ฉฐ, ๊ทธ ์ž์ฒด๋กœ ์ธ์ฆ์„ ๋Œ€์‹ ํ•˜๋Š” ์—ญํ• ์€ ํ•˜์ง€ ์•Š๋‹ค๋ผ๋Š”๊ฒƒ์„ ๊ผญ ์•Œ์•„๋‘์ž โ—โ—โ—โ—

๐Ÿ”– 3. CORS ์„ค์ • (Cross-Origin Resource Sharing) โš™๏ธ

ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ ๋™์ž‘ํ•  ๊ฒฝ์šฐ, CORS ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค. ํŠนํžˆ ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” credentials: true๋ฅผ ์„œ๋ฒ„์—์„œ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

๐Ÿ’ฅ CORS ์„ค์ •์„ ์œ„ํ•œ ์ฃผ์š” ํฌ์ธํŠธ ๐Ÿ’ฅ

Access-Control-Allow-Credentials: true: 
์ฟ ํ‚ค๋ฅผ ํฌํ•จํ•œ ์ธ์ฆ ์ •๋ณด๋ฅผ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์— ์ „๋‹ฌํ•˜๋ ค๋ฉด, ์ด ํ—ค๋”๋ฅผ ๋ฐ˜๋“œ์‹œ ์„ค์ • !!!

Access-Control-Allow-Origin: 
ํด๋ผ์ด์–ธํŠธ์˜ ์ถœ์ฒ˜(origin)๋ฅผ ์ •ํ™•ํžˆ ๋ช…์‹œํ•ด์•ผ ํ•˜๋ฉฐ, * (๋ชจ๋“  ์ถœ์ฒ˜) ๋Œ€์‹  ์ •ํ™•ํ•œ ๋„๋ฉ”์ธ์„ ์„ค์ •
@Configuration
public class CorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("http://localhost:5173"); // ํด๋ผ์ด์–ธํŠธ URL
        configuration.addAllowedMethod("*");
        configuration.addAllowedHeader("*");
        configuration.setAllowCredentials(true); // ์ฟ ํ‚ค ์ „์†ก ํ—ˆ์šฉ
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

๐Ÿ”– 4. ํ”„๋ก ํŠธ์—”๋“œ ์„ค์ • ์ฃผ์˜์‚ฌํ•ญ ๐Ÿšจ

โœจโœจโœจโœจโœจโœจ withCredentials: true

Axios๋‚˜ Fetch API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„๋กœ ์š”์ฒญํ•  ๋•Œ, ์ฟ ํ‚ค๋ฅผ ํฌํ•จํ•˜๋ ค๋ฉด withCredentials: true๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค. ๊ผญ!!!!!!!!!!!!!!!!!!!!!!!!!!!! ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ๋Š” withCredentials: true๋ฅผ ์„ค์ •ํ•ด์•ผ HttpOnly ์ฟ ํ‚ค๊ฐ€ ์š”์ฒญ ์‹œ ์ž๋™์œผ๋กœ ์ „์†ก๋œ๋‹ค.

์˜ˆ์‹œ (Axios ์‚ฌ์šฉ ์‹œ) :

const response = await axios.post('http://localhost:5000/users/login', 
  { 
    admin_code: formData.value.adminCode, 
    admin_password: formData.value.adminPassword 
  }, 
  {
    headers: { 'Content-Type': 'application/json' },
    withCredentials: true // ์ฟ ํ‚ค ์ž๋™ ์ „์†ก
  }
);

์ด๋ ‡๊ฒŒ ์œ„์˜ ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋ฅผ ๋„˜์–ด๊ฐ€๋„ ์ƒํƒœ๊ฐ€ ๊ทธ๋Œ€๋กœ์œ ์ง€๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

1. ์‚ฌ์šฉ์ž๋Š” `/users/login` ์—”๋“œํฌ์ธํŠธ๋กœ ์ธ์ฆ ์š”์ฒญ์„ ๋ณด๋‚ด ๋กœ๊ทธ์ธ
2. ๋ฐฑ์—”๋“œ๋Š” JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๊ณ , ์ด๋ฅผ `HttpOnly` ์ฟ ํ‚ค์— ์ €์žฅ
3. ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ, Pinia ์ƒํƒœ ๊ด€๋ฆฌ๋กœ ํ† ํฐ์„ ์œ ์ง€ํ•˜๊ณ , 
                `/admin/status` ์š”์ฒญ์„ ํ†ตํ•ด ์ธ์ฆ ์ƒํƒœ๋ฅผ ํ™•์ธ
4. ์ธ์ฆ ์„ฑ๊ณต ์‹œ, ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๊ถŒํ•œ์„ Pinia ์ƒํƒœ์— ์ €์žฅ
5. `HttpOnly` ์ฟ ํ‚ค์— ์ €์žฅ๋œ ํ† ํฐ์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€ํ•˜๋ฏ€๋กœ 
              ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์—†์Œ
6. ์ด๋ฅผ ํ†ตํ•ด JWT๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€

console.log(localStorage.getItem('token'));  

์›๋ž˜ ์ด๋ ‡๊ฒŒ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์— ๋‹ด๊ณ  ์ด ๋ช…๋ น์–ด๋ฅผ ์ฝ˜์†”์— ์ฐ๋Š”๋‹ค๋ฉด ํ† ํฐ๊ฐ’์ด ๋‚˜ํƒ€๋‚ฌ์ง€๋งŒ,
์ด์ œ๋Š” ์œ„์™€ ๊ฐ™์€ ์ฝ˜์†”๊ฐ’์„ ์ž…๋ ฅํ•ด๋„ ํ† ํฐ๊ฐ’ ํ™•์ธ ๋ถˆ๊ฐ€๋‹ค.

๐Ÿ“œ ๊ธฐ๋ก์šฉ

console.log(localStorage.getItem('token')); --> ํ† ํฐ๊ฐ’ ํ™•์ธ ๋ช…๋ น์–ด
localStorage.removeItem('token'); --> ํ† ํฐ๊ฐ’ ์‚ญ์ œ ๋ช…๋ น์–ด

๐Ÿ’ก ๋งˆ๋ฌด๋ฆฌ

HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์€ ๋ณด์•ˆ์„ฑ์„ ๋†’์ด๊ณ , ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ„ํŽธํ•˜๋ฉฐ ์ด๋ฅผ ํ†ตํ•ด, XSS ๊ณต๊ฒฉ์„ ์˜ˆ๋ฐฉํ•˜๊ณ  ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ, ์„œ๋ฒ„ ์ธก์—์„œ ํ† ํฐ์„ ๊ด€๋ฆฌํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ๋Š” ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Pinia ๋“ฑ)๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์ง€์†์ ์œผ๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค. ๐Ÿ›ก๏ธโœจ

๋‚˜๋„ ์‚ฌ์‹ค... ์ด๊ฑธ ์ž‘์„ฑํ•˜๋ฉด์„œ ๋” ๋งŽ์ด ์ˆ˜์ •๊ณผ!!! ๊ณต๋ถ€๊ฐ€ ํ•„์š”ํ•œ๊ฒƒ ๊ฐ™๋‹ค ใ… ใ… .. ๊ทธ๋ƒฅ ์ •๋ณด์ฐจ ์ ๋Š”๊ฒƒ์ด๋‹ˆ, ํ™•์‹คํ•œ ์ •๋ณด๊ฐ€ ์•„๋‹ˆ์—ฌ๋„ ์ด์‚ฌ๋žŒ์€ ์ด๋ ‡๊ตฌ๋‚˜ .. ๋ผ๊ณ  ๋ด์คฌ์œผ๋ฉด ํ•œ๋‹ค ๐Ÿ˜ญ

ํ .. ๊ทผ๋ฐ httponly์ฟ ํ‚ค๋กœ aws ๋ฐฐํฌ๋ฅผ ํ•˜๋Š” ๋„์ค‘์— ์˜ค๋ฅ˜๊ฐ€ ๊ณ„์† ๋‚ฌ๋Š”๋ฐ ๋‚ด๊ฐ€ CI/CD๋ถ„์•ผ๋ฅผ ๊ณต๋ถ€๋ฅผ ์ œ๋Œ€๋กœ ์•ˆํ•ด๋ด์„œ .. ์ฃผ์–ด์ง„ ๋ฐฐํฌ ๊ธฐ๊ฐ„์ด ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋ ดํ’‹์ด ์–ด๋–ค ์˜ค๋ฅ˜์ธ์ง€๋Š” ํŒ€์›๋“ค๊ณผ ํ•จ๊ป˜ ์•Œ์•„๋ƒˆ์ง€๋งŒ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ฐพ์ง€ ๋ชปํ•˜๊ณ  just jwtํ† ํฐ ์•”ํ˜ธํ™” ๋ฐฉ๋ฒ•์œผ๋กœ ๋ฐฐํฌ ํ•˜์˜€๋‹ค .. ๐Ÿ˜ญ๐Ÿ˜ญ๐Ÿ˜ญ
์ด ๋ถ€๋ถ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚ฌ์–ด์„œ 1์ผ์ •๋„ ์‹œ๊ฐ„์ด ๋”œ๋ ˆ์ด ๋˜์—ˆ๋Š”๋ฐ ๊ดœํžˆ ๋ณด์•ˆ ์–ด์ฉŒ๊ตฌํ•˜๋ฉด์„œ ํ™œ์šฉํ•ด๋ณด์ž๊ณ  ์„ค๋“ ํ–ˆ๋‚˜ .. ์‹ถ๋‹ค.. ๋ฏธ์•ˆํ•œ ๋งˆ์Œ์ด ํฌ๊ธฐ๋„ ํ•˜๋ฉด์„œ ๋๊นŒ์ง€ ๊ฐ™์ด ํ•ด๊ฒฐ๋ฐฉ์•ˆ์„ ์ฐพ์•„์ค€ ํŒ€์›๋“ค ๋•๋ถ„์— ์ฃผ๋ˆ…์ด (์กฐ๊ธˆ๋งŒ)? ๋“ค์—ˆ๋˜๊ฒƒ ๊ฐ™๊ธฐ๋„.. ๐Ÿ˜ฅ
์‚ฌ์‹ค ๋‚ด๊ฐ€ ๋ฐฐํฌ๊นŒ์ง€ ์ƒ๊ฐ์„ ํ•˜๊ณ  ๋” ๊ณต๋ถ€ํ•˜๊ณ .. ๊ตฌํ˜„ํ–ˆ๋”๋ผ๋ฉด... ใ…œใ…œ ์ˆ˜๊ณ ๊ฐ€ ๋œ ํ–ˆ์„ํ…๋ฐ.. ์•„์ง๋„ ๋ฏธ์•ˆํ•˜๋ฉด์„œ ๋‚˜์ค‘์— ํ˜ผ์ž ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค. ์•ž์œผ๋กœ๋Š” ๊ณต๋ถ€ ๋” ์—ด์‹ฌํžˆ ํ•˜๊ณ .. ๊ฐœ๋ฐœํ•˜๊ธฐ . ์•ฝ์†. ๐Ÿคผโ€โ™‚๏ธ๐Ÿคผโ€โ™‚๏ธ๐Ÿคผโ€โ™‚๏ธ๐Ÿคผโ€โ™‚๏ธ๐Ÿคผโ€โ™‚๏ธ๐Ÿคผโ€โ™‚๏ธ ๐Ÿ˜ญ๐Ÿ˜ญ๐Ÿ˜ญ๐Ÿ˜ญ
( ๋‹ค๋“ค ๋„ˆ๋ฌด ๊ณ ๋งˆ์›Œ.. ์–ด์—‰์—‰ )

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” SHA-512 ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜๋Š” ์•”ํ˜ธํ™” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ๋‹ค์Œ์—๋Š” ์•”ํ˜ธํ™” ๋ฐฉ์‹์„ ์ข€๋” ๊ณต๋ถ€ํ•ด๋ณด๊ณ ์‹ถ๋‹ค.

์ฐธ๊ณ  ๋ฌธํ—Œ : https://gamhongshi.tistory.com/28

profile
"๋กœ์ปฌ์—์„  ๋ฌธ์ œ์—†์—ˆ๋Š”๋ฐโ€ฆ?"

0๊ฐœ์˜ ๋Œ“๊ธ€