[취업반 스터디 1달 3주차: 과제]

Minseok Jo·2023년 11월 11일
post-thumbnail

1달차 3주차 과제는 다음 4가지와 같다.

  1. 복습 (로그인 로직 이해, 식별/인증)
  2. 밀린 과제 하기
  3. 로그인 페이지 로직 4가지 적용해서 만들기
  4. 추가과제
    + JWT 찾아보기
    + JWT 로그인 제작

1번 복습 내용은 "스터디 정리" 카테고리에 기록해두었다.
따라서 3번 과제부터 정리하였다.


1. 로그인 페이지 로직4가지 적용해서 제작

앞서 수업내용 정리에서 언급하였던 로그인 구조 4가지를 적용해서 각각 페이지를 제작해보고자 한다.
먼저, 회원가입 되어 DB에 저장되어 있는 유저의 정보는 다음과 같다.

members 테이블 내용
○ '유저'의 비밀번호는 md5로 해시처리 되어 DB에 저장되어 있다.

1) 식별/인증 동시

<?php
	$id = $_POST["id"];
    $pass = $_POST["pass"];
    
    $con = mysqli_connect("localhost", "user", "12345", "sample");
    
    $sql = "select * from members where id='$id' && pass='$pass'";
    $result = mysqli_query($con, $sql);
    
    if (mysqli_num_rows($result))
    	echo "<script>window.alert('로그인 성공')</script>";
    else
    	echo "<script>window.alert('로그인 실패')</script>";
?>

2) 식별/인증 분리

<?php
	$id = $_POST["id"];
    $pass = $_POST["pass"];
    
    $con = mysqli_connect("localhost", "user", "12345", "sample");
    
    $sql = "select pass from members where id='$id'";
    $result = mysqli_query($con, $sql);
    
    $row = mysqli_fetch_assoc($result);
    $db_pass = $row["pass"];
    
    if ($db_pass == $pass)
    	echo "<script>window.alert('로그인 성공')</script>";
    else
    	echo "<script>window.alert('로그인 실패')</script>";
?>

해시를 적용하는 경우에는 비밀번호를 받아와 변수에 대입할 때 해시함수를 적용해주기만 하면 된다.


3) 식별/인증 동시 with 해시

<?php
	$id = $_POST["id"];
    $pass = md5($_POST["pass"]); // md5로 해시 처리
    
    $con = mysqli_connect("localhost", "user", "12345", "sample");
    
    $sql = "select * from members where id='$id' && pass='$pass'";
    $result = mysqli_query($con, $sql);
    
    if (mysqli_num_rows($result))
    	echo "<script>window.alert('로그인 성공')</script>";
    else
    	echo "<script>window.alert('로그인 실패')</script>";
?>

4) 식별/인증 분리 with 해시

<?php
	$id = $_POST["id"];
    $pass = md5($_POST["pass"]); // md5로 해시 처리
    
    $con = mysqli_connect("localhost", "user", "12345", "sample");
    
    $sql = "select pass from members where id='$id'";
    $result = mysqli_query($con, $sql);
    
    $row = mysqli_fetch_assoc($result);
    $db_pass = $row["pass"];
    
    if ($db_pass == $pass)
    	echo "<script>window.alert('로그인 성공')</script>";
    else
    	echo "<script>window.alert('로그인 실패')</script>";
?>

이렇게 첫번째 과제인 '로그인 로직 4가지 개발' 과제가 완료되었다.


2. JWT 찾아보기

JWT는 들어본 적은 있지만, 상세한 정보는 이번 기회에 찾아보면서 알게되었다.
여러가지 포스트를 찾아보면서 알게된 내용을 최대한 자세히 정리해보았다!

1) JWT란?

JWT:
Json Web Token의 약자로, JSON 형식으로 작성된 토큰을 기반으로 인증하는, 인터넷 표준 인증 방식을 의미한다.

즉, 인증에 필요한 정보들을 토큰에 담아, 암호화시켜 사용하는 방식이 JWT이다.



2) JWT의 구조
JWT는 헤더, 페이로드, 서명의 3가지 파트로 구성되며, 각 구성요소는 점(.)으로 구분된다.

JWT의 구조


  • 헤더

헤더(header): 토큰의 타입이나, 어떤 해시 알고리즘이 사용되었는지 저장된다.

{
	"typ": "JWT"
    "alg": "HS256"
}

위 예시의 경우, 토큰의 타입은 JWT, 해시 알고리즘은 HS256 을 의미한다.



  • 페이로드

페이로드(Payload): 토큰에 담을 클레임(Claim) 정보를 포함한다.

클레임(Claim): 페이로드에 담는 정보의 한 '조각'을 의미하며, Key/Value의 한쌍으로 이루어져 있다.

토큰에는 여러개의 클레임을 넣을 수 있으며, 저장되는 정보에 따라 등록된(Registered) 클레임, 공개(Public) 클레임, 비공개(Private) 클레임으로 구분된다.

  • 등록된 클레임
    : 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기 위하여 이름이 이미 정해진 클레임들을 의미한다.

    <등록된 클레임의 표준 스펙은 다음과 같다>
    iss: 토큰 발급자 (issuer)
    sub: 토큰 제목 (subject)
    aud: 토큰 대상자 (audience)
    exp: 토큰 만료시간 (expiration)
    nbf: 토큰 활성 날짜 (Not Before)
    iat: 토큰 발급 시간 (issued at)
    jti: JWT의 고유 식별자 (JWT iD: issuer가 여러명일 때, 이를 구분하기 위한 값)

    상황에 따라 위의 7개 중에서 필요한 것을 선택하면 되며, 이외에도 필요에 따라 추가를 하는 것 또한 가능하다.
ex)
{
	"token_type": "access token"
}    
  • 공개 클레임
    : 표준 클레임에 덧붙여 공개되어도 무방한 페이로드를 의미한다.

    이름 충돌을 방지하기 위해 IANA "JSON Web Token Claims" 레지스트리에 클레임 이름을 등록하거나, 주로 URI 형식으로 정의한다.
ex)
{
	"http://example.com/is_root": true
}
  • 비공개 클레임
    : JWT 발급자와 사용자 간에 사용하기로 약속한 클레임을 의미한다.

    비공개 클레임 또한 이름 충돌이 발생하지 않도록 주의해야 한다.
ex)
{
	"username": "lowangle"
}    

따라서 클레임들로 구성된 페이로드의 한 예는 다음과 같다.

{
	"iss": "example.com",
    "exp": "1829120000000",
    "http://example.com/is_root": true,
    "username": "lowangle"
}    

위 예의 경우, 2개의 등록된 클레임, 1개의 공개 클레임, 1개의 비공개 클레임으로 구성되게 된다.


페이로드에서 중요한 것은, 민감한 정보를 담지 않는 것이다.
Header와 Payload의 경우 JSON이 인코딩 되어있을 뿐, 암호화가 된 것이 아니므로 누구나 그 데이터를 열람해볼 수 있기 때문이다.
JWT 디코딩
○ Header와 Payload는 암호화 없이 Base64 방식으로 인코딩을 하기 때문에 디코딩을 통해 위와 같이 그 값을 확인할 수 있다.
● 따라서 보안을 위해 JWT에는 "식별을 위한" 정보만을 담아두어야 한다.


  • 서명

서명(Signature): 헤더, 페이로드를 Base64로 인코딩한 후, Header에 명시된 해시함수를 적용하고, 개인키로 서명한 전자서명이 담겨있다.

Header와 Payload는 단순히 Base64로 인코딩되어 있기 떄문에 공격자가 원하는 값을 넣고 생성할 수 있다.
따라서 생성된 토큰이 유효한지 검증하는 수단으로써 시그니처가 사용된다.

시그니처를 만드는 구조는 다음과 같다.

HMACHSA256 (
	base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    "secret"
)    
  • 헤더에서 "alg": "HS256" 으로 선언하였으므로, HMACSHA256 알고리즘으로 토큰을 암호화 하고 있다.
  • Header와 Payload를 인코딩한 값을 합치고, 이를 secret 즉, 토큰을 생성하고 검증하는 서버가 가지고 있는 개인키로 암호화한다.

시그니처는 서버에 있는 개인키로만 복호화가 가능하므로, 다른 클라이언트는 임의로 시그니처를 복호화할 수 없다.
이러한 이유로 만약 공격자가 Payload를 변조하더라도, 시그니처에 작성된 페이로드 값(변조 불가능)과 다르기 때문에 인증이 불가능하게 되는 것이다.



3) JWT의 동작원리
JWT 토큰의 동작원리는 다음과 같다.

JWT의 동작원리
1. 사용자가 id 와 pw 로 로그인을 요청한다.
2. 서버에서 회원 DB를 통해 사용자를 인증한다.
3. 인증이 확인되면 secret 키를 통해 토큰을 발급한다.
4. 발급된 토큰을 클라이언트에 전달한다.
5. 이후 클라이언트에서 서비스를 요청할 때, 헤더에 토큰을 담아서 전달한다.
6. 서버는 토큰의 Signature를 체크하여 토큰이 유효한지 검증한다.
7. 클라이언트 요청에 대한 응답과 데이터를 전달한다.

이처럼 토큰 기반 인증방식은 사용자의 인증이 완료된 이후에 토큰을 발급한다.
클라이언트에서는 전달받은 토큰을 저장해두고, 서버에 요청할 때마다 토큰을 함께 전달함으로써 인증을 수행하게 된다.



4) JWT의 장/단점

장점
1. 이미 토큰 자체가 인증된 정보이기 때문에 세션 저장소와 같은 별도의 인증 저장소가 "필수적"으로 필요하지 않다.
2. 세션과는 달리 클라이언트의 상태를 서버가 저장해두지 않아도 된다.
3. 데이터의 위변조를 방지할 수 있다.

단점
1. 쿠키/세션과 달리 토큰의 길이가 길어, 인증 요청이 많을수록 네트워크 부하가 심해진다.
2. Payload 자체는 암호화가 되지 않아 중요한 정보는 담을 수 없다.

이렇게 두번째 과제인 'JWT 찾아보기' 과제가 완료되었다.


3. JWT 기반 로그인 제작

앞서 정리한 JWT를 사용하여 로그인 인증 로직을 제작해보고자 한다.
클래스를 직접 정의하여 JWT를 구현하는 방법과, PHP에서 제공하는 JWT 라이브러리를 사용하는 방법 2가지로 각각 구현해보았다.


1) JWT 클래스를 정의하여 구현

이 방법에서는 JWT를 생성하고, 검증하는데 필요한 모든 클래스(객체)와 함수를 직접 정의하여 구현하였다.

  • JWT 클래스 페이지
    JWT토큰 생성 · 인코딩 · 디코딩 · 값 추출에 필요한 클래스 및 함수가 선언되어 있는 페이지이다.
    사실상 기능의 전부를 담당하고 있는 페이지로서, 다른 페이지에서 참조(include)할 수 있도록 제작하였다.
// jwt.php
<?php
    // JWT 클래스
    class jwt {
        protected $alg;

        function __construct() {
            $this->alg = "sha256";  // 해시 알고리즘: sha256
        }

        // 해싱하여 Base64로 반환
        function hashing(array $data) {
            // 토큰 헤더 작성
            $header = json_encode(array(
                "alg"=>$this->alg,
                "typ"=>"JWT"
            ));

            // 토큰 페이로드 작성
            $payload = json_encode($data);

            // 토큰 시그니처 작성
            $signature = hash($this->alg, $header.$payload);

            // Base64 인코딩값 반환
            return base64_encode($header.".".$payload.".".$signature);
        }

        // 토큰 유효성 검사 및 페이로드 값 반환
        function dehashing($token) {
            // 토큰을 base64로 디코드 후, '.' 을 기준으로 분리
            $split = explode(".", base64_decode($token));
            
            // 시그니처 부분 추출
            $signature = $split[2];
			
            // 토큰 유효성 검사 및 페이로드 반환
            if (hash($this->alg, $split[0].$split[1]) == $signature)
                return json_decode($split[1], true);
        }
    }

    // 토큰 쿠키 생성
    function set_cookie($cookie_name, $value, $expire) {
        setcookie(md5($cookie_name), base64_encode($value), time()+$expire, '/');
    }

    // 쿠키이름을 받아 해당 쿠키값 반환
    function get_cookie($cookie_name) {
        $cookie = md5($cookie_name);

        if (array_key_exists($cookie, $_COOKIE))
            return base64_decode($_COOKIE[$cookie]);
        else
            return "";
    }
?>

위의 php파일은 클래스 내 메소드 2개, 사용자 정의 함수 2개로 이루어져 있다.
각 함수들의 결과값을 출력해보면 다음과 같다.

hashing()에서 생성되는 토큰의 구조
위처럼 Header, Payload, Signature로 구성된 JWT토큰값이, Base64로 인코딩되어 반환되게 된다.

dehashing()의 결과
JWT토큰의 Payload 값으로서, 배열의 형태를 가진다.

set_cookie()의 결과
<해시된 쿠키이름> = <base64로 인코딩된 Payload> 형태의 쿠키가 클라이언트 측에 저장되게 된다.

get_cookie()의 결과
hashing() 함수에서 만들어진 토큰의 base64 인코딩 값이 반환된다.



  • 로그인 처리 페이지

위에서 정리하였던 JWT 토큰의 동작원리에서, 로그인 인증에 성공시 JWT 토큰이 발행되었다.
"로그인 처리 페이지"는 아이디와 비밀번호를 바탕으로 식별 · 인증을 수행하고, 인증되었다면 토큰을 생성해주는 역할을 한다.

// login.php
<?php
    include "jwt.php";

    $id = $_POST["id"];
    $pass = $_POST["pass"];

    $con = mysqli_connect("localhost", "user", "12345", "sample");
    $sql = "select * from members where id='$id' && pass='$pass'";
    $result = mysqli_query($con, $sql);

    $num_match = mysqli_num_rows($result);

    if (!$num_match) {
        echo "<script>
        window.alert('아이디 혹은 비밀번호를 확인하여주십시오.')
        history.go(-1)
        </script>";
        exit;
    }

    $row = mysqli_fetch_assoc($result);
    $user_name = $row["name"];
    mysqli_close($con);


    $jwt = new jwt();
    $login_token = $jwt->hashing(array(
        "user_id"=> $id,
        "user_name"=> $user_name
    ));
    
    set_cookie("token", $login_token, 2592000);

    echo "<script>location.href='index.php';</script>";
?>

식별 · 인증을 동시에 수행하는 방식이며, 로그인 성공시 토큰이 쿠키에 담겨 클라이언트에 저장된다.
저장되는 형식은 위에서 정리한 set_cookie() 함수의 결과 사진과 같다.



  • 로그인 확인 페이지
    쿠키로 저장된 JWT토큰을 기반으로 사용자 인증을 수행한다.
    JWT 토큰 유무 (로그인/비로그인) 상태에 따라 출력되는 페이지가 다르게 나타난다.
// index.php
<?php
    include "jwt.php";

    $login_token = get_cookie("token");
    $jwt = new jwt();
    $jwt_data = $jwt->dehashing($login_token);

    $userid = $jwt_data["user_id"];
    $username = $jwt_data["user_name"];
?>

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>메인페이지</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div class="header">
            <h3 class="logo">
                <a href="index.php">취업반스터디</a>
            </h3>
            <div class="top">
                <?php
                    if (!$userid) {
                        ?>
                        <span><a href="form.php">회원가입</a></span>
                        <span> | </span>
                        <span><a href="login_form.php">로그인</a></span>
                <?php
                    }                
                    else {
                        $logged = $username."(".$userid.")";
                        ?>
                        <span><?=$logged?></span>
                        <span> | </span>
                        <span><a href="logout.php">로그아웃</a></span>
                        <span> | </span>
                        <span><a href="modify_form.php">마이 페이지</a></span>
                <?php        
                    }
                        ?>
            </div>
        </div>
    </body>
</html>

쿠키로 저장된 토큰을 dehashing() 함수를 통해 토큰의 유효성을 검사하고,
토큰이 유효한 경우, 유저의 정보를 가져와 페이지를 출력해준다.

토큰이 유효한 경우토큰이 유효하지 않은 경우
토큰에서 사용자 정보를 가져와 페이지 출력토큰이 없는 상태이므로 비로그인 페이지 출력



2) JWT 라이브러리를 이용하여 구현

PHP에서 JWT를 사용하려면 먼저 JWT 라이브러리를 설치해야 한다.
JWT를 사용할 PHP 파일들이 있는 위치에 composer를 사용하여 firebase/php-jwt 라이브러리를 설치해주었다.

composer require firebase/php-jwt

위 명령어를 실행하면 composer.json , composer.lock , vendor/ 가 설치되고 JWT 라이브러리를 사용할 수 있게 된다.


  • 로그인 처리 페이지

JWT클래스를 사용하였을 때와 같이 로그인에 성공하면 JWT토큰을 발행해주는 페이지이다.

// login.php
<?php
    $id = $_POST["id"];
    $pass = $_POST["pass"];

    $con = mysqli_connect("localhost", "user", "12345", "sample");
    $sql = "select * from members where id='$id' && pass='$pass'";
    $result = mysqli_query($con, $sql);

    $num_match = mysqli_num_rows($result);

    if (!$num_match) {
        echo "<script>
        window.alert('아이디 혹은 비밀번호를 확인하여주십시오.')
        history.go(-1)
        </script>";
        exit;
    }

    
    $row = mysqli_fetch_assoc($result);
    $user_name = $row["name"];
    mysqli_close($con);

    include "vendor/autoload.php";
    use Firebase\JWT\JWT;
    
    $key = "secret_key";
    $payload = array(
    "user_id" => $id,
    "user_name" => $user_name
    );
    
    $jwt = JWT::encode($payload, $key, 'HS256');
    setcookie(md5("token"), $jwt, time()+60*5, '/');

    echo "<script>location.href='index.php';</script>";
?>

개인키와 페이로드를 작성한 뒤, JWT라이브러리에서 제공하는 encode() 함수로 HS256 알고리즘을 적용해 JWT토큰을 생성하였다.
$jwt 의 값을 출력해보면 다음과 같다.

JWT::encode()로 생성한 토큰의 값
이렇게 생성된 토큰의 값이 setcookie()함수에 의해 쿠키로 클라이언트 측에 저장된다.


  • 로그인 확인 페이지

이번에도 마찬가지로 토큰 생성 여부를 쿠키로 확인하여 그에 맞는 페이지를 출력해준다.

// index.php
<?php
    include "vendor/autoload.php";
    use Firebase\JWT\JWT;
    use Firebase\JWT\Key;

    $cookie = md5("token");
    $key = "secret_key";
    
    if (array_key_exists($cookie, $_COOKIE)) {
        $jwt = $_COOKIE[$cookie];
    	$jwt_data = JWT::decode($jwt, new Key($key, 'HS256'));
    	$userid = $jwt_data->user_id;
    	$username = $jwt_data->user_name;
    }
?>

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>메인페이지</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div class="header">
            <h3 class="logo">
                <a href="index.php">취업반스터디</a>
            </h3>
            <div class="top">
                <?php
                    if (!$userid) {
                        ?>
                        <span><a href="form.php">회원가입</a></span>
                        <span> | </span>
                        <span><a href="login_form.php">로그인</a></span>
                <?php
                    }                
                    else {
                        $logged = $username."(".$userid.")";
                        ?>
                        <span><?=$logged?></span>
                        <span> | </span>
                        <span><a href="logout.php">로그아웃</a></span>
                        <span> | </span>
                        <span><a href="modify_form.php">마이 페이지</a></span>
                <?php        
                    }
                        ?>
            </div>
        </div>
    </body>
</html>

쿠키로서 JWT 토큰이 존재한다면, JWT라이브러리의 decode() 함수로 유저의 정보를 추출해 그에 맞는 페이지를 출력해준다.
여기서 JWT::decode() 함수의 결과는 다음과 같다.

JWT::decode() 함수 결과값
JWT라이브러리의 decode()함수의 결과값으로 문자열 인덱스 배열이 반환된다.

이렇게 디코딩된 정보를 바탕으로 로그인 화면을 출력해주게 된다.

토큰이 유효한 경우토큰이 유효하지 않은 경우
토큰에서 사용자 정보를 가져와 페이지 출력토큰이 없는 상태이므로 비로그인 페이지 출력

이렇게 세번째 과제인 'JWT기반의 로그인 구현' 과제가 완료되었다.


0개의 댓글