Almon Dev

JWT로 로그인 구현하기 본문

모의해킹/웹 개발

JWT로 로그인 구현하기

Almon 2024. 11. 3. 21:02

 

JWT로 로그인 구현하기

 

php에서 jwt를 사용하기 위해서는 firebase/php-jwt라는 패키지를 사용해야 합니다.
php-jwt를 설치하기 위해서는 php의 패키지 관리 툴인 composer를 사용해야 합니다.

composer 설치하기

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer

 

php-jwt 설치하기

// 웹루트 폴더로 이동해서 해야합니다
composer require firebase/php-jwt

 

JWT 클래스

jwt_auth.php

<?php
require_once 'vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;

class Jwt_auth {
    private static $secret_key = "DA5A431AD2AA76963B2DF723264CECCCDEF";
    private static $algorithm = "HS256";
    private static $cookie_name = "token";
    private static $login_page = "login2.php";
    private static $login_scess_page = "login_successful.php";

    public static function auth(): bool|stdClass {
        if(isset($_COOKIE[self::$cookie_name])){
            $token = $_COOKIE[self::$cookie_name];
            try {
                // 디코딩과 서명 검증
                $token_d = JWT::decode($token,new Key(
                    self::$secret_key, self::$algorithm
                    ));
                return $token_d;
            } catch (Exception $e) {
                // 검증실패
            }
        }
        return false;
    }
    
    public static function createToken($id):bool|string {
        $payload = [
            "sub" => $id,
            "iat" => time(),
            "exp" => time() + (60 * 60),
        ];
        try {
            $token = JWT::encode($payload, self::$secret_key, self::$algorithm);
            setcookie("token", $token, time() + (60 * 60),"/");
        } catch (Exception $e) {
            return false;
        } 
        return $token;
    }

    public static function delToken() {
        setcookie(self::$cookie_name,"", time() - (60 * 60),"/");
    }
}


?>

 

require_once 'vendor/autoload.php';

vendor/autoload.php는 composer로 설치한 패키지를 자동으로 포함해 줍니다.

 

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;

use를 이용해서 본래는 \Firebase\JWT\JWT 이렇게 써야 할 것을 JWT로 쓸 수 있도록 네임스페이스를 불러옵니다.

 

class Jwt_auth {

jwt의 기능을 관리하기 위해서 Jwt_auth라는 이름의 클래스를 정의합니다.

 

    private static $secret_key = "DA5A431AD2AA76963B2DF723264CECCCDEF";
    private static $algorithm = "HS256";
    private static $cookie_name = "token";
    private static $login_page = "login2.php";
    private static $login_scess_page = "login_successful.php";

클래스가 가지는 속성들을 정의합니다.

변수의 앞에 붙은 private static은 접근 제어자와 그 외 수정자를 의미합니다.

접근 제어자
 객체의 속성, 메서드에 대한 접근 범위를 수정합니다.

public : 클래스의 외부에서 이 속성에 접근할 수 있습니다.
ex) $jiho = new Student(); echo jigo->grades;

private : 클래스의 내부에서만 접근할 수 있습니다.
=> 외부에서 속성에 직접 접근할 수 없어서 값을 받아오는 메서드를 따로 만들어줘야 합니다.
ex) $jiho = new Student(); echo jigo->grades (X)  echo jigo->getGrades() (O)

protected : 클래스의 내부와 상속받은 자식 클래스에서 접근할 수 있습니다.
=> 외부에서는 접근이 불가능하고, 자식 클래스의 메서드와 클래스 자신의 메서드에서만 접근이 가능합니다.

수정자
클래스, 메서드, 속성의 동작방식과 특성을 지정합니다.
=> 접근 제어자도 수정자에 포함됩니다.

static : 클래스를 생성하지 않고도 사용할 수 있습니다.
=> new 명령어로 객체를 생성하지 않고도 내부의 속성이나 메서드에 접근할 수 있습니다.
=> 클래스가 static이 아니어도 static인 메서드를 사용할 수 있습니다.
=> static은 객체를 생성하지 않아도 메모리에 존재하므로 남발하면 성능에 문제가 생깁니다.
ex) class Stduent { static function grades(); } $score = Student::grades();

final : final 클래스는 클래스가 상속할 수 없다는 뜻이고 final 메서드는 오버라이딩(덮어쓰기)이 불가능합니다.
ex) final class Student {} $yoin = new Student() (X)


abstract : abstract 클래스는 추상 클래스로 상속 시 하나 이상의 메서드를 직접 구현해야 한다는 뜻입니다.
ex) abstract class Student { abstract  function grades();}

 

auth()

    public static function auth(): bool|stdClass {
        if(isset($_COOKIE[self::$cookie_name])){
            $token = $_COOKIE[self::$cookie_name];
            try {
                // 디코딩과 서명 검증
                $token_d = JWT::decode($token,new Key(
                    self::$secret_key, self::$algorithm
                    ));
                return $token_d;
            } catch (Exception $e) {
                // 검증실패
            }
        }
        return false;
    }

토큰이 존재하는지, 존재한다면 유효한 토큰인지 검사한 뒤 디코딩된 토큰을 반환하는 메서드입니다.

 

    public static function auth(): bool|stdClass {

외부에서 접근 가능한 static 메서드인 auth를 정의합니다. :bool|stdClass는 반환 타입을 나타냅니다.

bool인 true, false와 stdClass인 JWT토큰을 디코딩한 값을 반환합니다.

 

        if(isset($_COOKIE[self::$cookie_name])){
            $token = $_COOKIE[self::$cookie_name];

self::$cookie_name은 클래스의 static 속성인 $cookie_name의 값을 불러오는 것입니다.

token이라는 이름을 가진 쿠키가 있는지 확인하고 $token에 값을 대입합니다.

 

            try {
                // 디코딩과 서명 검증
                $token_d = JWT::decode($token,new Key(
                    self::$secret_key, self::$algorithm
                    ));
                return $token_d;

 

JWT의 static 메서드인 decode를 이용해서 서명을 검증하고 검증이 완료된 토큰을 디코딩해서 $token_d에 대입합니다.

decode함수는 인자로 토큰과 시크릿키, 암호화에 사용된 알고리즘을 받습니다.

그리고 return으로 디코딩된 stdClass타입의 토큰값을 반환합니다.

 

            } catch (Exception $e) {
                // 검증실패
            }
        }
        return false;

try문에서 에러가 날 경우 catch에서 에러를 잡아와서 {} 안에 코드를 실행합니다.

검증이 실패하거나 토큰이 없는 경우 false를 리턴합니다.

 

createToken()

    public static function createToken($id):bool|string {
        $payload = [
            "sub" => $id,
            "iat" => time(),
            "exp" => time() + (60 * 60),
        ];
        try {
            $token = JWT::encode($payload, self::$secret_key, self::$algorithm);
            setcookie("token", $token, time() + (60 * 60),"/");
        } catch (Exception $e) {
            return false;
        }  
        return $token;
    }

유저의 ID값을 받아와서 jwt토큰을 만들어서 반환하는 메서드입니다.

 

    public static function createToken($id):bool|string {

public static 함수인 createToken을 정의합니다. 반환값은 bool과 string입니다.

 

        $payload = [
            "sub" => $id,
            "iat" => time(),
            "exp" => time() + (60 * 60),
        ];

jwt토큰에서 사용될 페이로드로, sub는 ID를 iat는 생성시간을 exp는 만료시간을 의미합니다.

 

        try {
            $token = JWT::encode($payload, self::$secret_key, self::$algorithm);
            setcookie("token", $token, time() + (60 * 60),"/");

JWT클래스의 static 메서드인 encode를 이용해서 토큰을 생성해 $token에 저장합니다.

encode는 페이로드와 비밀키, 암호화 알고리즘은 인수로 받습니다.

setcookie를 이용해서 브라우저에 토큰을 저장합니다.

 

        } catch (Exception $e) {
            return false;
        }  
        return $token;
    }

생성에 실패할 경우 false를 반환하고, 성공할 경우 만들어진 jwt토큰을 반환합니다.

 

delToken()

    public static function delToken() {
        setcookie(self::$cookie_name,"", time() - (60 * 60),"/");
    }

delToken은 브라우저의 쿠키에 저장된 토큰을 제거하는 메서드입니다.

setcookie를 이용해 쿠키의 만료시간을 1시간 전으로 설정해 브라우저에서 삭제합니다.


로그인 페이지

login2.php

<?php
    //JWT
require_once("jwt_auth.php");
if(Jwt_auth::auth()) {
    header("Location: ".self::$login_scess_page);
    exit;
};

?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Almond Login</title>
    <link rel="stylesheet" href="/css/login2.css">
</head>
<body>
    <container class="login-container">
        <form class="login-form" action="login_proc2.php" method="post">
            <div class="logo-container">
                <a href="/login2.php"><img src="/imgs/almond.png" alt="logo" class="logo"></a>
            </div>
            <h2>Almond</h2>
            <input type="text" class="login-input" name="id" placeholder="아이디 또는 전화번호" required>
            <input type="password" class="login-input" name="passwd" placeholder="비밀번호" required>
            <?php
            if (isset($_GET['result'])) {
                $result = $_GET['result'];
                if ($result == "failed") {
                    echo '<p class="login-failed">로그인에 실패하셨습니다.</p>';
                }
            }
            ?>
            <button type="submit" class="login-submit">로그인</button>
        </form>
    </container>
    <div class="login-more">
        <a href="#">아이디 찾기</a>
        <a href="#">비밀번호 찾기</a>
        <a href="sign_up.php">회원가입</a>
    </div>
    <footer>
        <a href="https://www.flaticon.com/kr/free-icons/" title=" 아이콘" class="logo-source"> 아이콘 제작자: Freepik - Flaticon</a>
    </footer>
</body>
</html>

 

<?php
    //JWT
require_once("jwt_auth.php");
if(Jwt_auth::auth()) {
    header("Location: ".self::$login_scess_page);
    exit;
};

?>

jwt_auth.php를 require 해서 auth() 메서드로 토큰이 인증에 성공하면 로그인 프로필 페이지로 이동합니다.

 

login_proc2.php

<?php
    include("mysql.php");
    require("jwt_auth.php");
    use Firebase\JWT\JWT;    
    
    $id = $_POST["id"];
    $pass = $_POST["passwd"];
    $url = '/login_successful.php';
    $sql = "select nickname from users where id='$id' and password='$pass';";
    
    $sql_result = runSQL($sql);
    
    if ($row = mysqli_fetch_array($sql_result)) {
        $nick = $row['nickname'];
        $update_sql = "update users set login=1 where nickname='$nick';";
        runSQL($update_sql);

        // JWT 생성
        // $secret_key = secretKey();
        // $payload = [
        //     "iat" => time(),
        //     "exp" => time() + (60 * 60),
        //     "user_id" => $id,
        // ];
        $token = Jwt_auth::createToken($id);

        header("location:$url");
        exit;
    }
    
    header('Location: login2.php?result=failed');
?>

 

    include("mysql.php");
    require("jwt_auth.php");
    use Firebase\JWT\JWT;

mysql.php와 jwt_auth.php를 불러옵니다.

use를 이용해서 JWT 클래스의 네임 스페이스를 불러옵니다.

 

        $token = Jwt_auth::createToken($id);
        header("location:$url");
        exit;

Jwt_auth클래스의 createToken메서드를 이용해 토큰을 생성하고, 프로필 페이지로 이동합니다.


프로필 페이지

login_successful.php

<?php
require_once("jwt_auth.php");
require_once("mysql.php");

$nickname = "";

if($token = Jwt_auth::auth()){
    try {
        //인증 성공
        $user_id = $token->sub;
        $sql = "select nickname from users where id='$user_id'";
        $nickname = runSQL($sql)->fetch_array()['nickname'];
    } catch(Exception $e) {
        // 인증 실패
    }
}else {
    header("Location: login2.php");
    exit;
}
?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Almond</title>
    <link rel="stylesheet" href="/css/login_successful.css">
    <script src="./script/logout.js" defer></script>
    <script src="./script/mypage.js" defer></script>
</head>
<body>
    <container class="profile-container">
        <div class="profile">
            <img src="/imgs/almond-profile.jpg" class="profile_img">
            <div class="profile_info">
                <h2 class="profile_name"><?= $nickname; ?></h2>
                <p class="profile_subs">구독자 <strong>0명</strong></p>
            </div>
        </div>
        
        <div class="profile_link">
            <a href="#" class="link_tab"><p>글쓰기</p></a>
            <!-- <a href="mypage.php" class="link_tab"><p>마이페이지</p></a> -->
            <?php
                printf('<button class="link_tab mypage-btn" onclick=\'mypage("%s")\'>마이페이지</button>', $nickname);
            ?>
            <a href="logout.php" class="link_tab"><p>로그아웃</p></a>
        </div>
        <div class="content"></div>
        <button class="mypageset-btn">수정하기</button>
    </container>
</body>
</html>

 

if($token = Jwt_auth::auth()){
    try {
        //인증 성공
        $user_id = $token->sub;
        $sql = "select nickname from users where id='$user_id'";
        $nickname = runSQL($sql)->fetch_array()['nickname'];
    } catch(Exception $e) {
        // 인증 실패
    }
}else {
    header("Location: login2.php");
    exit;
}

Jwt_auth를 이용해서 토큰을 인증하고 인증에 성공하면 닉네임을 가져온 뒤 프로필 페이지를 출력하고,

인증에 실패하면 로그인 페이지로 이동시킵니다.


로그아웃

logout.php

<?php
require_once("jwt_auth.php");
if(Jwt_auth::auth()){
    Jwt_auth::delToken();
}
header("location: login2.php");
?>

Jwt_auth의 delToken 메서드로 쿠키를 삭제합니다.


마무리