개요

이 글에서는 JSON Web Tokens (JWT)에 대해 이야기하려고 한다. JWT는 JSON 객체로 정보를 안전하게 주고받는 표준으로 이 정보들은 디지털 서명되어 있어서 검증하고 신뢰할 수 있다. 주로 인증이나 정보 교환에 사용된다.

 

JWT 제작 시 주의점

1. HTTPS는 필수

프로덕션에서는 항상 HTTPS를 사용해야 한다. JWT가 전송 중에 암호화되므로, 도청 공격으로부터 안전해진다.

2. 비밀키 보안 강화

JWT를 서명하는 데 사용하는 비밀 키는 항상 안전하게 보관해야한다. 만약 노출된다면 공격자가 토큰을 위조할 수 있어서 위험하다.

3. 토큰 만료 시간

 토큰에 만료 시간을 설정하는 것은 꼭 필요하다. 토큰이 노출되더라도 만료 시간이 설정되어 있다면 피해를 최소화할 수 있다.

4. 민감한 정보는 패스

JWT의 페이로드에 민감한 데이터를 넣지 않는 것을 추천한다. 만약 토큰이 노출되면 그 안의 데이터도 노출되기 때문이다.

5. 클라이언트에서 토큰 처리 잘하기

브라우저의 로컬 스토리지나 세션 스토리지에 토큰을 저장하는 것은 피해야 한다. JavaScript로 접근할 수 없는 HttpOnly 쿠키를 사용하는 것이 제일 좋다.

6. 에러 처리 꼼꼼하게

JWT가 유효하지 않거나 만료되었거나 존재하지 않는 경우, 서버는 반드시 적절하게 응답해야 한다.

구현

그럼 이제 TypeScript와 Express를 사용해서 Node.js 서버에서 JWT 인증을 어떻게 구현하는지 알아보자.

먼저, 필요한 npm 패키지를 설치해야 한다.

npm install jsonwebtoken express
npm install @types/jsonwebtoken @types/express --save-dev


다음은 JWT 인증 구현 예시이다.

import express, { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

const app = express();
const SECRET_KEY = "YOUR_SECRET_KEY";  // 비밀 키

// JWT를 확인하는 미들웨어
function authenticateToken(req: Request, res: Response, next: NextFunction) {
  // 헤더에서 토큰을 가져옵니다
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (token == null) return res.sendStatus(401); // 토큰이 없는 경우

  jwt.verify(token, SECRET_KEY, (err: any, user: any) => {
    if (err) return res.sendStatus(403);

    req.user = user;
    next();
  });
}

app.post('/login', (req, res) => {
  // 사용자 인증
  // 사용자 인증 로직 구현
  // 사용자 이름과 비밀번호를 req.body에서 가져오기 
  // 데이터베이스와 대조 (ex. bcrypt 활용)

  const username = req.body.username;
  const user = { name: username };

  // 액세스 토큰 생성
  const accessToken = jwt.sign(user, SECRET_KEY);
  res.json({ accessToken: accessToken });
});

app.get('/protected', authenticateToken, (req, res) => {
  // 이 경로는 JWT로 보호됩니다
  // 'authenticateToken'을 호출한 후에는 'req.user'에 사용자 정보가 있습니다
  res.json({ title: 'Protected content', user: req.user });
});

app.listen(3000, () => console.log('Server started on port 3000'));

 

토큰 저장

마지막으로, 액세스 토큰을 어디에 저장할지 결정하는 것도 중요하다. 이는 애플리케이션의 맥락과 보안 고려사항에 따라 달라진다. 각 방법의 장단점을 보고 판단하면 된다.

1. Cookie

쿠키에 토큰을 저장하는 것은 일반적인 방법이다. HttpOnly 쿠키는 JavaScript에서 접근할 수 없으므로 XSS 공격에 대해 안전하다. 하지만 CSRF 공격에 취약할 수 있으며, 이는 동기 토큰 패턴이나 SameSite 쿠키 속성과 같은 기법으로 완화할 수 있다.

2. LocalStorage

LocalStorage에 토큰을 저장하는 것은 쉽고 편리하며, CSRF 공격에 대해 안전하다. 하지만 JavaScript를 통해 접근할 수 있으므로 XSS 공격에 취약하다.

3. Private variable

JavaScript 클로저 내의 변수에 토큰을 저장하는 것은 가장 안전한 옵션이다. 이 방법은 토큰이 전역적으로 노출되지 않으며 XSS를 통해 접근할 수 없다. 하지만 이 방법은 페이지 간에 지속되지 않는다 즉, 사용자가 페이지를 이동하거나 새로 고침하면 토큰이 소실된다.
단일 페이지 애플리케이션 (SPA)의 맥락에서는 일반적인 패턴은 메모리에 액세스 토큰을 저장하고 (예: private variable), 만료될 때 새로운 액세스 토큰을 얻기 위해 HttpOnly 쿠키에 저장된 새로 고침 토큰을 사용하는 것이다. 이 방법은 사용성과 보안 사이에서 좋은 균형을 제공한다.

결론

어떤 솔루션도 100% 보안은 아니다. 애플리케이션의 특정 필요에 따라 최선의 방법이 결정된다. 패키지를 최신 상태로 유지하고, 사용자 입력을 sanitize하여 XSS 공격을 방지하며(ex. sanitize-html), 쿠키에 대해 HttpOnly와 Secure 플래그를 사용하고, CSRF 보호를 활성화하여 애플리케이션의 보안을 강화해야한다.

반응형

개요

DNN을 활용한 얼굴인식 프로그램을 제작하기 위해 Linux 내에서 환경설정이 필요했다.
Python 버전을 3.9.16으로 업그레이드하고 AI 및 데이터베이스 작업에 필요한 라이브러리, 즉 OpenCV와 MySQL Connector를 설치하는 방법에 대해 알아보자.

Python 3.9.16으로 업그레이드하기


첫 번째 단계는 Python 3.9.16으로 업그레이드하는 것이다.

해당 버전은 AI 라이브러리 cv2와 데이터베이스 작업을 이용하기 위해 필요하다. 아래 절차를 따라해보자.

1. 현재 Python 버전 확인

python --version


2. 리눅스 배포판의 패키지 목록 업데이트

sudo apt update

Linux 배포판에서 패키지 목록을 업데이트하는 명령어.

이 명령어로 설치 가능한 패키지들의 최신 버전 정보를 알 수 있다.

 

3. 설치된 패키지를 최신 버전으로 업그레이드

sudo apt upgrade

 

4. Python 컴파일 및 설치하기 위해 필요한 패키지 설치

sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget libbz2-dev

 

5. Python 3.9.16의 소스 코드 다운로드

wget https://www.python.org/ftp/python/3.9.16/Python-3.9.16.tgz

Python 3.9.16의 소스 코드를 다운로드하는 명령어이다.

해당 명령어를 사용하면 별도의 tgz파일 다운로드 및 이동 없이 바로 활용 가능하다.

 

6. 다운로드한 소스 코드 파일 압축 해제

tar -xf Python-3.9.16.tgz


7. Python 3.9.16 소스 코드가 있는 디렉토리로 이동

cd Python-3.9.16


8. Python 컴파일하기 전 설정

./configure --enable-optimizations

--enable-optimizations는 최적화를 적용하여 Python을 컴파일하는 옵션이다.


9. 소스 코드 컴파일

make -j $(nproc)

-j $(nproc) 은 사용 가능한 CPU 코어를 최대한 활용하여 컴파일하는 것을 가능하게 하는 옵션이다.


10. 컴파일된 Python 설치

sudo make altinstall

altinstall는기본 Python 바이너리를 대체하지 않고 새로운 버전을 설치하는 옵션이다.


11. /usr/bin/python으로 설정된 이전 Python 바이너리를 삭제

sudo rm /usr/bin/python

기존 바이너리를 삭제해야 기존 버전과 겹쳐서 나는 이슈를 없앨 수 있다.

 

12. 새로 설치한 Python 3.9를 /usr/bin/python에 연결

sudo ln -s /usr/local/bin/python3.9 /usr/bin/python

이렇게 하면 python 명령어를 입력했을 때 Python 3.9가 실행된다.

 

13. 3.9.16 버전으로 업그레이드 된 Python 버전 확인

python --version

 

OpenCV 설치하기


OpenCV는 수백 가지 컴퓨터 비전 알고리즘을 포함한 강력한 오픈소스 라이브러리이다.

AI 구현 혹은 활용하기 위해 필요하다. 아래 절차를 따라해보자.

1. 기존의 libapache2-mod-php7.3 패키지 삭제

sudo apt purge libapache2-mod-php7.3

OpenCV라이브러리를 설치할 때 libabache2 패키지가 최신으로 돼있지 않으면 오류가 날 경우가 있다.

따라서 기존 설치된 것을 삭제하고 재설치 하는 작업이 필요하다.

해당 명령어는 libapache2-mod-php7.3 패키지를 시스템에서 완전히 제거하는 명령어이다.

'purge'는 패키지를 삭제할 뿐만 아니라, 그와 관련된 설정 파일까지 모두 제거한다.


2. libapache2-mod-php7.3 패키지 다시 설치

sudo apt install libapache2-mod-php7.3

참고 : 이 패키지는 Apache 웹 서버에서 PHP 7.3을 사용할 수 있게 해주는 모듈이다.


3. OpenCV 라이브러리의 Python 3 바인딩을 설치합니다

sudo apt install python3-opencv


4. Python 패키지 관리자인 pip를 설치합니다

sudo apt install python-pip

pip를 이용하면 다양한 Python 패키지를 손쉽게 설치할 수 있다.


5. opencv-contrib-python 패키지 설치

pip install opencv-contrib-python

 

이 패키지는 OpenCV의 주요 기능뿐만 아니라, 추가적인 기능들을 제공한다.

코드 내부 cv2 라이브러리를 사용하기 위해 설치한다.

 

MySQL Connector 설치하기


MySQL Connector/Python은 MySQL에서 제공하는 표준화된 데이터베이스 드라이버이다.

AI를 통해 얻은 결과물을 데이터베이스에 저장하는 데 활용할 수 있다. 아래 명령어를 통해 설치할 수 있다.

pip install mysql-connector-python



결론


이제 Python 3.9.16으로 업그레이드하고 AI 및 데이터베이스 작업을 위한 핵심 라이브러리를 설정했다. 다음 단계로는 AI 및 데이터베이스 프로젝트를 만들어 보는 것이 있겠다. 행복한 코딩이 되길 바란다!

설치가 정상적으로 되었는지 확인하기 위해 Python 스크립트에서 모듈을 임포트해보면 좋다. 에러가 발생하지 않는다면, 모두 준비된 것이다.

 

 

반응형

개요

PHP로 이루어진 웹 페이지 개발 도중 python 코드로 구현된 AI를 적용해야 하는 일이 있었다.

PHP에서 Python 코드를 호출하여 AI 실행 후 결과를 리턴받는 과정을 구현해보자.

 

방법

구현 방법은

  1. python 코드 구현
  2. python 로직 호출하는 PHP 코드 구현
  3. AI가 필요한 부분에 방법 2의 PHP 호출

3가지 순서로 이루어진다.

1. python 코드 구현

비지니스 로직에 맞춰 python 기반 AI를 구현한다.

이 때, print 문법을 활용하여 PHP코드로 결과 값을 리턴할 수 있다.

print("result")

 

2. python 호출하는 PHP 코드 구현

두 번째로 python 코드를 호출하고 값을 리턴 받을 수 있는 PHP를 작성한다.

AI가 필요한 부분에 호출을 직접 작성해도 되는거 아니냐 ! 뭐하러 PHP를 번거롭게 따로 만드나 !

라고 할 수도 있지만 ajax 사용을 위해 필수적인 작업이다.

ajax는 비동기 통신 문법으로써 문법 내에 호출한 PHP 작업을 실행한 채 리턴을 기다리지 않고 다음 라인을 바로 실행한다.

만약, ajax를 사용하지 않는다면  python작업이 끝날 때까지 코드가 진행되지 않고 멈출 것이다.

결과 도출까지 시간이 걸리는 AI라면 UX에 지장이 될 수 있다.

그럼, PHP 코드를 살펴보자

<?php
$branch_idx = $_POST['branch_idx'];
$student_idx = $_POST['student_idx'];
$python_output = shell_exec("python face_recog_dnn.py $branch_idx $student_idx");
echo $python_output;
?>

해당 코드는 브랜치, 학생 idx 값을 해당 PHP를 호출한 메소드로부터 POST 형식으로 받아와서 python 코드에 argv로 넣어서 실행시키는 로직이다.

shell_exec를 사용하면 쉘에서 파이썬 코드를 돌릴 때 처럼 파일명, argv를 지정할 수 있고,

좌측의 변수로 리턴 값을 가져올 수 있다. (여기서는 python 코드 내 print 값)

그 다음 echo를 활용하여 해당 PHP를 호출 한 부분에 결과를 리턴할 수 있다.

주의점 :

해당 코드에서는 python 코드가 같은 경로에 있어서 경로 이동 코드가 없었다.

만약 다른 경로에 있는 python 코드를 실행하고 싶을 때 쉘에서는

python ../a/b/face_recog_dnn.py

이렇게 사용하면 된다.

하지만 PHP에서

shell_exec("python ../a/b/face_recog_dnn.py");

이렇게 작성하면 동작하지 않는다.

아래와 같이 작성해야 다른 경로의 python 코드가 실행된다.

shell_exec("cd ../a/b/ && python face_recog_dnn.py");

 

3. AI가 필요한 부분에 방법 2의 PHP 호출

 비지니스 로직 상 AI 활용(Python 코드 호출)이 필요한 부분에 $ajax 문법을 사용하여 방법 2의 PHP를 호출한다.

$.ajax({
    type: "POST",
    url: "../face_recog/run_face_recog.php",
    data: {
        branch_idx: <?=$_SESSION['st_branchId']?>,
        student_idx: <?=$_SESSION['st_id']?>
    },
    success: function(response) {
        console.log("python response : ", response);
        // 얼굴 발견 못했을 때
        if (response.includes("Not Found")) {
        document.getElementById("ai_monitoring").style.display = "none";
        document.getElementById("ai_monitoring_warning").style.display = "block";
        } else {
        document.getElementById("ai_monitoring_warning").style.display = "none";
        document.getElementById("ai_monitoring").style.display = "block";
        }
    },
    error: function(xhr, status, error) {
        console.log("error : ", error);
        alert("얼굴 인식 프로그램에 문제가 생겼습니다.");
    }
});
type 호출할 프로토콜을 지정한다. ex. GET, POST, PUT
url 호출할 PHP 경로 및 파일 이름을 지정한다.
data POST 프로토콜일 경우 호출한 PHP파일로 넘길 parameter data를 작성한다.
success  PHP호출에 성공했다면, 해당 부분으로 리턴 값이 온다. 여기서 response 값으로는 호출한 PHP에서 echo로 작성한 내용이 넘어온다.
error PHP 호출에 실패했다면, 해당 부분이 실행된다. 사용자에게 에러가 났다는 사실을 알려준다.

 

결론

PHP에서 Python 코드를 실행하는 방법을 알아보았다.

단계가 많아 복잡해 보이지만 한 번 보고 따라하면 이후부터는 쉬울 것이다.

요새는 React와 같은 편리한 프레임워크가 많이 나오고 보안상의 이유로 PHP를 사용할 일이 많지 않겠지만,

레거시 코드 유지보수를 위해서는 알아 둘 필요성은 있다.

이를 통해 PHP에서 Python을 활용하여 만든 여러 tool을 사용해보자

반응형

개요

엑셀에 있는 10만 줄의 데이터를 삽입하는 과정에서 10만번의 insert를 할 경우 232초가 걸렸다.

10만번의 DB I/O 시간 때문인데, UX관점에서 좋지 않았다.

결론적으로, I/O 횟수 단축을 위해 다량의 데이터를 한 번에 insert하는 방법을 찾았다.

 

방법 1

insert의 values 구문에 여러 개의 데이터 Row를 넣는다.

기존

insert into dataset
(ID, FIRSTNAME, LASTNAME, EMAIL)
values
(1, "Gomgom", "Kim", "abc@naver.com")

다중 insert

insert into dataset
(ID, FIRSTNAME, LASTNAME, EMAIL)
values
(1, "Gomgom", "Kim", "abc@naver.com"),
(2, "Gomgom2", "Kim", "abc2@naver.com"),
(3, "Gomgom3", "Kim", "abc3@naver.com"),
(4, "Gomgom4", "Kim", "abc4@naver.com"),
(5, "Gomgom5", "Kim", "abc5@naver.com")

다중 insert Mybatis 표현

<insert id="insertMultiRow" parameterType="java.util.Map">
    insert into dataset
    <trim prefix="(" suffix=")" suffixOverrides=",">
        ID, FIRSTNAME, LASTNAME, EMAIL
    </trim>
    values
    <foreach collection="multiData" item="item" separator=",">
        <trim prefix="(" suffix=")">
            #{item.id,jdbcType=VARCHAR},
            #{item.firstname,jdbcType=VARCHAR},
            #{item.lastname,jdbcType=VARCHAR},
            #{item.email,jdbcType=VARCHAR}
        </trim>
    </foreach>
</insert>

 

주의점

10만개의 데이터 row를 SQL문으로 붙이다 보니

Database 처리 과정에서 mybatis query memory exceeded가 발생했다.

Memory를 늘리는 방법도 있지만 데이터 갯수가 몇 개 까지 늘어날 지 모르는 상황이어서

확실한 해결책은 아니었다.

결론적으로 1만개 까지는 Insert가 되었고,

1만개씩 끊어서 저장하는 방법으로 구현하게 되었다.

 

방법 2

SqlSessionFactory 이용

//쿼리 호출하는 서비스단(기존 소스에서 sqlSessionFactory 부분 외에는 다 생략)

@Autowired
private SqlSessionFactory sqlSessionFactory;

private void test(){
	
    SqlSession sqlSession = null;
    sqlSession = this.SqlSessionFactory.openSession(ExecutorType.BATCH);
        
	while(!startCal.equals(endCal)){
        
        sqlSession.insert("com.test.hubjob.dao.StatBatchDao.insertList", voList); //실행할 쿼리를 insert 해둠.
        loopCnt++;

        //sqlSession 1일단위로 flush(1일 = 144건)
        if(loopCnt % 144 == 0) {
            sqlSession.flushStatements(); //한번에 밀어넣으면 오래걸리므로 적당한 간격으로 끊어서 밀어넣음.
        }
    }
    
    sqlSession.flushStatements(); //남은거 밀어넣고
    sqlSession.commit();	//커밋!(이때 커넥션 한번!)

}

 

 

반응형

+ Recent posts