[restify] 회원가입 및 로그인, 카카오 로그인, 로그아웃, 회원인증까지

리진아·2023년 8월 23일
0
post-thumbnail

프론트에서 만든 로그인 회원가입 폼이다. 이제 이에 맞는 서비스 로직을 구현할 것이다.



회원가입

회원가입은 간단하게 구현했다.
어차피 프론트에서도 유효성검사를 해주기 때문에 서버에서도 간단하게 구현했고,
db에서도 간단하게 insert를 사용해서 구현했다.

// 라우터
// api의 엔드포인트
server.post('/api/SignUpPage', UserController.signUp);



// 컨트롤러
async signUp(req, res) {
  try {
    const { email, pass, name, age } = req.body;

    // 유효성 검사
    if (!name) {
      res.send(400, { message: '이름을 입력하세요' });
      throw new Error('이름을 입력하세요');
    }
    if (!/^[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z]+$/.test(email)) {
      res.send(400, { message: '이메일을 입력하세요' });
      throw new Error('이메일을 입력하세요');
    }
    if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$/.test(pass)) {
      res.send(400, { message: '비밀번호는 영어와 숫자를 포함하세요' });
      throw new Error('비밀번호는 영어와 숫자를 포함하세요');
    }else{
      
      //유효성 검사를 마친 후 코드
      const resultMessage = await UserService.signUp(email, pass, name, age); // 회원가입 서비스 메서드 호출
      res.send(resultMessage); // '회원가입 성공'
    } 
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: '회원가입 오류' });
  }
},
  
  
  
//서비스  
async signUp(email, pass, name, age) {
    const conn = await getConn();
    try {
        const saltRounds = 10;
        // 암호화된 비밀번호로 설정
        const hashedPassword = await bcrypt.hash(pass, saltRounds);

        const query = 'INSERT INTO UserTable (userEmail, userPassword, userName, userAge) VALUES (?, ?, ?, ?);';
        // 정보를 insert하기
        await conn.query(query, [email, hashedPassword, name, age]);

        return '회원가입 성공';
    } catch (error) {
        throw error;
    } finally { 
        conn.release();
    }
},  

회원가입을 하니 db에 잘 저장이 되었다.





로그인

로직
1. 로그인 로직은 먼저 SELECT로 해당하는 이메일을 조회한 다음 없으면 아이디가 없다 반환
2. 있으면 암호화된 비밀번호를 match함수로 검사. 불일치면 비밀번호 불일치 반환,
3. 일치하면 이메일, 닉네임, ID(auto increment)를 jwt 토큰에 담아 토큰을 리턴,
4. 클라이언트에서 받은 토큰을 쿠키에 저장한 후 로그인 인증 구현

// api 라우터 경로
server.get('/api/LoginPage', UserController.login);



// 컨트롤러
async login(req, res) {
    try {
        const { ID, password } = req.query;

        // 유효성 검사
        if(!ID || !password){
            res.send(400, { message: '내용을 입력하세요' });
            throw new Error('내용을 입력하세요');
        }else{
          	// 메서드를 호출 후 반환하는 리턴 값을 token 변수에 넣음
            const token = await UserService.login(ID, password, res);
            res.send({ token }); // 토큰을 응답 값으로 보냄
        }
    } catch (error) {
        console.error(error);
        res.status(500).json({ message: '로그인 실패' });
    }
},
  
  
  
// 서비스
async login(ID, password) {
    const conn = await getConn();
    try {
        const selectQuery = 'SELECT * FROM UserTable WHERE userEmail = ?;';
      	// 아이디를 가지고 조회하기
        const [selectUserResult] = await conn.query(selectQuery, [ID]);
    
        if (selectUserResult.length === 0) {
            return '아이디가 없습니다';
        }

        const UserID = await conn.query(selectQuery, [ID]);
    
        // 암호화된 비밀번호를 조회하기
        const isMatch = await bcrypt.compare(password, selectUserResult[0].userPassword);
    
        if (isMatch) {
          // 일치하면 로그인
            const enNickname = encodeURIComponent(selectUserResult[0].userName);
            const enEmail = encodeURIComponent(selectUserResult[0].userEmail);
    
          	//jwt 토큰을 생성
            const token = jwt.sign(
            {
                UserName: enNickname,
                UserEmail: enEmail,
                UserID: UserID[0][0].userID 
            },
            'your-secret-key',
            { expiresIn: '1h' }
            );
    
            return token; // 토큰 반환
    
        } else {
          	// 비밀번호가 일치하지 않으면
            return '비밀번호 불일치';
        }
    } catch (error) {
        throw error;
    } finally {
        conn.release();
    }
},

❗️ 에러 match함수 false

암호화한 비밀번호를 match함수를 사용해서 비교하는데 계속 에러가 났다. 코드가 잘못되어 에러가 계속 난 줄 알았는데 db의 password 필드가 varchar(50)으로 되어있어서 에러가 난 것이였다., (비교해도 값이 잘려서 false가 나온 상태)

테이블을 다시 생성하거나 수정하여 password 값을 100이상으로 바꿔주니 잘 되었다!

⬇️ 로그인 후 저장된 쿠키 값

⬇️ url에서 자체 로그인





카카오 로그인

카카오 디벨로퍼 설정

내 애플리케이션에서 앱 생성


사이트 도메인 설정


리다이렉트 url 추가


코드 작성

카카오 로그인 로직
클라이언트에서 로그인 버튼을 누르고 아이디 비번 입력 -> 클라에서 카카오 서버로 요청 -> 카카오 서버에서 인가코드를 발급 후 클라이언트로 전달 -> 클라이언트가 받은 인가코드를 서버로 전달 -> 서버에서 카카오 서버에 회원 인증 -> 인증 후 받은 token에서 고유키, 닉네임, 이메일을 도출 -> 이메일로 회원 조회를 해서 회원 정보가 없으면 회원가입 -> 회원 정보가 있으면 jwt 토큰에 회원 정보(고유키, 이메일, 닉네임, userID)를 넣어 반환

// 라우터
server.get('/api/Kakao', UserController.kakaoLogin);



// 컨트롤러
async kakaoLogin(req, res) {
    try {
        const { code } = req.query;

        // 유효성 검사
        if(!code){
            throw new Error('카카오 로그인 에러');
        }else{
            const token = await UserService.kakaoLogin(code, res);
            res.send({ token }); // 토큰

        }
    } catch (error) {
        console.error(error);
        res.status(500).json({ message: '카카오 로그인 실패' });
    }
},
  
  
  
// 서비스
async kakaoLogin(code, res) {
    const conn = await getConn();
    try {
    const header = { 'Content-Type': 'application/x-www-form-urlencoded' };
      //인증 요청
    const response = await axios.post(
        'https://kauth.kakao.com/oauth/token',
        {
        grant_type: 'authorization_code',
        client_id: 'restapi키', 
        client_secret: '시크릿 키', 
        redirect_uri: 'http://localhost:3000/loginpage/Kakao',
        code,
        },
        { headers: header }
    );

    // 엑세스 토큰을
    const Token = response.data.access_token;

      //받음
    const userResponse = await axios.get('https://kapi.kakao.com/v2/user/me', {
        headers: {
        Authorization: `Bearer ${Token}`,
        },
    });

      // 엑세스 토큰 안에 있는 회원 정보들
    let sub = userResponse.data.id; // 회원 고유 키
    const nickname = userResponse.data.kakao_account.profile.nickname; // 회원 닉네임
    const email = userResponse.data.kakao_account.email; //회원 이메일

    // 인코딩
    const enNickname = encodeURIComponent(nickname);
    const enEmail = encodeURIComponent(email);

      //회원이 있는지 확인하는 쿼리
    const selectQuery = "select userEmail, userID from UserTable where userEmail = ?;";
    const [Result] = await conn.query(selectQuery, [email]);

    let ID = null;

    if (Result.length === 0) { //회원가입
        const insertQuery = 'call usp_post_user(?, ?, ?);';
        const insertValue = [email, nickname, sub];

        //회원가입하고 바로 userID를 반환
        const [resultID] = await conn.query(insertQuery, insertValue);
        ID = resultID[0][0].userID
    } else if (Result.length > 0) { //회원이 이미 있을 때
        ID = Result[0].userID;
    }
    
      //jwt 토큰을 만듦
    let = token = jwt.sign(
        {
            UserEmail: enEmail,
            UserID: ID,
            UserName: enNickname,
            Sub: sub
        },
        'your-secret-key',
        { expiresIn: '1h' }
    );

    return token; // 토큰 반환
    
    } catch (error) {
        throw error;
    } finally {
        conn.release();
    }
},



성찰 사항 (서버에서 쿠키로 전달한 token)

처음에는 카카오 서버에서 주는 인가코드를 클라이언트에서 받는 것이 아닌 서버에서 받은 후 로직을 구현하여 jwt token을 쿠키에 저장하여 전달했다.

그런데 배포하니 서버와 클라의 도메인이 달라 쿠키를 전달하지 못한 상황이 되었다.

그래서 클라이언트에서 인가코드를 받고 서버는 인증 후 token만 반환값으로 보내는 역할만 하는 것이 옳다고 생각하여 코드를 수정했다.


미리 알았으면 두 번 고생하지 않아도 되는 것들이였던 것 같다.

⬇️ 로그인 후 저장된 쿠키 값




로그아웃

로그아웃은 기존에 로그인했을 때 토큰을 생성한 후 쿠키에 저장한 다음 회원 인증을 하기 때문에 쿠키 값을 초기화 해주는 형식으로 로직을 구현했다.

    async logout(req, res) {
        try {
            const resultMessage = await UserService.logout();
            res.setHeader('Set-Cookie', [`token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`]);
            res.send(resultMessage); // '로그아웃 성공'
        } catch (error) {
            console.error('로그아웃 실패:', error.message);
            res.status(500).json({ message: '서버 오류' });
        }
    }




더 나아가 회원인증..

어느 웹사이트든 로그인을 해야만 이용할 수 있는 서비스들이 있기 마련이다.
우리 프로젝트 같은 경우에는 어드민페이지, 댓글, 좋아요 기능은 로그인을 해야만 이용할 수 있다.

일반 로그인, 카카오 로그인 모두 token을 쿠키에 저장하도록 구현했기 때문에 쿠키에 있는 token 값을 빼온 후 jwt 인증 단계를 거쳐 이에 맞는 결과를 반환한 후 댓글, 좋아요 기능을 사용할 수 있도록 해보았다.

쿠키에서 token값 추출

function CookieAuth(cookies) { //cookies라는 매개변수를
    if (typeof cookies === 'string') { //문자열인지 확인
        const resultCookie = cookies.split(';'); //; 으로 나눔
        const tokenCookie = resultCookie.find(cookie => cookie.trim().startsWith('token=')); //토큰부분만 빼내기
        if (tokenCookie) {
            const token = tokenCookie.split('=')[1];
            //토큰만 추출
            return token.trim();    
        }
    }
    return null;
}

토큰 검증

//라우터
server.get('/api/Token', UserController.verifyToken);



//서비스
async verifyToken(cookies) {
    try {
        const token = CookieAuth(cookies);
        if (token) {
            jwt.verify(token, 'your-secret-key');
            return '토큰 인증 성공';
        } else {
            return '쿠키에 토큰이 없음';
        }
    } catch (error) {
        throw error;
    }
},
  
  
  
//컨트롤러
async verifyToken(req, res) {
    try {
        const cookies = req.headers.cookie;
        const resultMessage = await UserService.verifyToken(cookies);
        res.send(resultMessage); // '토큰 인증 성공'
    } catch (error) {
        console.error(error);
        res.json({ message: '토큰 인증 실패' });
    }
},

이렇게 토큰을 검증하는 로직을 구현했다. 아래와 같은 예시로 헤더에 있는 쿠키를 보낸 후 응답 값인 tokenResponse.data의 값이 '토큰 인증 성공'일 경우와 '토큰 인증 실패'일 경우에 따라 로직을 구현하면 된다.

const tokenResponse = await axios.get('http://localhost:4000/api/Token', {
                headers: {
                Cookie: req.headers.cookie,
                },
            });







전체 코드

userRoute.js

const UserController = require('../controllers/userController');

module.exports = (server) => {
    // 회원가입
    server.post('/api/SignUpPage', UserController.signUp);
    // 로그인
    server.get('/api/LoginPage', UserController.login);
    //카카오 소셜 로그인
    server.get('/api/Kakao', UserController.kakaoLogin);
    // 토큰 인증
    server.get('/api/Token', UserController.verifyToken);
    // 로그아웃
    server.post('/api/logout', UserController.logout);
};

userController.js

const UserService = require('../service/userService');

const UserController = {

    // 회원가입
    async signUp(req, res) {
        try {
            const { email, pass, name, age } = req.body;

            // 유효성 검사
            if (!name) {
                res.send(400, { message: '이름을 입력하세요' });
                throw new Error('이름을 입력하세요');
            }
            if (!/^[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z]+$/.test(email)) {
                res.send(400, { message: '이메일을 입력하세요' });
                throw new Error('이메일을 입력하세요');
            }
            if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$/.test(pass)) {
                res.send(400, { message: '비밀번호는 영어와 숫자를 포함하세요' });
                throw new Error('비밀번호는 영어와 숫자를 포함하세요');
            }else{
                const resultMessage = await UserService.signUp(email, pass, name, age);
                res.send(resultMessage); // '회원가입 성공'
            } 
        } catch (error) {
            console.error(error);
            res.status(500).json({ message: '회원가입 오류' });
        }
    },


    // 로그인
    async login(req, res) {
        try {
            const { ID, password } = req.query;

            // 유효성 검사
            if(!ID || !password){
                res.send(400, { message: '내용을 입력하세요' });
                throw new Error('내용을 입력하세요');
            }else{
                const token = await UserService.login(ID, password, res);
                res.send({ token }); // 토큰
            }
        } catch (error) {
            console.error(error);
            res.status(500).json({ message: '로그인 실패' });
        }
    },


    // 카카오 로그인
    async kakaoLogin(req, res) {
        try {
            const { code } = req.query;

            // 유효성 검사
            if(!code){
                throw new Error('카카오 로그인 에러');
            }else{
                const token = await UserService.kakaoLogin(code, res);
                res.send({ token }); // 토큰

            }
        } catch (error) {
            console.error(error);
            res.status(500).json({ message: '카카오 로그인 실패' });
        }
    },


    // 토큰 검증
    async verifyToken(req, res) {
        try {
            const cookies = req.headers.cookie;
            const resultMessage = await UserService.verifyToken(cookies);
            res.send(resultMessage); // '토큰 인증 성공'
        } catch (error) {
            console.error(error);
            res.json({ message: '토큰 인증 실패' });
        }
    },

    
    // 로그아웃
    async logout(req, res) {
        try {
            const resultMessage = await UserService.logout();
            res.setHeader('Set-Cookie', [`token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`]);
            res.send(resultMessage); // '로그아웃 성공'
        } catch (error) {
            console.error('카카오 로그아웃 실패:', error.message);
            res.status(500).json({ message: '서버 오류' });
        }
    }

};

module.exports = UserController;

userService.js

const { getConn } = require('../database');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const axios = require('axios');

// 쿠키값 빼내는 함수
function CookieAuth(cookies) { //cookies라는 매개변수를
    if (typeof cookies === 'string') { //문자열인지 확인
        const resultCookie = cookies.split(';'); //; 으로 나눔
        const tokenCookie = resultCookie.find(cookie => cookie.trim().startsWith('token=')); //토큰부분만 빼내기
        if (tokenCookie) {
            const token = tokenCookie.split('=')[1];
            //토큰만 추출
            return token.trim();    
        }
    }
    return null;
}

const UserService = {

    // 회원가입
    async signUp(email, pass, name, age) {
        const conn = await getConn();
        try {
            const saltRounds = 10;
            // 암호화된 비밀번호로
            const hashedPassword = await bcrypt.hash(pass, saltRounds);

            const query = 'INSERT INTO UserTable (userEmail, userPassword, userName, userAge) VALUES (?, ?, ?, ?);';
            await conn.query(query, [email, hashedPassword, name, age]);

            return '회원가입 성공';
        } catch (error) {
            throw error;
        } finally { 
            conn.release();
        }
    },


    // 로그인
    async login(ID, password) {
        const conn = await getConn();
        try {
            const selectQuery = 'SELECT * FROM UserTable WHERE userEmail = ?;';
            const [selectUserResult] = await conn.query(selectQuery, [ID]);
        
            if (selectUserResult.length === 0) {
                return '아이디가 없습니다';
            }
    
            const UserID = await conn.query(selectQuery, [ID]);
        
            const isMatch = await bcrypt.compare(password, selectUserResult[0].userPassword);
        
            if (isMatch) {
                const enNickname = encodeURIComponent(selectUserResult[0].userName);
                const enEmail = encodeURIComponent(selectUserResult[0].userEmail);
        
                const token = jwt.sign(
                {
                    UserName: enNickname,
                    UserEmail: enEmail,
                    UserID: UserID[0][0].userID 
                },
                'your-secret-key',
                { expiresIn: '1h' }
                );
        
                return token; // 토큰 반환
        
            } else {
                return '비밀번호 불일치';
            }
        } catch (error) {
            throw error;
        } finally {
            conn.release();
        }
    },


    // 카카오 로그인
    async kakaoLogin(code, res) {
        const conn = await getConn();
        try {
        const header = { 'Content-Type': 'application/x-www-form-urlencoded' };
        const response = await axios.post(
            'https://kauth.kakao.com/oauth/token',
            {
            grant_type: 'authorization_code',
            client_id: 'rest api 키', 
            client_secret: '시크릿 키', 
            redirect_uri: 'http://localhost:3000/loginpage/Kakao',
            code,
            },
            { headers: header }
        );
    
        // 엑세스 토큰
        const Token = response.data.access_token;
    
        const userResponse = await axios.get('https://kapi.kakao.com/v2/user/me', {
            headers: {
            Authorization: `Bearer ${Token}`,
            },
        });
    
        let sub = userResponse.data.id; // 회원 고유 키
        const nickname = userResponse.data.kakao_account.profile.nickname; // 회원 닉네임
        const email = userResponse.data.kakao_account.email; //회원 이메일
    
        // 인코딩
        const enNickname = encodeURIComponent(nickname);
        const enEmail = encodeURIComponent(email);
    
        const selectQuery = "select userEmail, userID from UserTable where userEmail = ?;";
        const [Result] = await conn.query(selectQuery, [email]);
    
        let ID = null;
    
        if (Result.length === 0) { //회원가입
            const insertQuery = 'call usp_post_user(?, ?, ?);';
            const insertValue = [email, nickname, sub];

            //회원가입하고 바로 userID를 반환
            const [resultID] = await conn.query(insertQuery, insertValue);
            ID = resultID[0][0].userID
        } else if (Result.length > 0) { //회원이 이미 있을 때
            ID = Result[0].userID;
        }
        
        let = token = jwt.sign(
            {
                UserEmail: enEmail,
                UserID: ID,
                UserName: enNickname,
                Sub: sub
            },
            'your-secret-key',
            { expiresIn: '1h' }
        );

        return token; // 토큰 반환
        
        } catch (error) {
            throw error;
        } finally {
            conn.release();
        }
    },
    

    // 토큰 검증
    async verifyToken(cookies) {
        try {
            const token = CookieAuth(cookies);
            if (token) {
                jwt.verify(token, 'your-secret-key');
                return '토큰 인증 성공';
            } else {
                return '쿠키에 토큰이 없음';
            }
        } catch (error) {
            throw error;
        }
    },

    
    // 로그아웃
    async logout() {
        try {
            return '로그아웃 성공';
        } catch (error) {
            throw error;
        }
    }

};

module.exports = UserService;
profile
알맹이가 가득 찬 개발자가 되기 위해 한 걸음 더 다가가는,

0개의 댓글