Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

JS Dev Blog

서버 인증 본문

Development/Server

서버 인증

chacot 2022. 4. 23. 03:14

인증이란?

프론트엔드 관점 : 사용자의 로그인, 회원가입 등 사용자의 도입 부분

서버 관점 : 모든 API 요청에 대해 사용자를 확인하는 작업

 

인증은 왜 필요할까? 

서버에 요청을 보내는 클라이언트는 각각 다른 정보를 가지고 있다. 그리고 이 정보를 보호하기 위해 서버는 요청하는 클라이언트가 누구인지 확인하고 이에 맞는 데이터를 줄 필요성이 있다. 이 과정을 인증이라고 한다. (보안 관점)

 

 

다양한 인증 방식들

1. Session 인증 방식

1. 사용자(Client)가 로그인 시도

2. 서버는 DB단에서 사용자 정보 확인,

3. 회원 정보를 담은 세션을 생성해서 세션 저장소에 저장. 

4. 세션 ID를 발행

5. 서버는 사용자에게 응답으로 세션 ID를 줌

6. 사용자는 쿠키에 세션 ID를 저장. 데이터를 요청할 때 쿠키를 헤더에 실어서 보냄

7. 서버는 쿠키를 받아서 세션 저장소를 통해 검증

8. 유저 정보(세션) 획득 (인증 완료)

9. 서버는 사용자가 요청한 데이터를 줌 

 

 

Session ID === Session ?

세션 ID는 쿠키를 이용해서 서버에 전달하는 Key라고 보면 됨. 즉 세션(유저 정보)를 직접적으로 가지고 있는 건 아님. 클라이언트 측에서 세션 ID가 노출되더라도 세션 ID자체만으로는 유저의 유의미한 정보를 볼 수 없음. 왜냐하면 실제 정보는 서버에서 관리하기 때문임.

즉, 세션 방식에서 인증 정보를 책임지는 주체는 서버다 !

 

 

장점

1. 계정정보를 직접 HTTP요청에 실어서 보내는 것보다 안전

2. 서버는 회원정보를 하나하나 확인할 필요 없이 세션 ID만 가지고 유저정보를 획득할 수 있음

 

단점

1. 해커카 쿠키를 탈취해서 세션ID를 서버에 보내면 서버가 해커에게 사용자 정보를 줄 수도 있다.

(HTTPS를 통해 암호화 하거나, 세션 유효시간을 넣어서 방지)

2. 서버에 세션 저장소라는 저장공간이 필요하기 때문에 서버의 리소스 부담이 커짐

 

 

예제 코드

//server-session/controller/users/login.js

// 해당 모델의 인스턴스를 models/index.js에서 가져옵니다.
const { Users } = require('../../models');

module.exports = {
  post: async (req, res) => {
    // userInfo는 유저정보가 데이터베이스에 존재하고, 완벽히 일치하는 경우에만 데이터가 존재합니다.
    // 만약 userInfo가 NULL 혹은 빈 객체라면 전달받은 유저정보가 데이터베이스에 존재하는지 확인해 보세요
    // console.log(req.body)
    // console.log(req.session)
    const userInfo = await Users.findOne({
      where: { userId: req.body.userId, password: req.body.password },
    });
    
    // TODO: userInfo 결과 존재 여부에 따라 응답을 구현하세요.
    // 결과가 존재하는 경우 세션 객체에 userId가 저장되어야 합니다.
    if (!userInfo) {
      // your code here
      res.status(400).json({message : "not authorized"})
    } else {
      // your code here
      // HINT: req.session을 사용하세요.

      req.session.userId = userInfo.userId;
      res.status(200).json({data: userInfo, message:'ok'})

    }
  }
}
//server-session/controller/users/userinfo.js

const { Users } = require('../../models');

module.exports = {
  get: async (req, res) => {

    // TODO: 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요.
    // HINT: 세션 객체에 담긴 정보가 궁금하다면 req.session을 콘솔로 출력해보세요

    if (!req.session.userId) {
      // your code here
      res.status(400).json({message: "not authorized"})
    } else {
      // your code here
      // TODO: 데이터베이스에서 로그인한 사용자의 정보를 조회한 후 응답합니다.
      const userInfo = await Users.findOne({
        where: { userId: req.session.userId },
      });

      res.status(200).json({data: userInfo.userId, message: "ok"})
    }
  },
};

 

2. TOKEN 인증 방식 (JWT)

 

JWT?

Json Web Token. 인증에 필요한 정보들을 암호화 시킨 토큰, Access Token을 HTTP 헤더에 실어서 서버에 보냄.

 

Header : 위 3가지 정보를 암호화할 방식(alg), 타입(type) 등이 들어감

Payload: 서버에서 보낼 데이터가 들어감. (유저의 고유 ID값, 토큰 만료 기간)

Signature: Base64방식으로 인코딩한 Header,payload, SECRET KEY(Salt)를 더한 값

 

 

Header, Payload는 인코딩 되긴 하지만 암호화 되지는 않는다.(누구나 디코딩 가능) 따라서 비밀번호같은 민감정보가 들어가면 안된다.

하지만 Signature는 SECRET KEY를 포함하여 암호화 된다. 만약 해커가 Payload의 ID를 다른 ID로 조작하여 서버로 보내도 Signature는 '기존 사용자의 ID + SECRET KEY'를 기반으로 암호화 되어있기 때문에 유효한 토큰이 아니게 된다. 즉, SECRET KEY를 알지 못하면 토큰 조작이 불가능함.

1. 사용자가 로그인 시도

2. 서버에서 DB를 통해 사용자 확인

3. 고유한 ID, 토큰 유효기간 설정, SECRET KEY 이용해서 암호화 하여 ACCESS TOKEN 발급

4. 사용자에게 Access Token 전달.

5. 사용자는 인증이 필요한 요청의 헤더에 토큰을 실어서 보냄.

6. 서버는 SECRET KEY로  TOKEN 복호화하여 검증.

7. 유효한 TOKEN일 시에 사용자가 요청한 데이터를 줌.

 

 

세션방식 vs 토큰방식

Client View : 헤더에 인증정보(세션or토큰)을 실어서 보낸다는 점에서 동일

Server View : 인증을 위해 암호화, 복호화 (토큰 방식) or 별도의 세션저장소 이용(세션방식) 하는지 차이점이 있음.

 

장점

1. 별도의 세션 저장소 필요 없음. 상태를 저장하지 않아서 별도의 저장공간이 필요 없는 Stateless 서버를 만드는데 유용

2. 서버 확장, 유지 보수에 유리

 

 

단점

1. 누구나 디코딩이 가능하기 때문에 Payload에 유저의 중요한 정보들을 넣을 수 없음. (세션방식은 유저 정보가 저장소에 안전하게 보관)

2. JWT의 길이가 길기 때문에 요청이 많다면 서버의 자원낭비 발생

3. 이미 발급한 Token에 대해서는 악의적 사용을 막을 수 없음. (세션은 쿠키가 악의적으로 이용되면 해당 세션을 지워버리면 됨)

=> Access Token의 유효기간을 짧게하여 상대적으로 피해를 줄일 수 있음. 다만 이러면 사용자가 불편을 겪기 때문에 Refresh Token 이라는 방식을 사용함

 

 

그렇다면 Refresh Token은 뭘까?

Access Token의 유효기간을 짧게하면서 유저가 로그인할때의 불편함을 최소화 하기 위해 사용되는 Token.

사용자가 처음 로그인하면 Access Token과 Refresh Token을 동시에 발급

Access Token에 비해서 상대적으로 긴 유효기간을 가짐

Access Token이 만료되었을 때 Refresh Token을 통해 새로운 Access Token을 발급 받을 수 있음

 

1. 사용자가 로그인 시도

2. 서버는 DB를 통해 회원 정보 확인

3. 서버는 Access Token, Refresh Token 발급. DB에 Refresh Token 저장

4. 사용자에게 응답으로 Access Token, Refresh Token을 줌. (Access Token은 Data, Refresh Token은 Cookie에)

5. 사용자는 데이터 요청 시 Access Token을 헤더에 실어서 보냄

6. 서버는 Access Token 검증

7. 요청한 데이터를 사용자에게 줌 (여기까지는 Access Token만 사용했을 경우와 유사하다.)

8. Access Token 만료 

9. 사용자는 데이터 요청 시 Access Token을 헤더에 실어서 보냄

10. 서버는 Access Token 만료 확인

11. 사용자에게 Access Token 만료되었다는 응답을 줌 (인증 실패) 

* Client가 Access Token이 만료되었음을 인지했다면 굳이 9~11 과정을 할 필요 없이 바로 12.과정을 진행해도 됨

12. 사용자는 Access Token과 Refresh Token(Cookie에)을 함께 서버로 보냄

13. 서버는 Access Token 조작 여부 확인, Refresh Token을 DB에서 확인하여 유효한 Token이라면 새로운 Access Token 발급하여 사용자에게 전달.

14. 사용자는 새로운 Access Token 사용

 

장점

1. Access Token보다 안전

 

단점

1. 구현이 복잡. 검증 프로세스 증가

2. HTTP 요청 횟수 증가. 서버 자원 낭비

 

 

예제 코드

 

//server-token/controllers/users/login.js

const { Users } = require('../../models');
const jwt = require('jsonwebtoken');


module.exports = async (req, res) => {
  // TODO: urclass의 가이드를 참고하여 POST /login 구현에 필요한 로직을 작성하세요.

  const userInfo = await Users.findOne({ where : {userId: req.body.userId, password : req.body.password}})
  
  if(!userInfo){
    res.status(400).json({message :"not authorized"})
  } 
  else{

    const payload = {
      id : userInfo.dataValues.id,
      userId: userInfo.dataValues.userId,
      email : userInfo.dataValues.email,
      createdAt : userInfo.dataValues.createdAt,
      updatedAt : userInfo.dataValues.updatedAt

    }
    
 	//accessToken, refresh Token 발급
    const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, { expiresIn: 60 * 60 });
    const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, { expiresIn: '2d' });    
    
    res.cookie('refreshToken', refreshToken)
    res.status(200).json({data: { "accessToken": accessToken }, message: "ok"})
    

  }



};

 

//server-token/controllers/users/accessTokenRequest.js

const { Users } = require('../../models');
const jwt = require('jsonwebtoken')

module.exports = async (req, res) => {
  // TODO: urclass의 가이드를 참고하여 GET /accesstokenrequest 구현에 필요한 로직을 작성하세요.

  if(!req.headers.authorization){
    res.status(400).json({data : null, message :'invalid access token'})

  }else{

    let token =  req.headers['authorization']; 

    token = token.replace(/^Bearer\s+/, "");
  	
    //token 유효성 검사
    const payload = jwt.verify(token, process.env.ACCESS_SECRET)
    const userInfo = await Users.findOne({ where : {userId: payload.userId}})
    delete payload.iat;
  
    if(!userInfo){
      res.status(400).json({data : null, message :'invalid access token'})
    }else{
      res.status(200).json({data: {userInfo : payload }, message: "ok"})
    }

  }
};

 

//server-token/controllers/users/refreshTokenRequest.js
const { Users } = require('../../models');
const jwt = require('jsonwebtoken')

module.exports = async (req, res) => {
  // TODO: urclass의 가이드를 참고하여 GET /refreshtokenrequest 구현에 필요한 로직을 작성하세요.
  
  
 //refresh Token 유효성 검사
  if(!req.cookies.refreshToken){
    res.status(400).json({data : null, message :'refresh token not provided'})
  }else{
    let verifiedData
    jwt.verify(req.cookies.refreshToken, process.env.REFRESH_SECRET, (error,decoded)=>{
      if(error){
        res.status(400).json({data : null, message :'invalid refresh token, please log in again'})
        return;
      }else{
        verifiedData = decoded
      }
    })


      const userInfo = await Users.findOne({ where : {userId: verifiedData.userId}})
      const payload = {
        id : userInfo.dataValues.id,
        userId: userInfo.dataValues.userId,
        email : userInfo.dataValues.email,
        createdAt : userInfo.dataValues.createdAt,
        updatedAt : userInfo.dataValues.updatedAt
  
      }
      //accessToken 새로 발급
      const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, { expiresIn: 60 * 60 });
      delete verifiedData.iat;
      res.status(200).json({data: { "accessToken": accessToken, "userInfo" : verifiedData }, message: "ok"})


    

  }
};

 

 

 

3. OAuth 인증 방식 (Oauth 2.0, SNS로그인)

 

Oauth란?

외부서비스의 인증(Authentication) 및 권한부여(Authorization)를 관리하는 범용 프로토콜. Facebook같은 SNS 로그인 방식이 이에 포함된다.

 

그러면 인증은 뭐고 권한부여는 뭘까?

Authentication(인증) vs Authorization(권한)
보통 Auth라고 하면 Authentication을 말하거나, Authentication+Authorization을 통칭한다.

Authentication: 로그인 인증
로그인 자격 증명을 확인하여 로그인 한 사용자를 인식하는 것.
인증에 사용되는 자격 증명 데이터와 이미 저장된 데이터를 비교함
이 데이터는 인증 서버에 저장.
가장 일반적인 방법은 비밀번호 사용

Authorization: 권한
액세스 제어로 사용자가 읽기, 수정 삭제를 허용하는지 여부를 확인하는 것.
권한 부여는 사용자 정보가 성공적으로 인증(Authentication) 된 후에 발생.

한 줄 요약
Authentication으로 안전한 사용자인지 확인한 후에 Authorization으로 세부 권한을 준다.

 

Oauth가 세션, 토큰 기반 인증방식을 완전히 대체하는 것은 아니다. SNS 로그인 기능을 넣더라도 세션이나 토큰을 활용해서 인증을 거쳐야 한다.

 

Oauth2.0이 Oauth 1.0에 비해 달라진 점

1. 모바일 어플에서 사용 용이

2. 반드시 HTTPS 사용

3. Access Token 만료 기간 생김

 

Oauth 2.0 인증 방식

1. Authorization Code Grant (가장 많이 쓰임)

2. Implicit Grant

3. Resource Owner Password

4. Client Credentials Grant

 

 

Resource Owner : 유저

Client : 우리가 관리하는 어플리케이션 서버. (어플의 프론트엔드, 백엔드를 모두 포함하고 있다고 생각하면 편함)

Authorization Server : 권한 관리 서버, Access Token, Refresh Token을 발급함

Resource Server : OAuth 2.0을 관리하는 서버 Facebook같은 SNS의 자원을 관리하는 서버

역할 구분 Token 인증 방식 Oauth 인증 방식
요청 사용자, Client 사용자, Client(Frontend)
응답 Server Client(Backend)
토큰 관리 Server Authorization Server(SNS)
자원 관리 DB Resource Server(SNS), DB

 

1. Resource Owner가 Client에 인증 요청

2. Client는 사용자를 인증할 수단(ex. Facebook 로그인 URL)로 Redirection

3. Resource Owner는 해당 Request를 통해 인증을 진행하고 Authorization Grant를 URL에 실어 Client에 보냄

4. Client는 Authorization Grant(권한 증서)를 Authorization Server에 보냄

5. Authorization Server는 권한 증서 확인 후, Client에게 Access Token, Refresh Token, User정보를 발급해줌

6. Client는 해당 Token을 DB에 저장하거나 Resource Owner에게 넘김.

7. Resource Owner(사용자)가 Resource Server에 자원이 필요하면 Client는 Access Token을 담아 자원 요청

8. Resoruce Server는 Access Token 유효성 검사 후 Client에 자원 보냄. (만일 Access Token이 만료되었다면 Client는 Authorization Server에 refresh Token을 보내 Access Token을 재발급 받음. 만일 Refresh Token도 만료되었을 경우 Resource Owner는 다시 SNS에 로그인해서 Authorization Grant를 Client에 넘겨야 함.)

9. Client는 받은 자원을 바탕으로 User와 세션/토큰 기반 인증 진행

 

 

 

예제 코드

1~3 과정

//client-oauth/src/components/Login.js

import React, { Component } from 'react';

class Login extends Component {
  constructor(props) {
    super(props)

    this.socialLoginHandler = this.socialLoginHandler.bind(this)

    // TODO: GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 합니다. 적절한 URL을 입력하세요.
    // OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 합니다.
    // 참고: https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps

    this.GITHUB_LOGIN_URL = 'https://github.com/login/oauth/authorize?client_id=clientID' //유효한 Client ID 넣어야함
  }
  socialLoginHandler() {
    window.location.assign(this.GITHUB_LOGIN_URL)
  }
  render() {
    return (
      <div className='loginContainer'>
        OAuth 2.0으로 소셜 로그인을 구현해보세요.
        <img id="logo" alt="logo" src="https://image.flaticon.com/icons/png/512/25/25231.png" />
        <button
          onClick={this.socialLoginHandler}
          className='socialloginBtn'
        >
          Github으로 로그인
          </button>
      </div>
    );
  }
}

export default Login;

4~6 과정 (여기선 Refresh Token 안쓰고 Access Token만 씀)

//server-ouath/controller/callback.js

require('dotenv').config();

const clientID = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const axios = require('axios');


module.exports = async (req, res) => {
  // req의 body로 authorization code가 들어옵니다. console.log를 통해 서버의 터미널창에서 확인해보세요!

    const params = {
    client_id : clientID,
    client_secret : clientSecret,
    code : req.body.authorizationCode
  }
  // TODO : 이제 authorization code를 이용해 access token을 발급받기 위한 post 요청을 보냅니다. 다음 링크를 참고하세요.

    const response = await axios({
      method : 'post',
      url: 'https://github.com/login/oauth/access_token',
      headers : {accept: 'application/json'},
      data: params
    })
  
    if(response){
      const accessToken=response.data.access_token;
      res.status(200).send({accessToken:accessToken});
    }else{
      res.status(400)
    }
  // TODO : 이제 authorization code를 이용해 access token을 발급받기 위한 post 요청을 보냅니다. 다음 링크를 참고하세요.
  // https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps#2-users-are-redirected-back-to-your-site-by-github
}

7~8 과정

// client-oauth/src/componets/Mypage.js

import React, { Component } from "react";
import axios from 'axios';
const FILL_ME_IN = 'FILL_ME_IN'

class Mypage extends Component {

  constructor(props) {
    super(props);
    this.state = {
      images: [],
      name: '',
      login : '',
      html_url : '',
      public_repos : ''
      // TODO: GitHub API 를 통해서 받아올 수 있는 정보들 중에서
      // 이름, login 아이디, repository 주소, public repositoty 개수를 포함한 다양한 정보들을 담아주세요.
    }
  }

  async getGitHubUserInfo() {
    // TODO: GitHub API를 통해 사용자 정보를 받아오세요.
    // https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-the-authenticated-user
  const response = await axios ({
    headers: {authorization : `token ${this.props.accessToken}`,
              accept: 'application/json'},
    method :'get',
    url: 'https://api.github.com/user'
  })

  this.setState({name : response.data.name,
            login : response.data.login,
            html_url : response.data.html_url,
            public_repos : response.data.public_repos          
  })
}

  async getImages() {
    // TODO : 마찬가지로 액세스 토큰을 이용해 local resource server에서 이미지들을 받아와 주세요.
    // resource 서버에 GET /images 로 요청하세요.
    
    const response = await axios({
      method: 'get',
      url: 'http://localhost:8080/images',
      headers: {authorization : `token ${this.props.accessToken}`,
                accept: 'application/json'},
    });
    if(response){
      this.setState({images : response.data.images})

    }
  }

  componentDidMount() {
    this.getGitHubUserInfo()
    this.getImages()
  }

  render() {
    const { accessToken } = this.props
    if (!accessToken) {
      return <div>로그인이 필요합니다</div>
    }
    return (
      <div>
        <div className='mypageContainer'>
          <h3>Mypage</h3>
          <hr />

          <div>안녕하세요. <span className="name" id="name">{this.state.name}</span>님! GitHub 로그인이 완료되었습니다.</div>
          <div>
            <div className="item">
              나의 로그인 아이디:
              <span id="login">{this.state.login}</span>
            </div>
            <div className="item">
              나의 GitHub 주소:
              <span id="html_url">{this.state.html_url}</span>
            </div>
            <div className="item">
              나의 public 레포지토리 개수:
              <span id="public_repos">{this.state.public_repos}</span>개
            </div>
          </div>

          <div id="images">
            {/* TODO: 여기에 img 태그를 이용해 resource server로 부터 받은 이미지를 출력하세요 */}
            {this.state.images.map(el=>{return <img src ={el.blob}/>})}
          </div>
        </div>
      </div >
    );
  }

}

export default Mypage;

 

 

 

 

 

 

 

 

 

 

참고자료 출처 : https://tansfil.tistory.com/

'Development > Server' 카테고리의 다른 글

res.json() , res.end() , res.send()  (0) 2022.04.29
Express.js  (0) 2022.04.18