Token 개념 정리 + im-sprint-auth-token 리뷰
Token
토큰이란 통행증, 회원증과 같은 개념
클라이언트로 로그인할 시 서버에서 인증을 거친 다음 토큰을 내어주는 방식으로,
다음 부터 서버에 요청시 토큰을 같이 보내어 인증을 가진다.
가장 대표적인 토큰 기반 인증은 'jwt'
인증정보가 어디에 저장되느냐? 특징
세션은 서버(세션스토어)+ 클라이언트(세션id가 담긴 쿠키)에 저장된다.
토큰은 클라이언트(Authorization header)에 저장된다.(jwt토큰)
이 특징을 통해 파생되는 특징
서버가 여러대 일 경우
-서비스가 수평 확장을 했거나
-로드 밸런싱
일 경우
- 토큰이 유리하다.
세션같은 경우는 인증정보를 여러 서버가 공유해야 한다. 클라이언트로만 인증하면 서버의 부담이 없다.
어떻게 jwt는 스스로를 안전하다고 증명하는가?
-signature :서명
-> salt를 포함해서 토큰 암호화
서버가 해야 하는 일은
salt, 서명검증(verify)을 할수있어야함
토큰기반 인증의 장점
1. Statelessness & Scalability (무상태성 & 확장성)
- 서버는 클라이언트에 대한 정보를 저장할 필요 없다.(토큰 해독이 되는지만 판단한다.)
- 클라이언트는 새로운 요청을 보낼때마다 토큰을 헤더에 포함시키면 된다.
- 서버를 여러개 가지고 있는 서비스라면 더더욱 빛을 발휘한다. (같은 토큰으로 여러 서버에서 인증 가능)
2. 안전하다
- 암호화 한 토큰을 사용하고, 암호화 키를 노출 할 필요가 없기 때문에 안전하다
3. 어디서나 생성 가능하다
- 토큰을 확인하는 서버가 토큰을 만들어야 하는 법이 없다.
- 토큰 생성용 서버를 만들거나, 다른 회사에서 토큰관련 작업을 맡기는 것 등 다양한 활용이 가능하다.
4. 권한 부여에 용이하다
- 토큰의 payload(내용물) 안에 어떤 정보에 접근 가능한지 정할 수 있다.(서비스의 사진과 연락처 사용권한만 부여할 수 있음)
JWT의 종류
1. Access Token: 짧아야 한다.(짧은 유효기간)
-> 권한을 가질 수 있게 해주는 부여에 사용
-> 클라이언트가 처음 인증을 받게 될 때(로그인 시),
access, refresh token 두가지를 다 받지만, 실제로 권한을 얻는 데 사용하는 토큰은 access token이다.
2.Refresh Token: 길어야함(기간)
-> 만료되면 로그인 풀림
->액세스 토큰을 새로 발급시켜준다.
->이때, 유저는 다시 로그인할 필요가 없다.
JWT의 구조
3부분으로 나뉜다.
Header
Header는 이것이 어떤 종류의 토큰인지(지금의 경우엔 JWT), 어떤 알고리즘으로 sign(암호화) 할지가 적혀있다.
JSON Web Token 이라는 이름에 걸맞게 JSON형태로 이런 형태를 볼 수 있다.
Payload
Payload에는 정보가 담겨 있다.
어떤 정보에 접근 가능한지에 대한 권한을 담을 수도 있고, 사용자의 유저 이름 등 필요한 데이터는 이곳에 담아 암호화 시킨다.
물론 암호화(헤더에서 정의한)가 될 정보지만, 민감한 정보는 되도록 담지 않는 것이 좋다.
Signature
base64로 인코딩된 첫번째, 그리고 두번째 부분이 완성 되었다면, 원하는 비밀 키(암호화에 추가할 salt)를 사용하여 암호화한다.
JWT 사용 흐름
1. 클라이언트가 아이디와 비빌번호를 담아 서버에 로그인 요청을 보낸다.
2. 아이디/비밀번호 확인후 일치한다면 클라이언트에게 암호화된 토큰을 생성한다.(access/refresh토큰 두개다!)
3. 토큰을 클라이언트에게 보내주면 클라이언트는 토큰을 저장한다.
-저장하는 위치는 local storage, cookie, react의 state등 다양함
4. 클라이언트가 http header에 토큰을 담아 보낸다.
5. 서버는 토큰을 해독하여, "우리가 발급해준 토큰이네" 라고 판단되면, 클라이언트의 요청을 처리하여 응답한다.
im-sprint-auth-token
server
1.여기서도 인증키 두개를 server와 client에 각각 복사해준다.
2. server/index.js 읽어보고 흐름 파악
3. controller/users들어가 파일 확인
4. .controllers/login.js (POST /login)
주의해야 할것은 urclass를 정말 잘읽고 그에 맞는 응답을 해줘야 한다.
처음 작성한 코드
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) {
return res.status(200).send({
"data" : null,
"message" : "not authorized"
});
}
else {
const userData = {
id: userInfo.id,
userId: userInfo.dataValues.userId,
email: userInfo.dataValues.email,
createdAt: userInfo.dataValues.createdAt,
updatedAt: userInfo.dataValues.updatedAt
}
const accessToken = jwt.sign(userData, process.env.ACCESS_SECRET, { expiresIn: '30s' });
const refreshToken = jwt.sign(userData, process.env.REFRESH_SECRET, { expiresIn: '2h' });
return res
.status(200)
.cookie("refreshToken", refreshToken, {
domain: 'localhost',
path: '/',
sameSite: 'none',
httpOnly: true,
secure: true,
})
.send({
"data" : {
"accessToken" : accessToken
},
"message" : "ok"
})
}
};
두번째 작성한 코드(accessToken과 refreshToken을 만드는 함수를 생성했다.)
module.exports = async (req, res) => {
// TODO: urclass의 가이드를 참고하여 POST /login 구현에 필요한 로직을 작성하세요.
const userInfo = await Users.findOne({
where: { userId: req.body.userId, password: req.body.password }
});
// console.log(userInfo);
if (!userInfo) {
res.status(400).send({ data: null, message: "not authorized" })
}
const token = (secret, expiresIn) => {
return jwt.sign({
id: userInfo.id,
userId: userInfo.dataValues.userId,
email: userInfo.dataValues.email,
createdAt: userInfo.dataValues.createdAt,
updatedAt: userInfo.dataValues.updatedAt
}, secret, { expiresIn });
//두번째 인자가 salt 비밀키
//세번째 인자는 옵션 중에 만료시간,
};
const accessToken = await token(process.env.ACCESS_SECRET, '5s');
const refreshToken = await token(process.env.REFRESH_SECRET, '16h');
const response = { accessToken: accessToken };
res.status(200).cookie('refreshToken', refreshToken, {
domain: 'localhost',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'None'
}).json({ data: response, message: 'ok' })
res.status(200).send()
};
https://github.com/auth0/node-jsonwebtoken
5. .controllers/accesstokenrequest.js (GET /accesstokenrequest)
6.controllers/refreshtokenrequest (GET /refreshtokenrequest)
-우리가 login에서 refreshToken을 어디다 저장했는지 잘봐야한다.
콘솔을 거기부터 찍어보면서 문제를 풀어나가면된다.
Client
login페이지는 axios.post로 아이디와 비밀번호 보내서 accessToken을 받아오고 로그인 함수와 엑세스토큰 상태를 변경해주는 함수 두개에 인자를 넣어 실행시키면 된다.
mypage 의 경우, 로그인으로 받아온 accessToken을 밑에 코드처럼 요청을 보내서 정보를 받아와
userId, email, createdAt 의 상태를 변경해주면 된다.
함수형이 아니라 클래스형이라 좀 헷갈리긴 하지만 결국 잘마무리했다.
//...생략
axios.get("https://localhost:4000/accesstokenrequest", {
"headers": {
"Authorization" : `Bearer ${this.props.accessToken}`
}
})
//생략...
이렇게 잘 마무리하면!!!!
이렇게 되는 것을 볼 수 있다. 스프린트 끝