https://jwt.io/ --- JSON WEB TOKEN 공식 문서
초기 셋팅
1. env 파일 생성 >> MYSQL DB 생성
2. cer.pem & key.pem 서버 디렉토리로 복사
server-token/controllers/users/accessTokenRequest.js
const { Users } = require('../../models');
const jwt = require('jsonwebtoken');
module.exports = (req, res) => {
// TODO: urclass의 가이드를 참고하여 GET /accesstokenrequest 구현에 필요한 로직을 작성하세요.
// authorization header에 담긴 토큰이 서버에서 생성한 JWT인지 확인합니다.
// 서버에서 생성한 유효한 토큰일 경우, 유효하지 않은 토큰일 경우 각각 다른 응답을 반환합니다.
// console.log("요청확인", req.headers);
// console.log("토큰 있는가?", req.headers.authorization);
//로그인 성공했으면 이미 쿠키에 토큰을 가지고 있을것
//하지만 토큰이 없으면 쳐내기
if(!req.headers.authorization){
res.status(404).send({ "data": null, "message": "invalid access token" })
}else{
const authorization = req.headers['authorization'];
const token = authorization.split(' ')[1];
const data = jwt.verify(token, process.env.ACCESS_SECRET); //올바른 토큰인지 검증
// console.log(data)
if(!data){ //애초에 없는 유저라면 쳐내기
res.status(400).send({ "data": null, "message": "invalid access token" })
}else{ //맞는 유저면 그 유저의 데이터 보내주기
res.status(200).send({
data:{
userInfo:{
id: data.id,
userId: data.userId,
email: data.email,
createdAt: data.createdAt,
updatedAt: data.updatedAt
}
}, message:'ok'
})
}
}
};
server-token/controllers/users/login.js
const { Users } = require('../../models');
const jwt = require('jsonwebtoken');
module.exports = async(req, res) => {
// TODO: urclass의 가이드를 참고하여 POST /login 구현에 필요한 로직을 작성하세요.
//request로부터 받은 userId, password와 일치하는 유저가 DB에 존재하는지 확인합니다.
// 일치하는 유저가 없을 경우:
/// 로그인 요청을 거절합니다.
/// 일치하는 유저가 있을 경우:
// 필요한 데이터를 담은 두 종류의 JWT(access, refresh)를 생성합니다.
// 생성한 JWT를 적절한 방법으로 반환합니다.
//// access token은 클라이언트에서 react state로 다루고 있습니다.
// refresh token은 클라이언트의 쿠키에서 다루고 있습니다.
//보안을 철저히 하기 위해 쿠키 셋팅을 해줌
//쿠키 탈취 방지를 위해 쿠키 유효기간도 셋팅
const cookieSetUp = {
sameSite: "none",
httpOnly: true,
secure: true,
maxAge: 5 * 60 * 1000,
};
const userInfo = await Users.findOne({
where: { userId: req.body.userId, password: req.body.password },
});
if(!userInfo){
res.status(404).send({ "data": null, "message": "not authorized" });
}else{
const payload = {
id : userInfo.id ,
userId : userInfo.userId,
email: userInfo.email,
createdAt: userInfo.createdAt,
updatedAt: userInfo.updatedAt
}
const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, { expiresIn: '1d' }); // 유효기간 1일
const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, { expiresIn: '2d' }); // 2일
res.cookie('refreshToken', refreshToken) //쿠키에 리프레시 토큰, 데이터에 액세스 토큰 넣어서 보냄
res.status(200).send({"data": { "accessToken": accessToken }, "message": "ok"})
}
};
server-token/controllers/users/refreshTokenRequest.js
const { Users } = require('../../models');
const jwt = require('jsonwebtoken');
module.exports = (req, res) => {
// TODO: urclass의 가이드를 참고하여 GET /refreshtokenrequest 구현에 필요한 로직을 작성하세요.
//요청에 담긴 refresh token이 유효하다면 새로운 access token을 발급해줌과 동시에 유저가 요청한 정보를 반환합니다.
//요청에 담긴 refresh token이 유효하지 않거나, 조작된 토큰일 경우 각각 다른 응답을 반환합니다.
const isRefreshToken = req.cookies.refreshToken
// 토큰 값이 있는지 확인
if(!isRefreshToken){
res.status(400).send({ "data": null, "message": "refresh token not provided" });
// 토큰이 유효한지 확인
}else if(isRefreshToken === 'invalidtoken'){
res.status(400).send({ "data": null, "message": "invalid refresh token, please log in again" });
// 토큰이 유효한데 해독한 데이터가 db의 정보와 같은지
}else{
const data = jwt.verify(isRefreshToken, process.env.REFRESH_SECRET);
if(!data){
res.status(400).send({ "data": null, "message": "refresh token has been tempered" })
// 정보가 같다면 payload에 데이터와
// accessToken을 새로 생성해서 보내줘야 한다
}else{
const payload = {
id : data.id ,
userId : data.userId,
email: data.email,
createdAt: data.createdAt,
updatedAt: data.updatedAt
}
const accessToken = jwt.sign(payload, process.env.ACCESS_SECRET, { expiresIn: '1d' });
res.status(200).send({
"data":{
accessToken: accessToken,
userInfo: payload
}, message:'ok'
})
}
}
};
client-token/package.json
..... 중략
"scripts": {
"start": "HTTPS=true SSL_CRT_FILE='../server-token/cert.pem' SSL_KEY_FILE='../server-token/key.pem' react-scripts start",
"build": "react-scripts build",
"test": "./node_modules/.bin/mocha --require ./node_modules/@babel/register '__tests__/client.test.js' --reporter ./node_modules/mocha-multi-reporters --reporter-options configFile=multi-reporters.json || true",
"report": "./node_modules/.bin/mocha --require @babel/register '__tests__/client.test.js' --reporter @mochajs/json-file-reporter",
"submit": "codestates"
},
..... 중략
client-token/src/App.js
import React, { Component } from "react";
import Login from "./components/Login";
import Mypage from "./components/Mypage";
class App extends Component {
constructor(props) {
super(props);
this.state = {
isLogin: false,
accessToken: "",
};
this.loginHandler = this.loginHandler.bind(this);
this.issueAccessToken = this.issueAccessToken.bind(this);
}
loginHandler(data) {
this.setState({isLogin: true})
}
issueAccessToken(token) {
this.setState({accessToken: token})
}
render() {
const { isLogin } = this.state;
return (
<div className='App'>
{/*
TODO: isLogin 상태에 따라 Mypage 혹은 Login 컴포넌트를 렌더해야합니다.
알맞은 컴포넌트를 렌더링하는것은 물론, 올바른 props전달하도록 작성하세요.
*/}
{isLogin ?
<Mypage
accessToken={this.state.accessToken}
issueAccessToken={this.issueAccessToken}
/> :
<Login
loginHandler={this.loginHandler}
issueAccessToken={this.issueAccessToken}
/>}
</div>
);
}
}
export default App;
client-token/src/components/Login.js
import axios from "axios";
import React, { Component } from "react";
class Login extends Component {
constructor(props) {
super(props);
this.state = {
userId: "",
password: "",
};
this.inputHandler = this.inputHandler.bind(this);
this.loginRequestHandler = this.loginRequestHandler.bind(this);
}
inputHandler(e) {
this.setState({ [e.target.name]: e.target.value });
}
loginRequestHandler() {
/*
TODO: Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현합니다.
로그인을 담당하는 api endpoint에 요청을 보내고, 받은 데이터로 상위 컴포넌트 App의 state를 변경하세요.
초기 App:
state = { isLogin: false, accessToken: "" }
로그인 요청 후 App:
state = { isLogin: true, accessToken: 서버에_요청하여_받은_access_token }
*/
axios.post('https://localhost:4000/login', {
userId: this.state.userId,
password: this.state.password
})
.then(res=> {
this.props.issueAccessToken(res.data.data.accessToken)
this.props.loginHandler(res.data.data.accessToken)
} )
// data: { data: { accessToken: 'fakeAccessToken' } }
}
render() {
return (
<div className='loginContainer'>
<div className='inputField'>
<div>Username</div>
<input
name='userId'
onChange={(e) => this.inputHandler(e)}
value={this.state.userId}
type='text'
/>
</div>
<div className='inputField'>
<div>Password</div>
<input
name='password'
onChange={(e) => this.inputHandler(e)}
value={this.state.password}
type='password'
/>
</div>
<div className='loginBtnContainer'>
<button onClick={this.loginRequestHandler} className='loginBtn'>
JWT Login
</button>
</div>
</div>
);
}
}
export default Login;
client-token/src/components/Mypage.js
import axios from "axios";
import React, { Component } from "react";
class Mypage extends Component {
constructor(props) {
super(props);
this.state = {
userId: "",
email: "",
createdAt: "",
};
this.accessTokenRequest = this.accessTokenRequest.bind(this);
this.refreshTokenRequest = this.refreshTokenRequest.bind(this);
}
accessTokenRequest() {
/*
TODO: 상위 컴포넌트인 App에서 받은 props를 이용해 accessTokenRequest 메소드를 구현합니다.
access token을 처리할 수 있는 api endpoint에 요청을 보내고, 받은 데이터로 Mypage 컴포넌트의 state (userId, email, createdAt)를 변경하세요
초기 Mypage:
state = { userId: "", email: "", createdAt: "" }
accessTokenRequest 후 Mypage:
state = { userId: "특정유저id", email: "특정유저email", created: "특정유저createdAt" }
** 주의사항 **
App 컴포넌트에서 내려받은 accessToken props를 authorization header에 담아 요청을 보내야 합니다.
*/
axios.get('https://localhost:4000/accesstokenrequest', {headers: {authorization: `Bearer ${this.props.accessToken}`}})
.then(res => {
this.setState({
...res.data.data.userInfo
})
})
}
refreshTokenRequest() {
/*
TODO: 쿠키에 담겨져 있는 refreshToken을 이용하여 refreshTokenRequest 메소드를 구현합니다.
refresh token을 처리할 수 있는 api endpoint에 요청을 보내고, 받은 데이터로 2가지를 구현합니다.
1. Mypage 컴포넌트의 state(userId, email, createdAt)를 변경
2. 상위 컴포넌트 App의 state에 accessToken을 받은 새 토큰으로 교환
*/
axios.get('https://localhost:4000/refreshtokenrequest')
.then(res=>{
this.setState({
...res.data.data.userInfo
})
this.props.issueAccessToken(res.data.data.accessToken)
})
}
render() {
const { userId, email, createdAt } = this.state;
return (
<div className='mypageContainer'>
<div className='title'>Mypage</div>
<hr />
<br />
<br />
<div>
안녕하세요. <span className='name'>{userId ? userId : "Guest"}</span>님! jwt 로그인이
완료되었습니다.
</div>
<br />
<br />
<div className='item'>
<span className='item'>나의 이메일: </span> {email}
</div>
<div className='item'>
<span className='item'>나의 아이디 생성일: </span> {createdAt}
</div>
<br />
<br />
<div className='btnContainer'>
<button className='tokenBtn red' onClick={this.accessTokenRequest}>
access token request
</button>
<button className='tokenBtn navy' onClick={this.refreshTokenRequest}>
refresh token request
</button>
</div>
</div>
);
}
}
export default Mypage;