์ธ์ ์ผ๋ก ๋ฐ๋ก ๋ค์ด๊ฐ๋ ค๊ณ ํ์ผ๋ Cookie๋ฅผ ์ค๋ช ํ์ง ์๊ณ ์ฐ๊ธฐ๊ฐ ์ ๋งคํ๋ค. ์ด์ TIL ํฌ์คํ ์์ ์กฐ๊ธ ์์ธํ๊ฒ ๋ค๋ค์ผ๋ ํธ์ํ ํฌ์คํ ์ด ๋ ๊ฒ ๊ฐ๋ค.
์ฟ ํค(cookie)๋ HTTP์ ์ผ์ข ์ผ๋ก ์ธํฐ๋ท ์ฌ์ฉ์๊ฐ ์ด๋ ํ ์น์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ ๊ฒฝ์ฐ ๊ทธ ์ฌ์ดํธ๊ฐ ์ฌ์ฉํ๊ณ ์๋ ์๋ฒ๋ฅผ ํตํด ์ธํฐ๋ท ์ฌ์ฉ์์ ์ปดํจํฐ์ ์ค์น๋๋ ์์ ๊ธฐ๋ก ์ ๋ณด ํ์ผ์ ์ผ์ปซ๋๋ค.
์ฟ ํค๋ ์์ ์กฐ๊ฐ์ ๋ฐ์ดํฐ๋ก์, ์น ๋ธ๋ผ์ฐ์ ์ ์ํด ์ ๋ณ๋์ด ์ฒ์ ์ก์ ๋๋ฉฐ ์น ๋ธ๋ผ์ฐ์ ์ ์ํด ํด๋ผ์ด์ธํธ ์ปดํจํฐ์ ์ ์ฅ๋๋ค. ์ดํ ๋ธ๋ผ์ฐ์ ๋ ์ํ(์ด์ ์ด๋ฒคํธ ๊ธฐ์ต)๋ฅผ ๋ฌด์ํ HTTP ํธ๋์ญ์ ์ผ๋ก ์ ์ ์ํค๋ฉด์ ๋ชจ๋ ์์ฒญ์ ์๋ฒ๋ก ๋๋๋ ค ๋ณด๋ธ๋ค. ์ฟ ํค๊ฐ ์์ผ๋ฉด ์น ํ์ด์ง์ ๊ฐ๊ฐ์ ๊ฒ์ ๋๋ ์น ํ์ด์ง์ ๊ตฌ์ฑ ์์๊ฐ, ๋์ฒด์ ์ผ๋ก ์น์ฌ์ดํธ์์์ ์ฌ์ฉ์๊ฐ ๋ง๋๋ ๋ค๋ฅธ ๋ชจ๋ ํ์ด์ง์ ๋ฌด๊ดํ ๋ณ๊ฐ์ ์ด๋ฒคํธ๋ก ์ทจ๊ธ๋๋ค.
HTTP๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฌด์ํ์ฑ์ด๋ค. ๋ชจ๋ ์์ฒญ์ ์ด์ ์ ์์ฒญ๊ณผ ๋ฌด๊ดํ๋ค๋ ์ ์ ๋ฅผ ๊ฐ์ง๊ณ ์๋ค. ์ด๋ฅผ ๋ณด์ํ๊ธฐ ์ํด์ ์ฌ์ดํธ๋ ์น ๋ธ๋ผ์ฐ์ ์ ์์ ํ ์คํธ ํ์ผ์ ๊ฑด๋ด์ฃผ๊ณ ์ด๋ฅผ ํตํ์ฌ ์ํ์ฑ์ ์ป์ ์ ์๋ค.
์น ๋ธ๋ผ์ฐ์ ์ ์ ์ฉ๋๋ ๋ด์ฉ์ด๋ผ ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ํตํ์ง ์๋๋ค๋ฉด(์๋๋ก์ด๋, IOS) ๊ธฐ๋ณธ์ผ๋ก ๊ฐ์ง๊ณ ์์ง ์๋ค.
์ฟ ํค๊ฐ ์ผ๋ฐ์ ์ผ๋ก ์น ์๋ฒ์ ์ํด ์ค์ ๋์ง๋ง ์๋ฐ์คํฌ๋ฆฝํธ์ ๊ฐ์ ์คํฌ๋ฆฝํธ ์ธ์ด๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์ ์ํด ์ค์ ์ด ๊ฐ๋ฅํ๋ค.(์คํฌ๋ฆฝํธ ์ธ์ด์ ์ํด ์ฟ ํค๋ฅผ ์์ ํ์ง ๋ชปํ๊ฒ ํ๋ ์ฟ ํค์ HttpOnly ํ๋๊ทธ๊ฐ ์ค์ ๋์ด ์์ง ์๋ ๊ฒฝ์ฐ์ ํํด)
Cookie๋ ์ค์ ์ ๋ฐ๋ผ ์๋ฐ์คํฌ๋ฆฝํธ <scirpt>
๋ก ์ถ์ถํ ์ ์๋ค. ๋ฐ๋ผ์ Cookie๋ก ์ค์ํ ์ ๋ณด๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๋๋ ๋ณด์์ ๊ฐํํด์ผ ํ๋ค.
์ฟ ํค๋ ์ํํธ์จ์ด๊ฐ ์๋๋ค. ์ฟ ํค๋ ์ปดํจํฐ ๋ด์์ ํ๋ก๊ทธ๋จ์ฒ๋ผ ์คํ๋ ์ ์์ผ๋ฉฐ ๋ฐ์ด๋ฌ์ค๋ฅผ ์ฎ๊ธธ ์๋, ์ ์ฑ์ฝ๋๋ฅผ ์ค์นํ ์๋ ์๋ค. ํ์ง๋ง ์คํ์ด์จ์ด๋ฅผ ํตํด ์ ์ ์ ๋ธ๋ผ์ฐ์ง ํ๋์ ์ถ์ ํ๋๋ฐ์ ์ฌ์ฉ๋ ์ ์๊ณ , ๋๊ตฐ๊ฐ์ ์ฟ ํค๋ฅผ ํ์ณ์ ํด๋น ์ฌ์ฉ์์ ์น ๊ณ์ ์ ๊ทผ๊ถํ์ ํ๋ํ ์๋ ์๋ค.
์ค์ํ๊ฒ๋ Session ID๋ ์ฟ ํค๋ฅผ ํตํด ์ ๋ฌ๋๋ฉฐ, ์์ํ๊ฒ๋ ์ฌ์ดํธ๋ฅผ ์ด์ฉํ ์ฌ์ฉ์์ ๊ธฐ๋ก์ ์ถ์ ํ ์ ์๋ค. ์ด๋ ๊ฒ ์ค์ํ ๋ฐ์ดํฐ๋ฅผ ์ฟ ํค๋ก ๋ณด๋ผ ๋๋ ๋ณด์(HTTPS)์ด ์ ์ฉ๋์ด์ผ ํ๋ค.
Frontend์ Backend ๋๋ฉ์ธ ์ฃผ์๊ฐ ๋ค๋ฅธ ๊ฒฝ์ฐ Cookie ์ ๋ฌ์ด ๋์ง ์๋๋ค. ํนํ Network ํญ์์ Response Header์ Set-Cookie๋ ์๋๋ฐ Application ํญ์์ Cookie๊ฐ ๋ณด์ด์ง ์๋๋ค. ํค๋์ ์ต์ ์กฐ์ ์ผ๋ก ์ด๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
ํค๋์ withCredentials: true
์ต์
์ผ๋ก ๋ณด์ ํต์ ์ค ๋ค๋ฅธ ๋๋ฉ์ธ(Cross) ์ฌ์ด์ ์ฟ ํค ๊ธฐ๋ฅ์ ํ์ฑํํ๋ค. ํ์ฌ ์ค์ต ํ๊ฒฝ์ ํด๋ผ์ด์ธํธ์ ์๋ฒ์ ๋๋ฉ์ธ์ด ๋ค๋ฅธ ์ํฉ์ด๋ค.
XMLHttpRequest.withCredentials ์์ฑ์ ์ฟ ํค, ๊ถํ ๋ถ์ฌ ํค๋ ๋๋ TLS ํด๋ผ์ด์ธํธ ์ธ์ฆ์์ ๊ฐ์ ์๊ฒฉ ์ฆ๋ช ์ ์ฌ์ฉํ์ฌ ์ฌ์ดํธ ๊ฐ ์ก์ธ์ค ์ ์ด ์์ฒญ์ ๋ง๋ค์ด์ผ ํ๋์ง ์ฌ๋ถ๋ฅผ ๋ํ๋ด๋ ๋ถ์ธ ๊ฐ์ ๋๋ค.
withCredentials ์ค์ ์ ๋์ผ ์ฌ์ดํธ ์์ฒญ์ ์ํฅ์ ๋ฏธ์น์ง ์์ต๋๋ค.
withCredentials๊ฐ true๋ก ์ค์ ๋์ด ์์ง ์์ผ๋ฉด ์์ ์ ๋๋ฉ์ธ์ ๋ํ ์ฟ ํค ๊ฐ์ ์ค์ ํ ์ ์์ต๋๋ค. withCredentials๋ฅผ true๋ก ์ค์ ํ์ฌ ์ป์ ํ์ฌ ์ฟ ํค๋ ์ฌ์ ํ ๋์ผ ์ถ์ฒ ์ ์ฑ ์ ๋ฐ๋ฅด๋ฏ๋ก document.cookie ๋๋ ์๋ต ํค๋๋ฅผ ํตํด ์์ฒญํ๋ ์คํฌ๋ฆฝํธ์์ ์ก์ธ์คํ ์ ์์ต๋๋ค.
XMLHttpRequest.withCredentials - mozilla
axios
.post(
'https://localhost:4000/users/login',
{
userId: this.state.username,
password: this.state.password,
},
{ 'Content-Type': 'application/json', withCredentials: true } // <------
)
์ด ์ต์ ์ ๋๋ฉด ์ฒซ๋ฒ์งธ ๋ก๊ทธ์ธ ์์ฒญ์์ ์๋์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ์์ฃผ ๋ณด๋ ์๋ฌ๋ค. CORS ์ ์ฑ ์ ์๋ฐ๋๋ค๋ ๋ด์ฉ์ด๋ฉฐ 'XMLHttpRequest์ ์ํด ์์๋ ์์ฒญ์ ์๊ฒฉ ์ฆ๋ช ๋ชจ๋๋ withCredentials ์์ฑ์ ์ํด ์ ์ด๋๋ค'๊ณ ํ๋ค.
Access to XMLHttpRequest at 'https://localhost:4000/users/login' from origin 'https://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
์๋๋ ๊ณต์ ๋ฌธ์์์ ์ธ๊ธํ๋ Access-Control-Allow-Credentials ์ต์ ์ ๋ํ ์ค๋ช ์ด๋ค. ์์ฝํ๋ฉด ๋ค๋ฅธ ๋๋ฉ์ธ๊ฐ ํค๋๋ฅผ ํฌํจํ ์ ์๊ฒ ํ๋ค.
์๋ตํค๋ Access-Control-Allow-Credentials ๋ ์์ฒญ์ ์๊ฒฉ์ฆ๋ช ๋ชจ๋(Request.credentials)๊ฐ "include" ์ผ๋, ๋ธ๋ผ์ฐ์ ๋ค์ด ์๋ต์ ํ๋กํธ์๋ ์๋ฐ์คํธ๋ฆฝํธ ์ฝ๋์ ๋ ธ์ถํ ์ง์ ๋ํด ์๋ ค์ค๋๋ค.
์์ฒญ์ ์๊ฒฉ์ฆ๋ช ๋ชจ๋๊ฐ (Request.credentials)๊ฐ "include" ์ผ ๋, Access-Control-Allow-Credentials ๊ฐ์ด true ์ผ ๊ฒฝ์ฐ์๋ง ๋ธ๋ผ์ฐ์ ๋ค์ ํ๋กํธ์๋ ์๋ฐ์คํธ๋ฆฝํธ์ ์๋ต์ ๋ ธ์ถ ํ ๊ฒ์ ๋๋ค.
Access-Control-Allow-Credentials - mozilla
credentials: Configures the Access-Control-Allow-Credentials CORS header. Set to true to pass the header, otherwise it is omitted.
cors - express
origin์ *
๋ก ์ ์ผ๋ฉด ์๋๋ค. ์ ํํ๊ฒ ์ ์ด์ผ ์ฟ ํค๊ฐ ๋ค์ด๊ฐ๋ค. express์์ true๋ ๋ฐ์์ค๋ค.
๋ก๊ทธ์ธ ์์ฒญ์ด ์ฒด์ธ์ผ๋ก ์ฐ๊ฒฐ๋์ด์ 2๋ฒ ์ด๋ฃจ์ด์ง๋๋ฐ ๊ทธ ๊ณผ์ ์์ ์์ ํค๋๊ฐ ๋ง๋ค์ด์ ธ CORS ์ ์ฑ
์ ์๋ฐํ๋ค. ๊ฐ๋ฐ์ ๋๊ตฌ์์ ๋ณด๋ฉด ์ฒซ Login Post ์์ฒญ์ ๋์ด๊ฐ์ง๋ง ์ด์ด์ง๋ userinfo Get ์์ฒญ์ CORS ์์ ๋งํ๋ค.
๊ทธ ์ด์ ๋ origin ์ต์
์ด *
์ผ ๋๋ 'Access-Control-Allow-Origin' ์ต์
์ด ์ง์๋์ง ์๋๋ค.
Reason: Credential is not supported if the CORS header โAccess-Control-Allow-Originโ is *
app.use(
cors({
origin: 'https://localhost:3000', // <-----
methods: ['GET', 'POST', 'OPTIONS'],
credentials: true, // <------
})
);
๋ฐฐ์ด ์์๋ก ๋ค์ด๊ฐ๋ฉฐ ['ํค=๊ฐ', 'ํค=๊ฐ']
ํํ๋ก ์ ์ฅ๋๋ค.
res.writeHead ๋ฉ์๋๋ก ์ฟ ํค๋ฅผ ์ ์ด์ค ์ ์๋ค.
res.writeHead(200, {
'Set-Cookie': [
`my-cookie=programmer`,
],
'Content-Type': 'text/html'
});
res.setHeader ๋ฉ์๋๋ก ๋ฌ์์ค ์ ์๋ค. ์ต์
์ ๋ฌ์์ฃผ๊ณ ์ถ์ผ๋ฉด ํด๋น ์ฟ ํค ๋ฐ์ดํ ์์์ ๋ค์ ์ธ๋ฏธ์ฝ๋ก ;
์ ์ฐ๊ณ ์ฟ ํค๋ณ๋ก ๊ฐ๊ฐ ์ต์
์ ์
๋ ฅํ๋ค.
res.setHeader('Set-Cookie',
[`my-cookie=programmer; HttpOnly`,
`more-data=imNotSecure`]
);
Token ์น์
์์ ๋ฑ์ฅํ๋ค. cookie-parser ๋ชจ๋์ ํ์ฉํ๋ค.
ํด๋น ์น์
์์ tokenFunction
๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ๊ธฐ๋ฅ๋ค์ ๊ตฌํํด๋๊ณ ์ฐ๊ธฐ๋ง ํด์ ์๋ฟ์ง๊ฐ ์์๋ค.
const {
generateAccessToken,
generateRefreshToken,
sendRefreshToken,
sendAccessToken,
} = require('../tokenFunctions');
...
.then((data) => {
if (!data) {
// return res.status(401).send({ data: null, message: 'not authorized' });
return res.json({ data: null, message: 'not authorized' });
}
delete data.dataValues.password;
// ์ฟ ํค์ ์ฐ๋ ๋ถ๋ถ์ ๋ณ๋์ ํจ์๋ก ๋นผ๋จ๋ค.
const accessToken = generateAccessToken(data.dataValues);
const refreshToken = generateRefreshToken(data.dataValues);
sendRefreshToken(res, refreshToken);
sendAccessToken(res, accessToken);
})
.catch((err) => {
console.log(err);
});
์ค์ ๋ก tokenFunctions
ํ์ผ์ ์ด์ด๋ณด๋ฉด ์ฟ ํค์ ์ฐ๋ ๋์์ ํ์ธํ ์ ์๋ค. RefreshToken
์ ์ฟ ํค์ ํ์ฌ๋๋ค.
sendRefreshToken: (res, refreshToken) => {
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
});
},
sendAccessToken: (res, accessToken) => {
res.json({ data: { accessToken }, message: "ok" });
},
์๋ฒ์์ ์ฟ ํค๋ฅผ ์ฝ์ ๋๋ req.cookies
๊ฐ์ฒด๋ก ์ ๊ทผํ ์ ์๋ค.
const refreshToken = req.cookies.refreshToken;
express-session ๋ชจ๋์ ํตํด ์ธ์
์ ๋ง๋๋ ๊ณผ์ ์์ ์๋์ผ๋ก ์ฟ ํค๋ฅผ ์์ฑํ๋ค. connect.sid
๊ฐ์ ํตํด ์ธ์
ID๋ฅผ ์ ๋ฌํ๋ค. ์๋ฒ์์ ์ฟ ํค๋ฅผ ๋ฌ์์ฃผ๋ฉด ์น ๋ธ๋ผ์ฐ์ ๋ ์ด ์ดํ๋ถํฐ ์ธ์
ID๊ฐ ๋ด๊ธด connect.sid
๋ผ๋ ์ฟ ํค๋ฅผ ๋ฌ๊ณ ๋ค๋๊ฒ ๋๋ค. ์ด ์ด๋ฆ์ ๊ธฐ๋ณธ๊ฐ์ด๋ฉฐ ๋ฐ๊ฟ ์ ์๋ค.
// TODO: express-session ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด ์ฟ ํค ์ค์ ์ ํด์ค ์ ์์ต๋๋ค.
app.use(
session({
secret: '@codestates', // ์ํธํ๋ก ์ฐ์ด๋ ํค
resave: false, // ์ธ์
์ ํญ์ ์ ์ฅํ ์ง ์ค์
saveUninitialized: true, // ์ธ์
์ด ์ ์ฅ๋๊ธฐ ์ uninitialized ์ํ๋ก ์ ์ฅ
cookie: { // ์ธ์
์ฟ ํค ์ค์
domain: 'localhost', // ์ฟ ํค๋ฅผ ๋ณด๋ด๋ ์ฌ์ดํธ(์๋ฒ)๋ฅผ ๋ช
์
path: '/', // URL ๊ฒฝ๋ก. ํ์ ๊ฒฝ๋ก๊น์ง ๋ชจ๋ ํฌํจ.
maxAge: 24 * 6 * 60 * 10000, // ์ฟ ํค ๋ง๋ฃ ์๊ฐ. ์ด๋จ์.
// XSRF(cross-site request forgery) ๊ณต๊ฒฉ ๋ฐฉ์ด ์ต์
sameSite: 'None', // ์ฌ์ดํธ ์ธ๋ถ์์ ์์ฒญ์ ๋ณด๋ผ ๋ ์ฟ ํค ์ ์ฑ
// Strict : ์ธ๋ถ ๋๋ฉ์ธ ์ฟ ํค ์ฐจ๋จ
// Lax : ์ธ๋ถ ๋๋ฉ์ธ ์ฟ ํค ์ผ๋ถ ํ์ฉ(HTTP get method / a href / link href)
// None : ์ธ๋ถ ๋๋ฉ์ธ ์ฟ ํค ๋ชจ๋ ํ์ฉ. ๊ฐ์ ๋ก origin ์ต์
๊ณผ ๊ฐ์ด ์จ์ผํจ
httpOnly: true, // ํด๋ผ์ด์ธํธ ์คํฌ๋ฆฝํธ๊ฐ ์ฟ ํค๋ฅผ ๋ชป๋ณด๊ฒ ํจ (document.cookie)
secure: true, // HTTPS ํ๊ฒฝ๋ง ์ฟ ํค๋ฅผ ์ ์ก
},
})
);
// CORS ์ฟ ํค ์ค์ ํ์ฑํ
app.use(
cors({
// origin: 'https://localhost:3000',
origin: true,
methods: ['GET', 'POST', 'OPTIONS'],
credentials: true, // ๋ค๋ฅธ ๋๋ฉ์ธ๊ฐ ์๊ฒฉ์ฆ๋ช
(credential, ์ฟ ํค ํฌํจ)์ ์ ์กํ ์ง ์ฌ๋ถ
})
);
ํด๋ผ์ด์ธํธ๋ ์๋ฒ์ ํต์ ํ ๋ connect.sid
์ฟ ํค์ ๋ํด์ ์๋ฌด๋ฐ ์ธ๊ธ์ด ์๋ค. ์ฟ ํค๋ ๋ธ๋ผ์ฐ์ ์ ์ ์ฅ๋๋ฉฐ ํด๋น ์ฌ์ดํธ์ ์ ์ํ ๋ ์๋์ผ๋ก ๋ฌ๊ณ ๋ค๋๊ธฐ ๋๋ฌธ์ด๋ค.
withCredentials
์ต์
์ Origin์ด ๋ค๋ฅธ ํต์ ์์ ์ฟ ํค ์กฐํ๋ฅผ ํ์ฉํ๋ ์ต์
์ด๋ค.
axios
.post('https://localhost:4000/users/logout', null, {
'Content-Type': 'application/json',
withCredentials: true,
})
.then(() => props.logoutHandler())
.catch((e) => alert(e));
ํด๋ผ์ด์ธํธ์์ ์ธ๊ธ์ด ์์ด๋ ์๋ฒ์์ req.session
์ด๋ผ๋ ๊ฐ์ฒด๋ก ์ ๊ทผํ ์ ์๋ค.
const result = await Users.findOne({
where: { userId: req.session.userId },
})