728x90

소개

노드서버를 구성하고 기본 CRUD(Create, Read, Update, Delete) 기능을 직접 코드로 작성하고, 로그인은 JWT(Json Web Token)를 이용한 인증, 로그아웃 기능, 그리고 회원가입 기능도 직접 코드로 작성해주고, 서버의 로깅 라이브러리도 적용해주었습니다.

프로젝트 파일 구성

  • controllers : 클라이언트의 요청을 받아들이고, 비즈니스 로직을 수행합니다.
  • models : 데이터를 저장하고, 검색하고, 업데이트하고, 삭제합니다.
  • routes : 클라이언트의 요청을 controllers에 전달합니다.
  • services : 비즈니스 로직을 처리합니다.
  • utils : 유틸리티 함수를 포함합니다.

CRUD 기능 추가하기

먼저, 라우팅을 통해 클라이언트의 요청을 받아들이는 API를 작성합니다. API는 HTTP 메서드별로 구분하여 작성합니다.

Create

데이터를 추가하는 API는 POST 메서드로 구현합니다. 클라이언트로부터 전달받은 데이터를 models에서 정의한 스키마에 맞게 생성하고, DB에 저장합니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.post('/users', UserController.createUser);

// controllers/user.controller.js

const User = require('../models/user.model');
const createUser = async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
};

module.exports = { createUser };

// models/user.model.js

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
  email: { type: String, required: true }
});

const User = mongoose.model('User', userSchema);

module.exports = User;

Read

데이터를 조회하는 API는 GET 메서드로 구현합니다. DB에서 데이터를 조회한 후, 클라이언트에게 응답합니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.get('/users', UserController.getUsers);

// controllers/user.controller.js

const User = require('../models/user.model');
const getUsers = async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

module.exports = { getUsers };

// models/user.model.js

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
  email: { type: String, required: true }
});

const User = mongoose.model('User', userSchema);

module.exports = User;

Update

데이터를 수정하는 API는 PUT 메서드로 구현합니다. 클라이언트로부터 전달받은 데이터로 DB에서 데이터를 수정합니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.put('/users/:id', UserController.updateUser);

// controllers/user.controller.js

const User = require('../models/user.model');
const updateUser = async (req, res) => {
  try {
    const { id } = req.params;
    await User.findByIdAndUpdate(id, req.body);
    res.status(204).end();
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

module.exports = { updateUser };

// models/user.model.js

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
  email: { type: String, required: true }
});

const User = mongoose.model('User', userSchema);

module.exports = User;

Delete

데이터를 삭제하는 API는 DELETE 메서드로 구현합니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.delete('/users/:id', UserController.deleteUser);

// controllers/user.controller.js

const User = require('../models/user.model');
const deleteUser = async (req, res) => {
  try {
    const { id } = req.params;
    await User.findByIdAndDelete(id);
    res.status(204).end();
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

module.exports = { deleteUser };

// models/user.model.js

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
  email: { type: String, required: true }
});

const User = mongoose.model('User', userSchema);

module.exports = User;

JWT를 이용한 로그인 및 로그아웃 기능 추가하기

JWT를 이용하여 로그인 및 로그아웃 기능을 구현합니다.

로그인

클라이언트가 로그인 요청을 보내면, 서버는 입력받은 정보를 검증하여 유효한 사용자인지 확인합니다. 유효한 사용자이면, JWT 토큰을 생성하여 클라이언트에게 전달합니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.post('/login', UserController.login);

// controllers/user.controller.js

const jwt = require('jsonwebtoken');
const User = require('../models/user.model');
const login = async (req, res) => {
  try {
    const { username, password } = req.body;
    const user = await User.findOne({ username });
    if (!user || user.password !== password) {
      res.status(401).json({ message: 'Invalid credentials' });
      return;
    }
    const token = jwt.sign(
      { username: user.username },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    res.json({ token });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

module.exports = { login };

로그아웃

클라이언트가 로그아웃 요청을 보내면, 서버는 JWT 토큰을 만료시킵니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.post('/logout', UserController.logout);

// controllers/user.controller.js

const logout = async (req, res) => {
  try {
    res.status(204).end();
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

module.exports = { logout };

회원가입 기능 추가하기

클라이언트가 회원가입 요청을 보내면, 서버는 입력받은 정보를 검증하여 유효한 정보인지 확인한 후, DB에 저장합니다.

// routes/index.js

const express = require('express');
const router = express.Router();
const UserController = require('../controllers/user.controller');

router.post('/signup', UserController.signup);

// controllers/user.controller.js

const User = require('../models/user.model');
const signup = async (req, res) => {
  try {
    const { username, password, email } = req.body;
    const user = new User({ username, password, email });
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
};

module.exports = { signup };

// models/user.model.js

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
  email: { type: String, required: true }
});

const User = mongoose.model('User', userSchema);

module.exports = User;

서버의 로깅 라이브러리 적용하기

서버의 로깅을 위해 morgan 라이브러리를 사용합니다. morgan 미들웨어를 추가하여, HTTP 요청과 응답의 정보를 로깅합니다.

// app.js

const express = require('express');
const mongoose = require('mongoose');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;

mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true
});

app.use(express.json());
app.use(morgan('tiny'));

const indexRouter = require('./routes/index');
app.use('/', indexRouter);

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

마무리

이번 글에서는, 노드서버를 구성하고, 기본 CRUD 기능부터 JWT를 이용한 로그인 및 로그아웃, 회원가입 기능까지 구현해보았습니다. 코드의 주요 부분을 한글로 주석처리하여, 이해하기 쉽게 작성하였습니다. 노드의 최신 버전을 사용하고, MVC 패턴으로 controller, service, repository를 구성하였습니다. 서버의 로깅을 위해 morgan 미들웨어를 적용하였습니다. 이를 통해 더욱 실용적인 노드서버를 구성할 수 있습니다.


내저장소 바로가기 luxury515

728x90

프로젝트에서 PDF를 통합하는 솔루션을 고민하다가 간단한 기능만(병합,분리) 필요하니 그냥 오픈소스를 활용하는것도 나쁘지 않을것 같아서 작성하였다.

iText vs PDFBox 둘중 고민하다가 그냥 PDFBox 를 사용하기로 했다. google 검색하니 소스가 널려 있어서 잘 되는걸로 가져다가 조금 수정하였다.

 

pom.xml 에서 library 추가.

<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>3.0.0-RC1</version>
</dependency>

java 코드는 아래 와같다.

path는 맥북 기준이니 windows 사용자는 알아서 수정하면 될것 같다.

public static void main(String[] args) {

        //pdf 분리
        // 테스트 시 path는  맥북 기준...windows 경로는 알아서...
        separate(new File("/Users/blake/Documents/pdf/pns_1_5.pdf"),2);
        //pdf 합치기
        File file1=new File("/Users/blake/Documents/pdf/merge_1.pdf");
        File file2=new File("/Users/blake/Documents/pdf/merge_2.pdf");
        List<File> files= List.of(file1,file2);
        merge(files,"/Users/blake/Documents/pdf/new_merge.pdf");
    }



    public static boolean separate(File file, int page){
        try {
            PDDocument document = Loader.loadPDF(file);
            int pages=  document.getNumberOfPages();
            if(pages>page){
                separate(file,1,page);
                return separate(file,page+1,pages);
            }
        } catch (IOException e) {
            return false;
        }
        return false;
    }
    public static boolean separate(File file,int startPage,int endPage){
        return separate(file,endPage-startPage+1,startPage,endPage);
    }
    public static boolean separate(File file,int page,int startPage,int endPage){
        try {
            //pdf 파일 로드
            PDDocument document = Loader.loadPDF(file);
            // 분리함수사용하기 위한 객체 생성.
            Splitter splitter = new Splitter();
            splitter.setStartPage(startPage);
            splitter.setSplitAtPage(page);
            splitter.setEndPage(endPage);
            // 분리후 파일 리스트
            List<PDDocument> Pages = splitter.split(document);

            Iterator<PDDocument> iterator = Pages.listIterator();
            //분리된 파일 저장
            int i = 1;
            while (iterator.hasNext()) {
                PDDocument pd = iterator.next();
                String folderPath= file.getParent();
                String fileName=file.getName().substring(0,file.getName().lastIndexOf("."));
                int start=startPage+(i-1)*page;
                int end=i*page>endPage?i*page:endPage;
                pd.save(folderPath+File.separator+fileName+(start+"~"+end)+".pdf");
            }
            System.out.println("PDF 분리완료!");
            document.close();
            return true;
        }catch (Exception e){
            return false;
        }
    }

    public static boolean merge(List<File> files,String path){
        PDFMergerUtility merger = new PDFMergerUtility();
        //파일 merge 설정
        merger.setDestinationFileName(path);
        files.forEach(f->{
            try {
                merger.addSource(f);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        });
        try {
            //병합시작...
            merger.mergeDocuments(null);
            System.out.println("PDF merge 완료!");
        } catch (IOException e) {
            return  false;
        }
        return true;
    }

스크린샷 2023-03-07 오후 3.17.29.png
0.05MB

 

728x90

말그대로 SQL 를 실행하기 위한 select ,insert ,delete , update 등 쿼리가 나갈때 해커님께서 영혼없는 개발자들이 작성한 코드들의 틈새를 노려 정보를 빼돌리거나 등 자신의 목적에 도달하게 하는 방식이다.
나무위키의 정의를 인용하면...


주로 사용자가 입력한 데이터를 제대로 필터링, 이스케이핑하지 못했을 경우에 발생한다. 공격의 쉬운 난이도에 비해 파괴력이 어마어마하기 때문에 시큐어 코딩을 하는 개발자라면 가장 먼저 배우게 되는 내용이다. 이러한 injection 계열의 취약점들은 테스트를 통해 발견하기는 힘들지만 스캐닝툴이나 코드 검증절차를 거치면 보통 쉽게 발견되기 때문에 탐지하기는 쉬운 편이다. 보안회사 Imperva가 2012년에 발표한 보고서에 따르면 월평균 4회가량의 SQL 인젝션 공격이 일어난다고 한다. OWASP에서도 수년 동안 인젝션 기법이 보안 위협 1순위로 분류되었던 만큼 보안에 각별한 주의가 필요하다.


아주 기본적? 인 수법을 알아보자

SELECT * FROM USERS WHERE ID = ''or 1=1--' and PW = '암호가뭐게?계?'

해커가
'or 1=1-- 입력
where id = ' ' 로 쿼리가 변경 될것이고
1=1 는 항상 true (참)이고 or 때문에 둘중 하나가 true이면 무조건 ture
-- 는 우리가 잘 아는 sql 주석 처리 표기 법이니
비밀번호 and PW = ' 암호가뭐게?계?' 까지는 그냥 무시된다.

즉 비번없이도 우리집에 들어와서 내 정보를 다 가져라! 라고 하는 식.

mybatis 에서 where 1=1 를 쓰지 말고 ${paramater} 대신 #{paramater}를 쓰는 이유도 이런 나쁜짓 하는 놈들을 무효화? 시키기 위해서이다.
내가 작성했던 지난 글들을 읽어보면 도움될지도 모른다.
https://rainsister.tistory.com/69

Mybatis 항상 헷갈렸던 ${} vs #{} 를 너의 머리속에 심어줄게!

선배한테 물어보면 10명중 8명은 아래와 같이 설명해 줄꺼다! 선배1 : #{} 은 보안에 좋고 ${} 보안엔 안좋고...그래서 보안에 좋은 #{} 를 써고 ${}은 쓰지말라고 선배2 : innjection 어쩌고 저쩌고... 뭐

rainsister.tistory.com

https://rainsister.tistory.com/68

mybtis 에서 where 1=1 ? 그냥 쓰지마!

소스 점검 하다가 아래와 같은 코드를 봤다. //... 생략 FROM TOMS_CO_M CM ) T1 WHERE 1 = 1 AND CORP_CD = #{param.srchCorpCd} AND CO_CRT_DTM BETWEEN TO_DATE(#{param.srchCoCrtDtmFr}, 'yyyyMMdd') AND TO_DATE('99991231', 'yyyyMMdd') 참 성의

rainsister.tistory.com

728x90

선배한테 물어보면 10명중 8명은 아래와 같이 설명해 줄꺼다!

선배1 : #{} 은 보안에 좋고 ${} 보안엔 안좋고...그래서 보안에 좋은 #{} 를 써고 ${}은 쓰지말라고

선배2 : innjection 어쩌고 저쩌고...

뭐 보안에 대해서 언급했으니 그 선배한테는 일단 50점을 준다. 하지만 보안에 좋으니 #{} 을 쓰고 ${}를 쓰지 말라는 얘기는 틀린 말이다.

왜냐면 2가지 모두 사용해도 문제 없다,하지만 용도가 틀리니 구분해서 사용해야 된다는것이다.

차이

1. #{ }는 수신 데이터를 문자열로 취급하고 수신 데이터에 큰따옴표를 추가한다.

select * from student where student_name = #{studentName}

전달된 값이 "blake" 이면 sql로 구문 분석된 값은 student_name="blake" 으로 된다.

 

2. ${ }는 SQL에서 들어오는 데이터를 직접 표시하고 생성한다.

select ${fieldNmae} from student where student_age = 18

이때 전달된 파라미터는 질의할 필드로 사용되며 전달된 값이 student_name인 경우 구문 분석된 SQL은 다음과 같다.

select student_name from student where age = 18

3. #{ } 메소드는 SQL injection 방지할수 있다.

4. ${ }는 SQL 삽입(injection)을 방지할 수 없다.


5. ${ } 메서드는 일반적으로 목록 및 테이블 이름과 같은 데이터베이스 개체를 전달하는 데 사용


6. #{ } 메서드는 보안선이 높으므로 #{}를 사용할 수 있는 곳에 ${ }를 사용하지 않도록 한다.


7. Mybatis 에서 정렬할 때 동적 매개변수에 의한 순서 order by 를 사용할때  #{ } 대신 ${ }를 사용한다.

 

계속해서 아래 코드를 보자

select * from student where name = #{name}
 
select * from student where name = ${name}

쿼리 분석된 결과는 동일하게 

select * from student where name = "blake"

라고 나올것이다.

#{}와 ${}의 차이점은 사전 컴파일 처리에 있다. 

#{} 사는 컴파일 단계에서 매개변수 부분을  다음 SQL 문이 되는 자리 표시자 ?로 대체 된다.

select * from student where name = "blake"

그리고 삽입관련 내용은 내가 예전에 작성한 injection 관련 포스팅을 참고하면 도움될것 같다!

 

끝!

'Back-end' 카테고리의 다른 글

StringUtil 이제 안녕!  (0) 2023.04.25
SQL Injection 에대해서  (0) 2023.01.20
mybtis 에서 where 1=1 ? 그냥 쓰지마!  (0) 2023.01.20
Springboot 3가지 CROS 설정  (0) 2023.01.09
Apache BeanUtils vs Spring BeanUtils 성능비교  (0) 2023.01.06

+ Recent posts