들어가는 말

 

저는 유지보수 업무를 주로 해왔었기 때문에 사용자 인증하는 절차나 관련 기술, 정의 등등에 대해

간단히 "그런게 있다더라~ 카더라~"로만 알고 있었는데,

이번에 회사에서 관련된 작업을 진행하게 되어 정의에 대해 알아보고자합니다.

(이직하고 (변명)적응하느라 포스팅을 거의 안썼는데, 다시 마음을 잡고 간단한 글이라도 쓰려고 노력 중)


JWS (Json Web Token)

정의

Json을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 웹 토큰입니다.

JWT는 토큰 자체의 정보를 담고 사용하는 Self-Containerd 방식으로 정보를 전달합니다.

 

여기서 클레임(Claim) 기반이란 무엇일까요?

클레임(Claim)은 주체가 수행할 수 있는 작업보다는 주체가 무엇인지를 표현하는 이름과 값의 쌍이라고 합니다. 

예를들어 운전 면허증을 가지고 있다고 가정해보겠습니다.

생년월일(1999년 1월 21일)이 적혀있는 클레임의 이름은 DateOfBirth라고 할 수 있고, 클레임의 값은 19990121이며 발급자는 운전면허 발급기관이 됩니다.

클레임 기반 권한부여는 클레임 값을 검사한 후에 이 값을 기반으로 리소스에 대한 접근을 허용합니다.

그리고 운전면허증을 통해 인증을 거쳐하는 경우가 생긴다면 권한 부여 절차가 진행됩니다.

인증을 요구하는 경우가 생기면 접근을 허용하기 전에 먼저 클레임의 값(DateOfBitrth)와 발급기관을 신뢰할 수 있는지 여부부터 확인합니다.

 

간단한 회원인증 시의 흐름을 확인해보겠습니다.

 

어플 실행
스토리지에 인증 값이 있는가?
Y N
스토리지 값을 통해 인증 서버에서 JWT 발생 및 응답 헤더에 담아 보냄
  스토리지에 JWS를 저장
인증

이렇게 받아온 JWT를 HTTP 헤더에 담아 통신을 할 때마다 보내주게되면

서버에서 JWT가 허가된 것인지의 여부를 검사하게됩니다.

 

 

구조

JWT는 3개의 파트로 나누어지며 각 파트는 점으로 구분합니다.

구분되는 파트는 Header, Payload, Signature 이며 각 부분은 Json 형태인 (Base64로 인코딩된 형태로 표현)입니다.  

Base64 인코딩의 경우 “+”, “/”, “=”이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한  Base64url 인코딩을 사용합니다.

(Base64는 암호화된 형태가 아니므로 매번 같은 인코딩 문자열을 반환한다고 합니다.)

 

JWT 구조 (출처: http://www.opennaru.com/opennaru-blog/jwt-json-web-token/)

 

JWT 구조 디테일 (출처: https://mangkyu.tistory.com/56)

Header

토큰의 타입과 해시 암호화 알고리즘으로 구성되어 있습니다.

{
	"alg": "HS256", // 해시 알고리즘 (HMAC, SHA256, RSA)
    "typ": "JWT" // 토큰 유형
}

 

Payload

토큰에 담을 클레임(Claim) 정보를 포함하고 있습니다.

클레임은 json 형태로 key-value 한 쌍으로 이루어져 있습니다.

클레임의 정보는 등록된 (registered) 클레임, 공개(public) 클레임, 비공개(private) 클레임으로 세 종류가 있습니다. 

{
	"name": "John",
    "age": "28"
}

 

등록된 클레임 (Registered Claim)

- 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터로 선택적으로 작성이 가능하며 사용이 권장됩니다.

 

iss 토큰 발급자(issuer)
sub 토큰 제목(subject)
aud 토큰 대상자(audience)
exp 토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함 ex) 1480849147370
nbf 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음
lat 토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음
jti JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용

 

공개 클레임 (Public Claim)

- 사용자 정의 클레임으로 공개용 정보를 위해 사용되며 충돌 방지를 위해 URI 포맷을 이용합니다.

{
	"https://naver.com": true
}

 

비공개 클레임 (Private Claim)

- 사용자 정의 클레임으로 서버와 클라이언트 사이에 임의로 지정한 정보를 저장합니다.

{
	"token_type": "access"
}

 

Signature

서명은 비밀키를 포함하여 암호화되어있습니다.

토큰을 인코딩하거나 유효성을 검증할 때 사용하는 고유한 암호화 코드입니다.

헤더, 페이로드의 값을 Base64로 인코딩하고 이 값을 비밀 키를 이용해 헤더에서 정의한 알고리즘으로 해싱합니다.

그리고 해싱된 값을 다시 Base64로 인코딩하여 생성합니다.

 

JWT 사용 예시

생성된 토큰은 HTTP 통신 시 인증 시 value 값으로 사용되며 일반적으로 토큰 value 앞에는 'Bearer'을 붙여 사용합니다.

{
	accesstoken: Bearer <token>
}

 

 


 

참고

http://www.egocube.pe.kr/translation/content/asp-net-core-security/201701150001

http://www.opennaru.com/opennaru-blog/jwt-json-web-token/

https://meetup.toast.com/posts/239

 

 

back작업01번에 이후 작업을 진행하겠습니다.

실제 api 요청과 응답 값 받아서 로그인 절차를 진행해보겠습니다.

 

 

Front

 

front 하위에 sagas 폴더를 생성한 후에 관리할 파일명으로 js파일을 생성합니다.

예를 들어 user 정보를 관리할 것이다라고 한다면 user.js가 되겠죠.

저는 user.js로 만들었습니다.

파일명 예시

 

sagas는 리덕스 생태계 패키지입니다.

비동기 작업을 세분화 시킬 수 있습니다.

reducer는 리액트의 상태 생성자입니다. 액션이 오면 리듀서가 스토어의 상태를 변경시키는 방식으로 동작합니다.

reducers와 sagas를 짝으로 사용하기때문에 동일한 파일명으로 작업했습니다.

두 파일모두 확인해보겠습니다.

 

 

sagas/user.js

api 호출 주소는 back의 서버 포트를 입력하면됩니다. (app.js에서 listen하는 포트)

저는 3065이므로 localhost:3065로 접근할 수 있습니다.

import axios from 'axios';
import Router from 'next/router';
import { all, fork, put, takeLatest, delay, call } from 'redux-saga/effects';
import { 
    LOG_IN_ADMIN_REQUEST, LOG_IN_ADMIN_SUCCESS, LOG_IN_ADMIN_FAILURE,
} from '../reducers/user';

function adminLogInAPI(data){
    return axios.post('http://localhost:3065/admin/login', data);
};

function* adminLogIn(action){
    try{
        const result = yield call(adminLogInAPI, action.data);
        
        yield put({
            type: LOG_IN_ADMIN_SUCCESS,
            data: result.data
        });

        yield Router.replace('/');

    }catch(err){
        console.error(err);
        yield put({
            type: LOG_IN_ADMIN_FAILURE,
            error: err.response.data
        })
    }
}

function* watchAdminLogIn(){ 
    yield takeLatest(LOG_IN_ADMIN_REQUEST, adminLogIn);
}

export default function* userSaga(){
    yield all([
        fork(watchAdminLogIn),
    ]);
}

 

 

reducer/user.js

기본 state 정보가 있고 switch 문으로 해당 상태들을 관리하거나 변경 시킵니다.

import produce from '../util/produce';
import Router from 'next/router';

export const initialState = {
    admin: {},
    logInAdminLoading: false,
    logInAdminDone: false,
    logInAdminError: null,
};

export const LOG_IN_ADMIN_REQUEST = 'LOG_IN_ADMIN_REQUEST';
export const LOG_IN_ADMIN_SUCCESS = 'LOG_IN_ADMIN_SUCCESS';
export const LOG_IN_ADMIN_FAILURE = 'LOG_IN_ADMIN_FAILURE';

const reducer = (state = initialState, action) => produce(state,(draft) => {
    switch(action.type){
        case LOG_IN_ADMIN_REQUEST: 
            draft.logInAdminLoading = true;
            draft.logInAdminDone = false;
            draft.logInAdminError = false;
            break;
                
        case LOG_IN_ADMIN_SUCCESS: {
            draft.logInAdminLoading = false;
            draft.logInAdminDone = true;
            draft.logInAdminError = false;
            draft.admin = action.data;

            Router.replace('/');
            break;
        }

        case LOG_IN_ADMIN_FAILURE:
            draft.logInAdminLoading = false;
            draft.logInAdminDone = false;
            draft.logInAdminError = true;

            alert(action.error);
            break;

        default: 
            break;
    }
});

export default reducer;

 

그리고 submit을 하는 컴포넌트가 존재합니다.

 

 

form.js

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { LOG_IN_ADMIN_REQUEST } from '../reducers/user';

import styled from 'styled-components';
import { Button } from 'antd';

const Wrap = styled.div`
    position: absolute;
    padding: 10px;
    width: 300px;
    border-radius: 5px;
    background: #fff;
    box-sizing: border-box;

    button + button {
        margin-left: 10px;
    }
`;

const Input = styled.input`
    padding: 5px;
    margin-bottom: 10px;
    width: 80%;
    border: 1px solid #666;
    border-radius: 3px;
    outline: none;
`;

const AdminButton = styled(Button)`
    padding: 7px 10px;
    height: auto;
    line-height: 1;
    border: 1px solid #666;
    border-radius: 3px;
    background: none;
    outline: none;
    cursor: pointer;

    &:hover, 
    &:focus { 
        color: #666;
        border: 1px solid #666;
        background: none;
    }
`;

const AdminLoginForm = ({ onClose }) => {
    const dispatch = useDispatch();
    const { logInAdminLoading, logInAdminError } = useSelector((state) => state.user);
    const nicknameRef = useRef(null);
    const passwordRef = useRef(null);

    useEffect(() => {
        nicknameRef.current.focus();
    }, []);

    useEffect(() => {
        logInAdminLoading && onClose();
    }, [logInAdminLoading]);

    const onSubmit = useCallback(() => {
        const nickname = nicknameRef.current.value;
        const password = passwordRef.current.value;
        
        if(!nickname || !nickname.trim()) {
            alert('관리자 id를 입력해주세요.');
            nicknameRef.current.focus();
            return;
        }
        
        if(!password || !password.trim()) {
            alert('관리자 password를 입력해주세요.');
            passwordRef.current.focus();
            return;
        }
		
        // 1.
        dispatch({
            type: LOG_IN_ADMIN_REQUEST,
            data: {
                nickname: nickname,
                password: password
            }
        });
    }, []);

    return (
        <Wrap>
            <form>
                <Input 
                    type="text"
                    name="admin-id"
                    placeholder="id"
                    ref={nicknameRef}
                    maxLength={20}
                />
                <Input 
                    type="password"
                    name="admin-password"
                    placeholder="password"
                    ref={passwordRef}
                    maxLength={20}
                />
            </form>
            <AdminButton onClick={onClose}>닫기</AdminButton>
            <AdminButton 
                type="submit"
                loading={logInAdminLoading}
                onClick={onSubmit}
            >
                접속
            </AdminButton>
        </Wrap>
    );
};

export default AdminLoginForm;

 1. dispatch하는 부분이 중요합니다.

LOG_IN_ADMIN_REQUEST가 dispatch되면 reducer와 sagas안의 user.js가 동작합니다.

 

 

 

back

(back_01 블로그에서 sagas에서 api 호출 시 처리할 라우터를 참고해서 만들면되고...)

그 이에 라우터에서 프론트에서 보내온 값을 처리하기 위해서 선행되어야 하는 작업이 있습니다.

 

 

app.js

라우터 연결한 것과 listen한 것보다 위에 작성해야합니다.

미들웨어의 순서는 위에서 아래로 실행되기 때문입니다.

json, urlencoded 예시

두줄의 역할은.. 

프론트에서 보낸 데이터를 해석해서 라우터의 req.body 안에 넣어주는 역할을 합니다.

json은 json 형식의 데이터를 처리하고 urlencoded는 폼 데이터로 넘어왔을때 처리를 해줍니다.

 

데이터가 넘어올때 비밀번호도 있을테니까, 보안을 위해 bcrypt 라이브러리를 사용해보겠습니다.

bcrypt 참고 => www.npmjs.com/package/bcrypt

npm i bcrypt

 

라우터에 적용해보도록 하겠습니다.

가입이라고 예시를 들어보겠습니다.

 

 

routes/admin.js

적용 전

const express = require('express');
const router = express.Router();
const { Admin } = require('../models');

router.post('/signup', async (req, res, next) => { // POST /admin/signup
	
    await Admin.create({
    	nickname: req.body.nickname,
        password: req.body.password    
    });
    
    res.send('ok');
});

module.exports = router;

적용 후 

const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const { Admin } = require('../models');

router.post('/signup', async (req, res, next) => { // POST /admin/signup
	try {
    	const hashedPW = await bcrypt.hash(req.body.password, 12);
    
      await Admin.create({
          nickname: req.body.nickname,
          password: hashedPW    
      });

      res.send('ok');
      
    } catch (error) {
    	console.error(error);
        next(error);
    }
});

module.exports = router;

이때 bcrypt도 비동기이므로 await을 붙여줘야합니다. 

이때 비동기 작업이 있다면 try, catch로 감싸줘야합니다.

그리고 bcrypt.hash는 암호화가 아닌 해쉬화가 되는 것입니다.

첫번째 인자는 변경할 문자열이고 두번째 인자의 숫자는 보안 등급? 같은 것이라고 합니다. 숫자가 높아질수록 해쉬화가 쎄진다고 합니다. 다만 많이 높을 경우 해쉬화하는 시간이 길어져 곤란하니 적당한 값을 찾아넣는 것이 중요하다고 합니다.

(보통 10-13을 많이 넣는다 합니다)

에러가 나면 next를 통해 에러를 보냅니다. next를 통해 에러를 보내면 에러가 한번에 처리된다고 합니다.(express가 알아서 브라우저로 전달한다고..)

 

 

 

이렇게 작업을 하고나서 시도해보면 cors 문제가 발생합니다.

cors 에러

서버에서 서버로 요청 시에는 cors 문제가 발생하지 않지만 브라우저에서 서버로 요청할 경우에는 cors 문제가 발생합니다. 이때 cors를 피해가는 방법들이 있는데, 

 

강의에서 간단히 말해주신 proxy방식은

브라우저에서 프론트 서버로 먼저 요청을 보내고 프론트 서버에서 백엔드 서버로(서버-서버) 요청을 보내는 형식으로하여 cors 문제를 피할 수 있다고 합니다.

거꾸로 응답도 저런식이겠죠?

직접적으로 피해가려면 브라우저에서 백엔드 서버로 요청을 보낼때 header 설정을 해주면 됩니다.

 

cors에 대해 이해할때 차단은 브라우저, 허용은 서버가 해줘야한다는 것을 알고가야합니다.

cors 미들웨어를 통해 해결해줍니다.

npm i cors

 

 

그다음 app.js로 가서 미들웨어를 등록합니다.

cors 등록 예시

 

cors는 보안 정책이어서 아래와 같이 간단하게 작업하지는 않는다고 합니다.

사용자 중에는 해커들도 존재하기 때문이죠.

cors를 전체 허용하면 위험하니 세세하게 잡는 것이 좋다고 합니다.

전체 허용 예시입니다.

app.use(cors({
    origin: '*',
    credentials: false,
}));

 

특정 주소 예시입니다.

app.use(cors({
    origin: 'https://okayoon.com',
    credentials: false,
}));

 

*는 모두다 허용을 뜻하므로 조심해야합니다.

개발 단계여서 *로 개발하고 나중에는 꼭 수정해주세요.

credentials 옵션은 기본 값은 같은 출처내에서만 인증 정보를 사용하겠다는 것입니다.

개발 상태여서 false하지만 true로 해야 같은 출처내에서만 되므로 나중에는 true로 바꿔줘야합니다.

참고 -> evan-moon.github.io/2020/05/21/about-cors/

 

 

잠깐 front로 가서

front/sagas/user.js로 갑니다.

우리가 작업을 할때마다 api 주소에 http://localhost:3065를 등록하기도 번거롭고

배포했을때 저 많은 것을 하나하나 바꾸기에도 어려우니까 베이스 url을 등록해주겠습니다.

베이스url 설정 예시1

 

 

front/sagas/index.js 에 베이스 url를 추가합니다.

베이스url 설정 예시2

axios의 기본 베이스 url을 설정해준 다음에 sagas/user.js에서 localhost 주소를 지웁니다.

베이스url 설정 예시3

주소를 지워도 기존과 같이 http://localhost:3065/admin/login으로 호출이 됩니다.

 

패스포트를 이용해서 로그인 전략을 작성해보겠습니다.

passport는 로그인 기능을 만들기 위한 라이브러리입니다.

페북, 깃헙, 이메일 로그인등을 한번에 관리할 수 있는 라이브러리입니다.

npm i passport passport-local

passport-local은 로컬에서 email이나 id로 로그인할 수 있도록 해주는 것입니다.

설치 후 패스포트 셋팅을 위해서 app.js로 이동합니다.

 

 

back

app.js

app.js에 passport등록 

그 후 back 하위에 passport폴더를 만들고 index.js와 local.js 파일을 생성합니다.

passport 폴더

 

차례로 입력해주겠습니다.

 

 

passport/index.js

const passport = require('passport');
const local = require('./local');

module.exports = () => {
    passport.serializeUser(() => {});
    passport.deserializeUser(() => {});

    local();
};

먼저 큰 틀입니다. 

local()은 local.js에 작성합니다.

 

 

passport/local.js

const passport = require('passport');
// 1.
const { Strategy: LocalStrategy } = require('passport-local');
const { Admin } = require('../models');

module.exports = () => {
    // 2.
    passport.use(
        // 3.
    new LocalStrategy({
        usernameField: 'adminId',
        passwordField: 'password',
        
    // 4.
    }, async (adminId, password, done) => {
        try {
            // 5.
            const user = await Admin.findOne({
                where: { adminId }
            });
            
            // 5.
            if(!user) {
                return done(null, false, { reason: '존재하지않는 사용자입니다.' });
            }
            
            // 6.
            const result = password === user.password;

            if (result) {
                return done(null, user);
            } else {
                return done(null, false, { reason: '비밀번호가 틀렸습니다.' });
            }

        } catch(error) {
            console.error(error);
            return done(error);
        }
    }));
};

이 파일에 로그인 전략을 세우게됩니다.

 

1. 

Strategy 메서드를 passport-local에서 구조분해로 가져오는데 이때 네임을 변경하면서 가져오는 이유는

나중에 로그인 전략이 많이 생기면 구분하기 위함입니다.

구글 로그인, 카카오 로그인 등...

변경하지 않고 사용해도 무방합니다.

 

2. 

passport.use 메서드는 두 개의 인자를 넘길 수 있으며 하나는 객체, 하나는 함수입니다.

 

3.

new LocalStrategy에서 usernameField, passwordField에 해당하는 값은 req.body의 네임과 일치해야합니다.

 

4.

함수에서 전략을 세웁니다.

위의 객체에 넣어준 값이 인자로 옵니다.

 

5.

Admin 모델을 require해ㅇ서 findOne 메서드를 통해 db에서 해당 아이디가 있는지 찾습니다.

passport에서는 응답을 보내지 않고 done으로 결과를 판단해줍니다.

done첫번째 인자는 서버에러, 두번째 인자는 성공, 세번째는 클라이언트 에러를 넣어줍니다.

이것을 통해 routes에서 작업을 이어갑니다.

 

6.

bcrypt.compare를 통해 비밀번호 비교를 합니다.

저는 테스트 때문에 bcrypt를 안써서 작업했는데, 사용 예시는 아래와 같습니다.

const result = await bcrypt.compare(password, user.password)

첫번째 인자와 두번째를 비교합니다.

 

 

routes/admin.js 

로그인 api 호출 작업을 이어갑니다.

const express = require('express');
const router = express.Router();
const passport = require('passport');
const { Admin } = require('../models');

router.post('/login', (req, res, next) => {
    // 1.
    passport.authenticate('local', (err, user, info) => {
       
        if (err) {
            console.error(err);            
            return next(err);
        }

		// 2.
        if (info) {
            return res.status(401).send(info.reason);
        }

       // 3.
        return req.login(user, async (loginErr) => {
            if (loginErr) {
                console.error(error);
                return next(loginErr);
            }

            // 4.
            return res.status(200).json(user);
        });
    
    // 1.
    })(req, res, next);
});


module.exports = router;

 

1. 라우터 내부에 미들웨어 확장 방식으로 작업합니다.

내부에서 req, res, next를 사용하기 위해 맨 마지막에 호출하면서 인자를 전달합니다.

done이 콜백같이 동작해서 authenticate 두번째 인자로 전달됩니다. (네이밍은 상관없습니다.)

err은 서버 에러, user은 사용자 정보, info는 클라이언트 에러 입니다.

이 정보를 통해 if문을 통해 분기합니다.

 

2.

info는 클라이언트 에러입니다.

상태 값을 내려줄 때는 http 상태 코드를 참고하세요.

(10자리나 1자리는 프론트 개발자와 백엔드 개발자간의 약속으로 유하게 변경하여 작업하고는 합니다)

https://developer.mozilla.org/ko/docs/Web/HTTP/Status

 

3.

로그인되었을때 실행될 코드입니다.

login 메서드를 통해 로그인하는 것은 패스포트 로그인입니다.

우리서버 로그인 통과 후에 패스포트 로그인이 실행됩니다.

 

4.

에러가 없다면 사용자 정보를 프론트로 넘겨줍니다.

 

데이터 통신 흐름을 짚어보겠습니다.

로그인 폼에서 로그인 시도 -> saga 데이터 받음 -> 백엔드 서버로 데이터를 보냄 -> 백엔드 라우터에서 req.body로 데이터를 받고 -> authenticate 내부가 실행됨 -> passport 전략으로 진행(local.js) -> 성공 시 authenticate의 콜백으로 감 -> 다른 에러가 없다면 패스포트 login 시도 -> 문제가 없다면 서버 프론트로 응답을 보냅니다.

 

 

쿠키와 세션을 통해 로그인을 실행해보겠습니다.

로그인을 하면 브라우저랑 서바가 같은 정보를 가지고 있어야합니다.

자동으로 되는 것이 아니기때문에..

백엔드 서버에서 로그인이 완료되면 브라우저나 프론트 서버로 정보를 보내주는 작업을 해야합니다.

이때 정보 그대로 보내게 되면 해킹에 취약합니다. (모든 정보를 다 보내도 안됨)

+ 메모리도 너무 과해집니다.

따라서 어떠한 문자열만을 보내고 서로 각각 가지고 있게 됩니다.

브라우저도 가지고 있고 서버들도 각각 가지고 있습니다.(각각 쿠키와 세션을 통해)

그리고 이 문자열을 통해 서로를 연결해주는 역할을 합니다.

(이 문자열로 원하는 정보를 요청해서 따로 가져옵니다)

 

 

쿠키와 세션 라이브러리를 설치합니다.

npm i express-sesstion cookie-parser

 

 

back/app.js

미들웨어들을 추가합니다.

const session = require('express-session');
const cookieParser = require('cookie-parser');
const passport = require('passport');

// ...

app.use(cookieParser());
app.use(session());
app.use(passport.initialize());
app.use(passport.session());

세션, 쿠키 미들웨어등록

 

실제 로그인 시.. back의 라우터 부분입니다.

주석된 코드가 존재하지 않아도 내부적으로 res.setHeader에서 문자열을 보내줍니다(아까 말한 문자열)

그리고 세션과 연결해줍니다.

위의 코드에서 req.login이 동작하게되면 동시에 passport/index.js의 serializeUSer가 실행됩니다.

passport/index.js 코드

 

이때 매칭하는 작업을 하게됩니다.

serializeUser를 통해 id(문자열)을 저장하고 deseializeUser를 통해 복원해서 db에서 정보를 가져옵니다.

 

아래와 같이 수정해줍니다.

const passport = require('passport');
const local = require('./local');
const { Admin } = require('../models');

module.exports = () => {
   // 1.
    passport.serializeUser((user, done) => {
        // 2.
        done(null, user.id);
    });

    // 3.
    passport.deserializeUser(async (id, done) => {
        try {
            const admin = await Admin.findOne({ where: { id } });
            done(null, admin);
        } catch (error) {
            console.error(error);
            done(error);
        }   
    });

    local();
};

 

1. 

req.login을 했을때 동시에 실행되며 user인자는 req.login의 user가 들어온다.

 

2.

user 정보 중에서 쿠키랑 묶어줄 정보만 저장합니다.

첫번째 인자는 서버에러, 두번째 인자는 성공입니다.

 

3. 

복원하는 작업입니다.

db에서 정보를 가져옵니다.

 

수정이 끝났으면 app.js를 실행해보겠습니다. 

npm run dev

// 만약 스크립트 등록을 안했다면
// nodemon app.js

nodemon app.js시 경고문구

 

위와 같이 경고가뜹니다.

따라서 세션 옵션을 설정해줍니다.

 

 

back/app.js

app.js 세션 옵션

// 1.
app.use(cookieParser('secret'));
app.use(session({
    // 2.
    saveUninitialized: false,
    resave: false,
    // 1.
    secret: 'secret',
}));
app.use(passport.initialize());
app.use(passport.session());

1.

쿠키, 세션 간의 통신 중에 랜덤한 문자열을 생성할 기반 문자(보안문자열)를 넣어 줍니다.

테스트여서 secret으로 작성해 넣었는데, 다른걸 작성하세요.

이 문자열은 쿠키파서에 넣은 것과 세션 시크릿에 넣은 문자열이 동일해야합니다.

또한 이 문자열은 보안에 신경써서 보관해야합니다.

만약 이것을 해커가 가져가면 이 문자열을 통해 생성한 랜덤 문자열을 복원할 수 있다고 합니다.

따라서 .env 파일로 옮기겠습니다.

 

2.

세션설정입니다.

 

 

back/.env

COOKIE_SECRET=secret
DB_PASSWORD=secret

저는 그냥 secret으로 작성했는데, 변경해서 사용해야합니다.

키=값 형식으로 작성합니다.

 

그리고 app.js와서 dotenv 작업을 이어갑니다.

dotenv 설정

 

dotenv 문자열로 변경

문자열을 저렇게 변경해 주면 .env 에 등록한 것으로 치환된다고 합니다.

.env 파일 보안에 주의하세요(공개되면 안됨)

 

여기까지 로그인 시도에 대한 것이었습니다.

다음 작업을 이어가야겠습니다.

 

 

--추가 참고할 글--

1. 쿠키를 통해 로그인한 정보를 가지고 있어야한다.

2. 백엔드 서버와 프론트 서버 cors문제가 생기는 것을 이해하고 있다면 

-> 백엔드에서 app.js core에서 credentials를 true로 

-> 프론트에서는 saga index.js에서 axios.default.withCredentials를 true로 변경

(axios 세번째 인자로 withCredentials를 각각 true로 해줘도 되나 번거로우니..)

3. 한가지 제약사항이 생김, credentials 모드일 경우 민감해지니 보안 철저 cors에서 origin에 정확한 url 주소를 적어야한다. (origin: true해도 됨)

 

 

프론트에서 안전하게 로그인 처리하기

보안에 위협이 되는 문제

1. XSS (Cross Site Scriptiong)

공격자가 <input>태그나 url등에 Javascript를 삽입해 실행되도록 공격한다.

악성데이터를 실행하거나 사이트의 전역 변수를 이용해 API 요청을 하여 사이트의 로직인 척 행동하여 악의적인 행동을 한다.

 

2. CSRF (Cross Site Request Forgery)

서버에서 클라이언트 도메인을 통제하고 있지 않으면 공격자가 다른 사이트에서 API 요청을 할 수 있다.

이때 공격자가 사용자만이 가능한 행위(수정, 삭제, 등록 등)들을 할 수 있다.

(이러한 행위로인해 최악의 상황 -> 비밀번호, 송금 등에 접근)

 

원본 글 요약

  • JWT(JSON Web Token)로 유저 인증
  • Refresh Token은 httpOnly
  • Access Token은 JSON payload
  • Access Token은 지역변수로 저장
  • 새로고침이나 브라우저 종료 후 재접속(mount) 처리, 각각의 API 이용 
    • 최초 접근 API
    • 재 접근 API
  • 재 접근 시 Refresh Token과 Access Token을 새로 발급 받을 수 있도록 한다.
  • CSRF, XSS 공격에서 다소 안전하나 XSS는 어떤식으로든 취약점이 생길 수 있어 서버에서도 같이 처리 필요

 

이미지 출처 원본글

 

Access Token, Refresh Token이란?

  • OAuth(Open Auth)에서 나온 개념이며 두개를 묶어 사용자 토큰이라고 부른다.
  • Access Token은 보안상 짧은 유효기간, Refresh Token은 긴 유효기간을 가지고 있다.
  • Access Token이 만료되면 Refresh Token을 통해 Access Token을 갱신한다.
  • 왜 두개가 필요한지에 대해서는 Stack overflow 글 참고 하면 되고 참고 링크에서도 확인가능하다.
  • 보안상 두가지를 사용하는 것으로 이해하면된다.

-참고 https://dreamaz.tistory.com/22

 

Access Token과 Refresh Token이란 무엇인가?

안녕하세요. 개발자 드리머즈입니다.    [카카오 아이디로 로그인하기]에서나.. 네이버, 페이스북을 통한 로그인 시에 access token과 refresh token에 관한 내용을 들을 수 있습니다. 대충 뭐하는 녀��

dreamaz.tistory.com

 

보안관련 작업을 해본적도 관심도 없었던 나에게 보안에 얕게나마 관심을 가지게해주는 좋은 글이었다.

 

 


 

원본 글

https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

 


페이지를 꾸미기 전에 플러그인을 설치를 해보도록 하겠습니다.


테마는 집 전체 구조라 생각하면 플러그인은 가구라고 생각하면 됩니다.

슬라이드 플러그인, 사이드 메뉴 플러그인 등등..


[플러그인 > 새로추가]


추천, 인기, 추천됨, 즐겨찾기에서 플러그인 확인 및 다운로드 받을 수 있고 

키워드 검색을 통해 다운받을 수 있습니다.




1. WPS Hide Login


관리자 페이지 접속할때

www.okayoon.com/wp-admin으로 들어오는데, 

이게 보안에 안좋다고 합니다. 


다 똑같은 wp-admin 이기 때문에 일반 사용자도 관리자 로그인 페이지 접근이 수월하다는거죠.

이것 만으로도 보안에 문제가 있기 때문에 일단 저 부분을 수정하는 플러그인을 다운 받겠습니다.


검색창에 WPS Hide Login 검색

지금설치 후 활성화



활성화를 누르면 플러그인 > 설치된 플러그인으로 페이지가 이동됩니다.

그리구 WPS Hide Login 플러그인이 활성화되어 있는 모습이 확인 가능 합니다.



Setting 클릭



클릭 시 설정>일반으로 이동 됩니다.

(설정>일반에서 수정 가능)


WPS Hide Login 영역이 추가 되었고 설정하는 부분이 생겼습니다!


Login Url : wp-admin 대신 어떤 경로로 로그인 페이지를 접근할지

Redirection Url : 잘못된 url로 접근 시 어떤 경로로 보낼지



저는 테스트를 위해 hello로 설정하겠습니다. 

*현재는 변경했습니다.



이제 로그인 페이지는 

www.okayoon.com/hello 로 접근 가능합니다.


www.okayoon.com/wp-admin으로 접속하면 

www.okayoon.com/404로 이동되며 페이지가 없음을 알립니다.



+ Recent posts