설계 문서 · 2026년 10월 12일 기준

세션 인증 설계서

아이디와 비밀번호로 로그인하고, 서버가 세션을 관리하는 구조입니다. 브라우저는 세션 ID를 쿠키로, 앱은 Authorization 헤더로 보냅니다. 만료, 폐기, 인가 같은 서버 규칙은 둘이 똑같습니다. 일반 사용자는 MFA 없이 쓰고, 관리자에게는 법이 요구하는 조건을 더합니다. 각 항목에 근거 문서 번호와 등급을 붙였고, 단계마다 직접 조작해 볼 수 있는 데모가 있습니다.

Spring Boot 4 + KotlinWAS 2대 + PostgreSQLOAuth 미사용 (1st-party)브라우저 쿠키 · 앱 헤더일반 사용자 MFA 없음

브라우저쿠키로 운반

  • __Host- 쿠키에 Secure, HttpOnly, SameSite=Strict
  • CSRF는 Origin 검사와 고정 커스텀 헤더
  • nonce 기반 CSP로 XSS 실행 차단
  • 선택: DBSC로 기기 바인딩

앱헤더로 운반

  • 비밀번호는 네이티브 화면에서 입력
  • 세션 ID는 Keychain, Keystore에 보관
  • Authorization 헤더로 전송, 쿠키 저장 끔
  • 선택: DPoP로 기기 바인딩

서버 공통같은 세션 테이블

  • 256비트 세션 ID, 저장소에는 해시만
  • 로그인과 재인증마다 새 ID
  • 무활동 1시간, 최대 24시간 (관리자 15분, 4시간)
  • 객체 단위 인가, 비밀번호 변경 시 다른 세션 폐기
필수법(고시)이나 표준의 최소 수준. ASVS L1, NIST의 SHALL, RFC의 MUST.
권장표준의 권장 수준. ASVS L2, NIST의 SHOULD, RFC의 SHOULD, OWASP 우선 권고.
선택"보안 최우선" 결정에 따라 우리가 더 얹은 것. ASVS L3 포함.
등급
대상
01

로그인 시도

입력 화면과 비밀번호 규칙

MFA를 쓰지 않으면 비밀번호가 계정을 지키는 유일한 관문입니다. 그래서 NIST SP 800-63B-4는 비밀번호를 단독으로 쓸 때 최소 15자를 요구하고, MFA와 함께 쓸 때만 8자까지 허용합니다. OWASP ASVS 6.2.1은 아직 "8자 이상, 15자 강력 권장"이라 우리는 더 엄격한 NIST 쪽을 따릅니다.

대신 대문자, 숫자, 특수문자를 섞으라는 조합 규칙과 주기적 변경 강제는 금지됐습니다. 사람들은 이런 규칙에 Password1!, Password2!처럼 예측 가능한 변형으로 대응합니다. 강도를 만드는 건 길이이고, 약한 비밀번호는 유출 목록과 흔한 비밀번호 목록으로 걸러냅니다. 서비스 이름이나 아이디가 들어간 비밀번호도 막습니다.

최대 길이는 "64자 이상 허용"이 기준입니다. 이 숫자는 NIST 지침 작성에 참여한 Jim Fenton이 "패스프레이즈를 막지 않으면서 많은 화면에 한 줄로 들어가는 길이"라는 사용성 이유로 제시한 값입니다. 그렇다고 무한정 받으면 느린 해시 계산으로 서버 CPU를 소모시키는 공격이 가능해서, 우리는 128자를 상한으로 둡니다.

앱은 비밀번호를 네이티브 화면에서 받습니다. WebView 안의 웹 페이지는 앱이 들여다보고 조작할 수 있고, 사용자는 그 화면이 진짜인지 주소창으로 확인할 수 없습니다. RFC 8252 §8.12가 OAuth 맥락에서 이 위험을 다루고, OAuth를 쓰지 않는 우리도 같은 원칙을 따릅니다.

직접 해보기

비밀번호 정책 검사기

가입이나 비밀번호 변경 때 서버가 하는 검사를 그대로 흉내 냅니다. 입력값은 이 페이지 안에서만 계산되고 어디로도 전송되지 않습니다. 예시 계정은 아이디 minsu, 서비스 이름 ourshop입니다.

02

자격 증명 검증

서버가 비밀번호를 확인하는 순간

서버가 막아야 할 공격은 두 가지입니다. 로그인 창에 비밀번호를 계속 넣어 보는 온라인 추측, 그리고 DB가 유출됐을 때 해시를 내 컴퓨터에서 깨 보는 오프라인 공격입니다.

온라인 추측은 실패 시도 제한으로 막습니다. 계정 하나에 실패가 쌓이면 응답을 점점 늦추고, 일정 횟수를 넘으면 잠시 잠급니다. 그런데 유출된 아이디·비밀번호 목록으로 계정마다 한 번씩만 시도하는 크리덴셜 스터핑은 계정 단위 제한에 걸리지 않습니다. 그래서 IP 단위 제한과 "한 IP가 여러 계정에 실패" 탐지를 함께 둡니다. 한국 고시 제5조도 인증에 일정 횟수 실패하면 접근을 제한하라고 요구합니다.

오프라인 공격은 느린 해시로 막습니다. SHA-256 같은 빠른 해시는 GPU로 초당 수십억 번 계산할 수 있어 비밀번호 저장에 쓰면 안 됩니다. OWASP는 Argon2id를 1순위로, bcrypt를 작업 계수 10 이상의 허용 선택지로 둡니다. 고시 제7조는 알고리즘을 지정하지 않고 일방향 암호화만 요구하므로 둘 다 법적 요건은 채웁니다.

bcrypt를 쓴다면 앞 72바이트만 읽는다는 점을 처리해야 합니다. 한글은 UTF-8에서 한 글자가 3바이트라 한글 24자 정도만 넘어도 뒷부분이 조용히 버려집니다. 72바이트를 넘는 입력을 거부하거나, HMAC으로 먼저 줄인 뒤 bcrypt에 넣습니다. Argon2id에는 이 제한이 없습니다.

실패 메시지는 "아이디 또는 비밀번호가 올바르지 않습니다" 하나로 통일합니다. 어느 쪽이 틀렸는지 알려주면 공격자가 가입된 아이디 목록을 만들 수 있습니다. MFA가 없는 대신, 처음 보는 기기에서 로그인하면 이메일로 알려 사용자가 탈취를 빨리 알아채게 합니다.

직접 해보기

로그인 시도 제한 시뮬레이터

정책: 같은 계정에 3번째 실패부터 응답을 1초, 2초, 4초로 늦추고, 5번 실패하면 15분 잠급니다. 같은 IP가 10분 안에 서로 다른 계정 10개 이상에 실패하면 그 IP를 막습니다. 시계는 버튼으로 빨리 돌릴 수 있습니다.

직접 해보기

빠른 해시와 느린 해시

이 브라우저의 WebCrypto로 실제 계산 시간을 잽니다. WebCrypto에는 Argon2id와 bcrypt가 없어서, 같은 "느린 해시" 계열인 PBKDF2-SHA256을 OWASP의 FIPS 환경 권장값인 600,000회로 대신 측정합니다. 핵심은 "한 번 확인하는 데 드는 시간"이 공격자에게는 "한 번 추측하는 데 드는 시간"이라는 점입니다.

PBKDF2 측정은 기기에 따라 0.1~1초쯤 걸립니다.

아직 측정하지 않았습니다.

// Spring Security: Argon2id (Bouncy Castle 의존성 필요)
@Bean
fun passwordEncoder(): PasswordEncoder =
    Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()

// bcrypt를 쓴다면 72바이트 초과 입력은 거부
fun validateForBcrypt(raw: String) =
    require(raw.toByteArray(Charsets.UTF_8).size <= 72) { "비밀번호가 너무 깁니다" }
03

세션 발급

인증 상태를 만드는 순간

비밀번호가 맞으면 서버는 세션을 만들고 세션 ID를 발급합니다. 이후의 모든 요청은 이 ID 하나로 "로그인한 그 사람"임을 증명하니, 세션 ID는 비밀번호와 같은 무게의 비밀입니다.

RFC 10017 §7.1은 프론트와 API가 같은 도메인에 있는 1st-party 서비스라면 OAuth 토큰 대신 서버 측 쿠키 세션으로 인증 상태를 관리해도 된다고 명시합니다. 외부 클라이언트가 없으니 우리는 이 구조를 씁니다. 앱도 같은 세션 ID를 쓰고, 운반 방법만 다릅니다.

세션 ID는 추측할 수 없어야 합니다. ASVS 7.2.3은 CSPRNG(암호학적으로 안전한 난수 생성기)로 만들고 128비트 이상의 엔트로피를 가지라고 요구합니다. 우리는 256비트를 씁니다. 아래 계산기에서 보듯 128비트도 추측이 사실상 불가능해서, 256비트는 "비용이 거의 0이라 여유를 둔" 선택입니다. 참고로 Spring Session의 기본 ID는 UUID라 실제 랜덤 비트가 122비트이고, 엄밀히는 128비트 요건에 조금 못 미칩니다.

로그인할 때마다 새 ID를 발급하는 것(ASVS 7.2.4)은 세션 고정 공격을 막습니다. 공격자가 자기가 아는 세션 ID를 피해자 브라우저에 미리 심어 두고, 피해자가 그 ID인 채로 로그인하기를 기다리는 공격입니다.

서버 저장소에는 세션 ID 원문 대신 SHA-256 해시를 저장합니다. 표준의 필수 항목은 아니고 결정문 조건 ①로 우리가 정한 심층 방어입니다. DB 백업이나 Redis 덤프가 유출돼도 그 안의 값으로는 로그인할 수 없습니다. 256비트 랜덤 값이라 느린 해시나 salt는 필요 없습니다.

직접 해보기

세션 ID 생성기와 추측 계산기

이 브라우저의 CSPRNG(crypto.getRandomValues)로 실제 세션 ID를 만듭니다. 서버 Kotlin의 SecureRandom과 같은 역할입니다. 왼쪽 값은 클라이언트에 주는 원문, 아래 해시는 DB에 저장되는 값입니다.

계산식: 아무 세션 하나라도 맞힐 때까지의 평균 시간 = 2비트 ÷ (세션 수 × 초당 추측) ÷ 2. 우주의 나이는 약 1.38 × 1010년입니다.

직접 해보기

세션 고정 공격 재현

세 단계를 순서대로 눌러 보세요. "로그인 시 새 ID 발급"을 끄면 공격이 성공하고, 켜면 실패합니다. 실제로는 __Host- 쿠키가 하위 도메인에서 쿠키를 심는 것도 막고, 로그인 전에는 세션을 만들지 않으므로(결정문 ③) 공격 면 자체가 좁습니다. 이 데모는 그중 마지막 방어선만 보여줍니다.

// 256비트 세션 ID 생성과 저장용 해시
private val rng = SecureRandom()

fun newSessionId(bytes: Int = 32): String {
    val buf = ByteArray(bytes).also { rng.nextBytes(it) }
    return Base64.getUrlEncoder().withoutPadding().encodeToString(buf)  // 43자
}

fun storageKey(raw: String): String =
    HexFormat.of().formatHex(
        MessageDigest.getInstance("SHA-256").digest(raw.toByteArray())
    )
04

세션 전달

세션 ID를 클라이언트에 맡기는 방법

발급한 세션 ID를 클라이언트가 보관하고 요청마다 다시 보내야 합니다. 브라우저와 앱은 이 보관과 운반 방법만 다르고, 서버가 하는 일은 같습니다.

브라우저는 쿠키를 씁니다. 쿠키는 브라우저가 자동으로 붙여 보내서 편하지만, 그 자동성 때문에 공격 표면도 생깁니다. RFC 10017 §6.1.3.2는 세션 쿠키에 Secure와 HttpOnly를 반드시(MUST) 켜고, SameSite=Strict, Path=/, Domain 미지정, 접두어 사용을 권장(SHOULD)합니다. 각 속성이 무엇을 막는지는 아래 실험실에서 하나씩 꺼 보며 확인할 수 있습니다.

__Host- 접두어는 이름이 아니라 브라우저가 강제하는 규칙입니다. 이름이 __Host-로 시작하면 브라우저는 Secure가 있고, Path=/이고, Domain이 없을 때만 쿠키를 저장합니다. 그래서 이벤트용 promo.example.com 같은 하위 도메인이 뚫려도 공격자가 우리 세션 쿠키를 덮어쓰거나 심을 수 없습니다. RFC 10017은 더 새로운 __Host-Http- 접두어도 예로 들지만, 브라우저 지원 범위를 따로 확인해야 해서 지금은 __Host-를 씁니다.

앱은 로그인 응답 본문으로 세션 ID를 받아 iOS Keychain이나 Android Keystore로 보호한 저장소에 두고, 요청마다 Authorization 헤더에 직접 붙입니다. 앱 코드가 명시적으로 붙이니 다른 사이트가 위조 요청을 만들 수 없고, CSRF 방어가 필요 없습니다. 대신 HTTP 클라이언트의 쿠키 저장을 꺼서, 앱 쪽에 쿠키로도 인증되는 경로가 생기지 않게 합니다.

// Spring Session 쿠키 설정
@Bean
fun cookieSerializer() = DefaultCookieSerializer().apply {
    setCookieName("__Host-SID")
    setUseSecureCookie(true)
    setUseHttpOnlyCookie(true)
    setSameSite("Strict")
    setCookiePath("/")
    // setDomainName은 호출하지 않음
}
# 앱: 로그인 응답 본문으로 받은 값을
# Keychain/Keystore에 두고 매 요청 헤더에
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Session Kx8Qm2…(43자)

# iOS: httpShouldSetCookies = false
# Android OkHttp: CookieJar.NO_COOKIES (기본값)
05

요청마다 인증 확인

로그인 이후의 모든 요청

로그인 이후 모든 요청에서 서버는 세 가지를 확인합니다. 세션이 살아 있는가, 이 요청을 정말 사용자가 보냈는가, 그리고 선택적으로 세션을 처음 받은 그 기기에서 왔는가입니다.

세션이 살아 있는지는 시계 두 개로 판단합니다. 무활동 타임아웃은 마지막 요청 이후 경과 시간이고, 절대 만료는 로그인 시점부터의 총 시간입니다. 활동이 계속돼도 절대 만료가 오면 다시 로그인해야 합니다. 비밀번호만 쓰는 수준(NIST AAL1)에서 NIST의 최소선은 30일 이내 재인증이지만, 보안 우선이라 일반 사용자는 무활동 1시간에 최대 24시간으로 잡습니다. ASVS 7.1.1은 이 값과 NIST에서 벗어나는 이유를 문서로 남기라고 합니다.

브라우저 요청은 CSRF를 막아야 합니다. 쿠키가 자동으로 붙기 때문에, 공격 사이트가 사용자의 브라우저를 빌려 우리 서버로 요청을 보낼 수 있습니다. RFC 10017 §6.1.3.3은 SameSite=Strict를 기본으로 두되, 같은 사이트의 다른 하위 도메인에서 오는 공격에는 그것만으로 부족하니 CORS와 고정 커스텀 헤더를 함께 쓰라고 합니다. 커스텀 헤더가 붙은 교차 출처 요청은 브라우저가 항상 사전 확인(preflight)을 하므로, 서버가 허락하지 않으면 요청이 아예 나가지 않습니다.

XSS는 이 모든 방어를 우회합니다. 악성 스크립트가 우리 페이지 안에서 실행되면 사용자와 똑같이 요청을 보낼 수 있고, 이건 어떤 인증 방식도 막지 못합니다(RFC 10017 §5). 그래서 nonce 기반 CSP와 SRI로 XSS가 실행되는 것 자체를 막는 데 집중합니다.

기기 바인딩은 선택입니다. 브라우저는 DBSC로 5분짜리 단기 쿠키를 보안 칩 키 서명으로만 갱신하고, 앱은 DPoP(RFC 9449)로 요청마다 서명을 붙입니다. 둘 다 "세션 ID를 훔쳐 다른 기기에서 쓰는 것"을 막지만, 감염된 기기 안에서의 악용은 막지 못합니다. 보안 칩이 없는 기기에서 DBSC는 오류 없이 일반 쿠키로 동작합니다.

직접 해보기

만료 시계

로그인 후 흐른 시간과 마지막 요청 후 흐른 시간을 움직여 보세요. 두 시계 중 하나라도 한도에 닿으면 세션이 끝납니다.

직접 해보기

CSRF 요청 판정기

요청이 어디서 왔고 무엇을 하려는지 고르면, 브라우저와 서버의 방어층을 차례로 통과시켜 결과를 보여줍니다. 우리 서버의 커스텀 헤더 규칙은 X-Client: web입니다.

직접 해보기

기기 바인딩이 막는 것과 못 막는 것

앱의 DPoP 기준입니다. 브라우저 DBSC도 원리는 같고, 서명을 단기 쿠키 갱신 때만 한다는 점이 다릅니다.

시나리오를 골라 보세요.

// DPoP 헤더에 담기는 proof (JWT, 요청마다 새로 서명)
{ "typ": "dpop+jwt", "alg": "ES256",
  "jwk": { /* 보안 칩 키의 공개키. 서버는 이 지문(jkt)을 세션에 저장 */ } }
.
{ "htm": "POST",                              // 이 메서드에만 유효
  "htu": "https://api.example.com/address",   // 이 URL에만 유효
  "iat": 1791789000,                          // 발급 시각 (±30초)
  "jti": "d3b0c7e2-…",                        // 1회용 ID, 재사용 거부
  "nonce": "srv-node2-…",                     // 서버가 준 값
  "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo" } // 세션 ID의 해시
06

인가

이 사용자가 이 데이터에 접근해도 되는가

인증이 "당신이 누구인가"라면, 인가는 "당신이 이걸 해도 되는가"입니다. 실제 사고는 인증보다 인가에서 더 자주 납니다. OWASP API Security Top 10에서도 객체 단위 인가 실패(BOLA)가 1위입니다.

인가는 세 층으로 나눠 봅니다. 기능 단위(ASVS 8.2.1)는 "일반 사용자가 관리자 API를 부를 수 있나", 객체 단위(8.2.2)는 "민수가 지영의 주문을 볼 수 있나", 필드 단위(8.2.3)는 "민수가 자기 프로필을 고치면서 role까지 바꿀 수 있나"입니다. 셋 다 서버에서 검사해야 합니다(8.3.1). 프론트에서 버튼을 숨겨도 공격자는 API를 직접 호출합니다.

객체 단위는 조회 조건에 항상 소유자를 넣는 것으로 막습니다. 남의 주문 ID가 들어오면 403이 아니라 404를 돌려줘서, 그 주문이 존재한다는 사실도 숨깁니다.

필드 단위는 요청 DTO와 엔티티를 분리해서 막습니다. 요청 JSON을 엔티티에 그대로 바인딩하면, 클라이언트가 보내면 안 되는 필드까지 덮어써집니다(Mass Assignment).

세션 방식의 장점이 여기서도 드러납니다. 권한을 요청마다 DB에서 읽거나 변경 시 세션을 갱신하면 권한 박탈이 즉시 반영됩니다(ASVS 8.3.2). JWT라면 토큰이 만료될 때까지 옛 권한이 남습니다.

직접 해보기

남의 주문 보기 (IDOR)

로그인한 사람은 민수(user 1001)입니다. 주문 5001은 민수의 것, 5002는 지영(user 1002)의 것입니다. 서버 구현을 바꿔 가며 GET /api/orders/{id}를 보내 보세요.

서버 구현

요청을 보내 보세요.

직접 해보기

필드 덮어쓰기 (Mass Assignment)

민수가 닉네임만 바꾸는 화면에서, 요청 본문에 몰래 role과 point를 끼워 넣었습니다. PATCH /api/me 처리 방식을 바꿔 가며 결과를 보세요.

서버 처리 방식

요청을 보내 보세요.

// 객체 단위: 조회 조건에 항상 소유자
interface OrderRepository : JpaRepository<Order, Long> {
    fun findByIdAndUserId(id: Long, userId: Long): Order?
}

@GetMapping("/api/orders/{id}")
fun get(@PathVariable id: Long, @AuthenticationPrincipal me: LoginUser) =
    orders.findByIdAndUserId(id, me.id) ?: throw NotFoundException()  // 403 아닌 404

// 필드 단위: 바꿔도 되는 필드만 가진 요청 DTO
data class UpdateProfileRequest(val nickname: String)
07

민감 작업 재인증

이미 로그인했어도 한 번 더

로그인한 세션이라도, 그 세션을 지금 누가 쓰고 있는지는 확신할 수 없습니다. 자리를 비운 PC, 탈취된 쿠키, XSS 모두 "로그인한 상태"로 요청을 보냅니다. 그래서 피해가 큰 작업 직전에는 비밀번호를 한 번 더 받습니다.

ASVS 7.5.1은 이메일, 전화번호, 비밀번호처럼 계정 복구에 쓰이는 정보를 바꾸기 전에 재인증을 요구합니다. 이 정보가 바뀌면 공격자가 계정을 영구히 가져갈 수 있기 때문입니다. 7.5.3(L3)은 결제 같은 고위험 작업 전 추가 확인을 요구합니다. MFA가 없으니 재인증 수단은 비밀번호입니다.

세션에 마지막 인증 시각(auth_time)을 두고, 민감 작업이 오면 그 시각이 5분 이내인지 봅니다. 오래됐으면 비밀번호를 다시 받고, 성공하면 auth_time을 갱신하면서 세션 ID도 새로 발급합니다(7.2.4). 재인증 전의 ID가 어딘가 새어 나갔더라도, 그 ID로는 높아진 신뢰를 쓸 수 없게 하기 위해서입니다.

이메일로 계정을 복구한 직후는 특히 위험합니다. 이메일 계정이 뚫렸다면 공격자가 복구했을 수 있으니, 복구 후 전체 세션을 폐기하고, 24~72시간 동안 민감 작업을 막고, 기존 이메일로 알립니다(결정문 ⑫).

직접 해보기

재인증 흐름

정책: 이메일과 비밀번호 변경은 마지막 인증이 5분을 넘었으면 재인증, 결제는 매번 재인증, 주문 조회는 재인증 없음. 데모 비밀번호는 demo입니다.

08

세션 종료와 폐기

끝내야 할 때 확실히 끝내기

세션 방식을 고른 가장 큰 이유가 여기 있습니다. 서버가 세션 행을 지우는 순간 그 세션은 바로 끝납니다. 다음 요청부터 조회 단계에서 막히고, 기다릴 만료 시간이 없습니다.

종료가 필요한 순간은 정해져 있습니다. 로그아웃(ASVS 7.4.1)은 그 세션 하나, 계정 정지나 삭제(7.4.2)는 그 사용자의 모든 세션, 비밀번호 변경(7.4.3)은 지금 쓰는 세션을 뺀 나머지입니다. 브라우저와 앱의 세션이 같은 테이블에 있으니 한 번의 삭제로 전부 끝납니다.

사용자가 로그인된 기기 목록을 보고 직접 끊을 수 있어야 합니다(7.5.2). MFA가 없는 구조에서 모르는 기기를 발견하는 거의 유일한 창구입니다. 운영자는 특정 사용자나 전체 사용자의 세션을 끊을 수 있어야 합니다(7.4.5).

앱은 로그아웃할 때 서버 세션 삭제와 함께 기기 저장소의 세션 ID도 지웁니다. 서버 쪽만 지우면 기기에 죽은 ID가 남고, 기기 쪽만 지우면 서버에 살아 있는 세션이 남습니다.

직접 해보기

로그인된 기기 관리

민수의 계정에 브라우저 세션 2개, 앱 세션 2개가 있습니다. 각 버튼이 실행하는 쿼리도 함께 보여줍니다. 7단계에서 비밀번호 변경을 하면 여기에도 반영됩니다.

    -- 실행된 쿼리가 여기 표시됩니다
    +

    관리자 추가 조건

    전 단계에 걸쳐 붙는 규칙

    관리자는 다른 사용자의 개인정보를 열람하니, 관리자 계정 하나가 뚫리면 피해 범위가 전체 회원입니다. 그래서 일반 사용자 규칙 위에 법적 의무와 우리 선택을 더 얹습니다.

    개인정보의 안전성 확보조치 기준(고시) 제6조 ②는 정당한 접근 권한자(정보주체 제외)가 외부에서 개인정보처리시스템에 접속할 때 인증서, 보안토큰, 일회용 비밀번호 같은 안전한 인증수단을 적용하라고 요구합니다. VPN 같은 안전한 접속수단으로 대신할 수 있다는 단서는 이용자가 아닌 정보주체의 개인정보를 처리하는 시스템에만 해당합니다. 우리 회원은 "이용자"이므로 VPN만으로는 부족합니다.

    그래서 관리자에게 MFA를 쓰지 않으려면 사무실 내부망에서만 접속하게 해야 합니다. 외부 접속을 허용한다면 OTP나 보안키가 필요합니다.

    무활동 자동 차단(제6조 ④)은 외부와 내부를 가리지 않고 적용됩니다. 법은 시간을 정하지 않으며, 우리는 NIST AAL3와 PCI DSS를 참고해 15분으로 정했습니다. 관리자 화면은 IP 허용 목록으로 막고 인터넷에 공개하지 않습니다(제6조 ①, ISMS-P 2.6.3).

    이 내용은 법률 자문이 아닙니다. 실제 적용 전에는 개인정보보호위원회의 고시 해설서나 전문가 확인이 필요합니다.
    직접 해보기

    관리자 접속 판정기

    관리자의 접속 조건을 고르면, 법적 요건과 우리 규칙을 순서대로 적용해 허용 여부를 판정합니다.

    +

    기록

    누가 무엇을 했는지 남기기

    기록은 사고를 막지는 못하지만, 사고가 났을 때 무엇이 일어났는지 알 수 있는 유일한 수단이고 보호 조치를 했다는 법적 증거입니다.

    고시는 접속기록을 계정, 접속일시, 접속지 정보, 처리한 정보주체 정보, 수행 업무 등을 전자적으로 기록한 것으로 정의하고, 보관과 정기 점검을 요구합니다. 관리자가 어떤 회원의 정보를 언제 봤는지가 남아야 합니다. 보관 기간과 점검 주기는 고시 원문에서 확인하세요.

    이와 별도로 로그인 실패, 세션 폐기, 재인증, 권한 변경 같은 보안 이벤트를 남겨 이상 징후를 찾습니다(ASVS V16). 비밀번호와 세션 ID 원문은 절대 로그에 남기지 않습니다. 세션은 해시 앞부분만 남깁니다.

    아래 표는 이 페이지의 데모에서 일어난 일이 실제로 어떻게 기록되는지 보여줍니다. 위의 데모를 조작하면 행이 추가됩니다.

    실시간

    보안 이벤트와 접속기록

    시각계정접속지수행 업무대상결과
    §

    근거 문서

    표의 번호가 가리키는 문서들

    표의 근거 칩을 누르면 여기로 옵니다. 각 문서가 누가 만든 것인지, 무엇을 다루는지, 의무인지 권고인지, 우리와 어떤 관계인지 정리했습니다.

    RFC 10017

    IETF 모범 사례 · 2026년 8월
    다루는 것
    브라우저에서 도는 웹앱이 로그인 상태와 토큰을 다루는 방법. "페이지에 악성 JS가 실행되면 무엇을 뺏기나"를 위협 모델로 놓고 구성 방식 세 가지를 보안 순으로 비교합니다.
    핵심
    업무용, 민감한 앱, 개인정보를 다루는 앱에는 브라우저에 토큰을 두지 않는 BFF를 강하게 권장합니다. §7.1은 같은 도메인의 1st-party 앱이라면 OAuth 없이 서버 쿠키 세션으로 충분하다고 봅니다. §6.1.3.2는 쿠키 속성, §6.1.3.3은 CSRF 방어, §5는 XSS 위협을 다룹니다.
    우리와
    브라우저를 쿠키 세션으로 가는 결정의 직접 근거입니다.

    RFC 6265bis

    IETF 쿠키 표준 개정판
    다루는 것
    2011년 쿠키 표준(RFC 6265)을 고친 개정판. SameSite 속성과 __Host-, __Secure- 접두어가 여기서 정의됩니다. 최종 RFC 번호 부여 여부는 따로 확인이 필요하지만 주요 브라우저는 이미 구현했습니다.
    우리와
    세션 쿠키 설정의 기술 근거입니다.

    RFC 9449 (DPoP)

    IETF 표준 · 2023년
    다루는 것
    토큰을 클라이언트의 개인키에 묶어, 토큰을 훔쳐도 키가 없으면 못 쓰게 하는 방법. 요청마다 서명한 proof를 붙입니다. §4.3이 서버의 proof 검증 절차입니다. OAuth 확장이지만 불투명 토큰(우리의 세션 ID)에도 쓸 수 있습니다.
    우리와
    앱 세션을 기기 보안 칩 키에 묶는 선택적 강화 수단. 관리자 앱 접속에는 필수로 둡니다.

    RFC 8252

    IETF 모범 사례 · 2017년
    다루는 것
    모바일·데스크톱 앱이 OAuth로 로그인할 때 앱 안 WebView가 아니라 시스템 브라우저를 쓰라는 규칙. §8.12가 내장 WebView의 위험을 설명합니다.
    우리와
    OAuth를 쓰지 않으니 직접 대상은 아니고, "WebView에서 비밀번호를 받지 않는다"는 원칙만 따릅니다.

    OWASP ASVS 5.0

    OWASP 검증 기준 · 약 350개 요구사항
    다루는 것
    웹 애플리케이션과 API의 보안 요구사항 전체를 17개 장으로 정리한 체크리스트. 인증(V6), 세션 관리(V7), 인가(V8), 로깅(V16) 등이 있고 항목마다 "7.2.4" 같은 번호가 붙습니다. L1은 모든 앱, L2는 대부분 앱, L3는 고가치 앱 기준입니다.
    성격
    법적 의무는 없지만 보안 감사, 취약점 진단, 납품 계약에서 가장 많이 인용됩니다.
    우리와
    세션과 인가 설계의 가장 구체적인 근거. L2를 목표로 하고 관리자 기능은 일부 L3를 적용합니다.

    OWASP MASVS

    OWASP 모바일 검증 기준 · v2.1
    다루는 것
    모바일 앱 보안. 저장소(STORAGE), 암호화, 인증(AUTH), 네트워크, 플랫폼 연동(PLATFORM, WebView 포함), 코드 품질, 변조 방지, 프라이버시 8개 그룹. 테스트 방법은 짝 문서 MASTG에 있습니다.
    우리와
    앱에서 세션 ID를 Keychain/Keystore에 두고, 로그아웃 시 기기와 서버 양쪽에서 지우는 근거입니다.

    OWASP Password Storage Cheat Sheet

    OWASP 실무 지침
    다루는 것
    비밀번호 저장 방법. Argon2id(1순위), scrypt, bcrypt(작업 계수 10 이상, 72바이트 제한), PBKDF2(FIPS 환경, 600,000회) 순으로 권합니다. SHA-256 같은 빠른 해시는 부적합하다고 명시합니다.
    우리와
    해시 알고리즘 선택의 근거입니다.

    NIST SP 800-63B-4

    미국 표준기술연구소 · 2025년 확정
    다루는 것
    디지털 인증 지침. 비밀번호 규칙(3.1.1.2: 단독 사용 15자, 64자 이상 허용, 조합 규칙과 주기 변경 금지, 유출 목록 대조, 시도 제한), 인증 보증 수준 AAL1~3, 등급별 세션 타임아웃, 패스키 같은 피싱 저항 인증.
    성격
    미 연방기관에는 의무, 민간에서는 세계적 사실상 기준. ASVS의 세션 요구사항도 상당 부분 이 문서를 따릅니다.
    우리와
    비밀번호 정책과 타임아웃 수치의 근거입니다.

    개인정보의 안전성 확보조치 기준

    개인정보보호위원회 고시 · 법적 의무
    다루는 것
    개인정보보호법 제29조를 근거로 한 기술적·관리적 보호조치의 세부 기준. 접근 권한 관리(제5조), 접근 통제(제6조: IP 제한, 외부 접속 시 안전한 인증수단, 무활동 자동 차단), 암호화(제7조), 접속기록 보관과 점검(제8조) 등. 2025년 10월 31일 개정판이 시행됐습니다.
    성격
    유출 사고 시 과징금과 손해배상 판단의 핵심. 일반 사용자(정보주체)는 외부 접속 안전한 인증수단 의무에서 제외됩니다.
    우리와
    관리자 기능에 직접 적용됩니다. 조항 번호는 2023년 고시 구조 기준이라 개정판 원문과 대조가 필요합니다.

    ISMS-P

    KISA·개인정보위 인증 기준
    다루는 것
    정보보호 및 개인정보보호 관리체계 인증. 계정 관리(2.5.x), 응용프로그램 접근(2.6.3: 세션 자동 차단, 동시 세션 제한, 관리자 페이지 노출 금지), 로그 관리(2.9.4) 등.
    성격
    일정 규모 이상 사업자는 의무, 그 외에는 선택.
    우리와
    지금은 의무가 아니지만, 고시를 실무적으로 지키는 방법의 참고 기준입니다.

    W3C DBSC

    W3C 초안 · Chrome 출시
    다루는 것
    브라우저가 세션 쿠키를 기기의 보안 칩 키에 묶는 기능. 쿠키를 5분 같은 단기로 두고 키 서명으로만 갱신해, 쿠키를 훔쳐 가는 인포스틸러를 무력화합니다. Chrome(Windows)에서 시작했고 Safari, Firefox는 검토 단계라 Baseline이 아닙니다.
    우리와
    일반 사용자는 지원 브라우저에서만 켜지는 덤, 관리자 브라우저 접속에는 필수입니다.

    세션 결정문

    이 프로젝트의 내부 결정
    다루는 것
    "보안 최우선이면 JWT+DPoP 대신 서버 세션을 쓴다"는 결정과 그 조건 ①~⑮. 예: ① 세션 ID 해시 저장, ③ __Host- 쿠키와 Origin 검사, 익명 세션 미생성, ⑫ 이메일 복구 후 전체 폐기와 쿨다운, ⑮ 관리자 기기 바인딩 필수.
    우리와
    표에서 "선택" 등급의 출처입니다.