인증 서비스 구현의 필요성과 절차

henry·2024년 10월 24일

사용자가 웹사이트에 로그인하여 서비스를 이용하려면 인증이 필수적.

HTTP의 Stateless 특성으로 인한 인증 시스템이 필요한 이유를 정리.


1. HTTP의 Stateless 특성

  • HTTP는 상태 비저장(Stateless) 프로토콜
  • 각 요청 간에 서버가 사용자 상태를 기억하지 않음
    • 예를 들어, 사용자가 한 번 로그인했다고 해서 이후의 모든 요청에 대해 서버가 그 사용자를 기억하지 않음.

예시 (Stateless의 문제점)

클라이언트 : "안녕하세요, 저는 user123입니다."
서버 : 첫 사용자의 요청을 처리하지만, 이후의 요청에서 "누구시죠?"라고 묻는 것과 같음

서버는 이전 요청의 사용자를 기억하지 않는 특성

➡️ 해결 필요성

서버는 각 요청마다 사용자가 누구인지 확인할 별도의 방법이 필요


2. 인증의 필요성

인증(Authentication)이 필요한 이유:

  • 보안 유지 : 사용자가 누구인지 확인해 특정 서비스나 데이터에 대한 접근을 제어
  • 연속된 세션 유지 : 사용자가 여러 요청을 할 때마다 서버가 사용자 식별 필요

3. 토큰 기반 인증의 절차

📌인증 흐름 예시

1. 사용자 요청

  • 클라이언트(브라우저)에서 "안녕하세요, 저는 user123입니다."라고 서버에 요청

2. 서버 인증 처리

  • 서버는 사용자의 신원을 확인한 후, 해당 정보를 토큰(Token)에 담아 생성

3. 토큰 전송

  • 서버는 생성된 토큰을 HTTP 응답 헤더에 담아 클라이언트에 전달

4. 클라이언트 저장

  • 클라이언트는 받은 토큰을 로컬 스토리지나 세션 스토리지에 저장

5. 요청 시 토큰 포함

  • 이후의 요청에 클라이언트는 저장된 토큰을 함께 전송

6. 서버 검증

  • 서버는 전달받은 토큰을 복호화하여 사용자 정보를 확인하고 요청을 처리

4. HTTP Stateless 문제 해결

  • 토큰 기반 인증은 HTTP의 Stateless 문제를 해결
  • 서버는 매 요청마다 클라이언트가 전달한 토큰을 통해 사용자를 식별
  • 연속된 요청에도 사용자가 인증된 상태를 유지


적용


1. 토큰 생성

  • 회원 가입된 회원인 test123@test.com이 로그인을 시도
  • 사용자로부터 입력받은 Email이 DB에 일치하는 데이터가 있는지를 확인
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(400).send('Auth failed, email not found');
  • bcrypt에 의해 암호화된 비밀번호입력받은 비밀번호를 비교
const isMatch = await user.comparePassword(req.body.password);
if (!isMatch) return res.status(400).send('Wrong password');

userSchema.methods.comparePassword = async function (plainPassword) {
   let user = this;
   const match = await bcrypt.compare(plainPassword, user.password);
   return match;
};
  • JsonWebToken을 사용하여 Token 생성
const jwt = require('jsonwebtoken');

const payload = {userId: user._id.toHexString()};

const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
  • 로그인 성공 시, 토큰 생성 성공


2. 토큰 체크

페이지가 변경되거나, 로그인 상태가 변경될 때마다 인증된 사용자인지 확인

	useEffect(() => {
      if (isAuth) {
         dispatch(authUser());
      }
   }, [isAuth, pathname]);

소스 코드 설명

- 사용자가 로그인한 상태가 되었을 때, authUser 액션이 실행
- 사용자가 다른 페이지로 이동할 때도 이 훅이 실행

소스 코드 설명 올바른 토큰인지를 확인하는 미들웨어 소스 코드

  let auth = async (req, res, next) => {
     const authHeader = req.headers['authorization'];

     const token = authHeader && authHeader.split(' ')[1];
     if (token === null) return res.sendStatus(401);

     try {
        const decode = jwt.verify(token, process.env.JWT_SECRET);
        const user = await User.findOne({ _id: decode.userId });
        if (!user) return res.status(400).send('존재하지 않는 정보입니다.');

        req.user = user;
        next();
     } catch (error) {
        next(error);
     }
  };

소스 코드 설명

  • 토큰을 생성할 때, payload에 id를 넣었기 때문에
  const payload = {userId: user._id.toHexString()};
  const accessToken = jwt.sign(payload, process.env.JWT_SECRET});
  • 해당 token을 복호화하면 id정보를 확인할 수 있다.
 const decode = jwt.verify(token, process.env.JWT_SECRET);

  • /users/auth 엔드포인트로 인증이 유효한지를 확인 요청을 보내게 된다.
  export const authUser = createAsyncThunk('user/authUser', async (_, thunkAPI) => {
     try {
        const response = await axiosInstance.get(`/users/auth`);
        return response.data;
     } catch (error) {
        console.log('authUser error : ', error);
        return thunkAPI.rejectWithValue(error.response.data || error.message);
     }
  });
  • auth 요청 시, 사용자에게 전달하기 위한 token값을 localStorage로 부터 저장
  axiosInstance.interceptors.request.use(
     async function (config) {
        config.headers.Authorization = 'Bearer ' + localStorage.getItem('accessToken');
        return config;
     },
     function (error) {
        return Promise.reject(error);
     },
  );


  • 기 가입된 회원 정보임을 확인할 수 있다.

0개의 댓글