이 기사에서는 데이터, 사용자 인증 및 JWT 처리를 위해 초고속 스토리지/캐시 시스템으로 사용할 Next.js API 경로 및 Upstash Redis를 활용하여 최소한이지만 완벽하게 기능하는 인증된 Rest API 서비스를 구축할 것입니다. 이 프로젝트에는 프런트엔드가 없으며 다양한 클라이언트에서 쿼리할 수 있는 API만 노출된다는 점에 유의하시기 바랍니다.
전제조건
튜토리얼을 따라가려면 다음이 필요합니다:
- Upstash 계정 — 여기에서 무료 계정에 가입하세요
- Redis 기본 지식
- Next.js API 경로에 대한 기본 지식
- 인증 및 권한 부여 워크플로에 대한 기본 지식
- HTTP 요청을 만들기 위해 선택한 도구
Upstash Redis란 무엇인가요
Upstash는 서버리스 인메모리 클라우드 데이터베이스입니다. Redis 기반. 이를 사용하여 API에서 제공할 데이터를 저장하고 사용자 기반과 사용자 토큰을 Upstash Redis에 저장할 것입니다.
우리가 구축할 것
클라이언트 애플리케이션이 데이터(이 특정 경우에는 영화 목록)를 요청할 수 있도록 하는 REST API 서비스를 코딩하겠습니다. JWT를 사용하여 엔드포인트를 보호하고, 토큰을 얻기 위해 API 로그인 서비스를 코딩하고, 새로 고침 토큰 워크플로도 구현합니다.
우리는 집중하지 않을 것입니다 (우리는 '의견이 없는' 서비스를 구축하고 있기 때문에) 클라이언트 개발에 관한 것이지만 누구나 클라이언트를 구축할 수 있도록 서비스 사양을 제공할 것입니다.
저장소 및 데모
따라가려면 프로젝트 저장소를 복제할 수도 있습니다
GitHub의 소스
다음 URL에서 데모를 시도해 볼 수도 있습니다:
https://upstash-dwov9jbiq-popland.vercel.app/api/auth/signin
서비스에 연결하려면 사용자 이름(me@home.org)을 전달하는 POST HTTP 요청을 수행하세요. ) 및 비밀번호(비밀번호 ) 다음 예와 같습니다(Postman 사용):
Redis 데이터베이스 설정
우선, Upstash Redis에 가입해야 합니다(무료 플랜은 테스트 목적으로 작동합니다). 콘솔에 로그인한 후에는 새 데이터베이스를 생성할 수 있습니다:
계속해서 "데이터베이스 생성"을 클릭하고 이름을 MovieManager로 지정하고 Global로 설정합니다. 이제 Upstash CLI를 사용하여 더미 데이터를 추가합니다
HMSET 명령을 사용하여 일부 영화를 Redis 해시(기본적으로 객체임)로 추가합니다:
hmset movie:’Dr. Strangelove’ director ‘Stanley Kubrick’ year 1964
hmset movie:’2001: A Space Odyssey’ director ‘Stanley Kubrick’ year 1968
hmset movie:’Pulp Fiction’ director ‘Quentin Tarantino’ year 1994
hmset movie:’Django Unchained’ director ‘Quentin Tarantino’ year 2012
또한 데이터에 액세스할 수 있는 권한이 부여된 사용자를 추가할 예정이며, 이 사용자는 Redis 해시가 됩니다:
hmset user:’me@home.org’ password $2b$10$zctxUVDyy3jzvSp68oKpMOnkyra4R.NzOFVh9aii3Y43X7XtetoyK level 0
참고 :비밀번호는 bcrypt로 암호화되어 있습니다. (간단히 말하면 비밀번호입니다. ), 일반적으로 웹사이트를 통해 API 레지스터에 액세스해야 하는(또는 웹사이트에서 자격 증명을 가져오는) 사용자입니다. 이 예에서는 레지스터 엔드포인트를 제공하지 않습니다.
Upstash CLI에 입력된 모든 명령은 모든 것이 정확하고 데이터 브라우저로 이동하면 OK 응답을 제공해야 합니다. 해시를 선택하면 삽입한 데이터 목록이 표시됩니다.
승인 작업 흐름
앞서 언급했듯이 엔드포인트는 공개적으로 액세스할 수 없으므로 사용자를 인증하고 권한을 부여하는 방법이 필요합니다. 인증을 위해 로그인 엔드포인트를 제공합니다. 인증을 위해 보호된 엔드포인트에는 요청과 함께 전송되는 인증 헤더가 필요합니다. 작업흐름의 세부적인 작동 방식은 다음과 같습니다:
- 사용자가 로그인 엔드포인트를 요청하고 사용자 이름과 비밀번호를 게시합니다.
- 서버는 사용자 인증을 시도합니다. 사용자가 유효한 경우 서버는 JWT(JSON 웹 토큰)와 새로 고침 토큰을 생성하여 다시 보냅니다. 새로 고침 토큰은 Upstash Redis 인스턴스에도 저장됩니다.
- 클라이언트는 토큰을 다시 가져와 어딘가에 저장합니다(토큰을 저장하는 방법/위치는 클라이언트의 책임입니다)
- 클라이언트는 헤더에 JWT를 전송하여 보호된 엔드포인트를 요청합니다.
- 서버는 JWT를 수신하고 이를 확인하며, 확인되면 클라이언트가 요청한 데이터를 다시 보냅니다.
- JWT가 만료되거나 만료가 가까워지면 클라이언트는 새로 고침 토큰을 특정 엔드포인트로 전송하여 다시 로그인하지 않고도 새 JWT를 요청할 수 있습니다.
- 서버는 새로 고침 토큰을 수신하고 이를 확인하며, 확인 결과가 긍정적인 경우 새 JWT 및 새로 고침 토큰을 발급하고 이를 클라이언트에 다시 보내고 새 새로 고침 토큰을 다시 저장합니다.
JWT와 새로 고침 토큰은 동일한 형식이고 거의 동일한 정보를 갖지만 두 개의 다른 키를 사용합니다(.env에서 설정함). 파일) 및 두 가지 다른 만료를 얻었습니다. JWT에 대한 짧은 만료(세션 중에 가장 많이 사용되는 토큰이므로 가로채는 경우 곧 만료되도록 설정)와 새로 고침 토큰에 대한 긴 만료입니다. 두 기간 모두 보안을 유지해야 하는 정도에 따라 다릅니다. 일반적으로 JWT는 1시간 이내에 만료되며 새로 고침 토큰은 한 달 동안 지속될 수 있습니다. 두 토큰이 모두 만료되면 사용자는 다시 로그인해야 합니다.
프로젝트 설정
Upstash Redis 데이터베이스 작업이 완료되면 프로젝트 초기화를 시작할 수 있습니다. 먼저 새로운 Next.js 프로젝트를 만듭니다:
npx create-next-app upstash-jwt
그런 다음 새로 생성된 폴더 upstash-jwt를 입력합니다. 필요한 모듈을 설치합니다:
npm i bcrypt jsonwebtoken @upstash/redis
.env.local 만들기 키를 저장하고 올바른 데이터를 입력할 파일
SECRET_TOKEN=
SECRET_RTOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= JWT를 생성하는 데 사용할 SECRET_TOKEN 및 SECRET_RTOKEN을 생성하세요. 이러한 키는 비밀로 유지되어야 하며 매우 무작위이거나 추측하기 어려워야 한다는 점을 기억하세요. 64비트의 16진수 문자열을 사용할 수 있습니다. Upstash 콘솔, 세부 정보 탭, Rest API 섹션에서 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 가져옵니다.
이제 엔드포인트 배치를 시작할 수 있습니다:
POST /auth/signin 사용자를 로그인하려면 JSON 개체 {"email":"email", "password": "password"}로 전달된 이메일과 비밀번호가 필요합니다. , 사용자 정보, JWT 및 새로 고침 토큰이 포함된 JSON 개체를 반환합니다.
/영화/ 받기 영화 목록을 JSON 객체로 반환하려면 Authorization:Bearer xxx 형식으로 헤더에 전달되는 유효한 JWT가 필요합니다.
/movies/$ID 가져오기 ID가 $ID인 영화의 세부정보를 반환합니다.
POST /auth/새로고침 새 JWT를 생성하고 반환합니다. 새로 고침 토큰은 refreshToken로 전달되어야 합니다. 매개변수입니다.
API 경로 코드
로그인 엔드포인트부터 시작하여 pages/api/auth/signin.js 파일을 생성해 보겠습니다. 다음과 같습니다:
import bcrypt from "bcrypt";
import {
addToList,
generateAccessToken,
generateRefreshToken,
redis,
} from "../../../utils";
export default async (req, res) => {
if (req.method === "GET") {
res.status(405).send("Not Allowed");
} else {
console.log(req.body.user);
try {
const user = await redis.hgetall(`user:${req.body.user}`);
if (user) {
const validPassword = bcrypt.compare(req.body.password, user.password);
if (validPassword) {
const token = generateAccessToken(req.body.user, user.level);
const refreshToken = generateRefreshToken(req.body.user, user.level);
const refresh = await addToList(req.body.user, refreshToken);
const content = {
user: req.body.user,
level: user.level,
};
res.status(200).json({
message: "Logged in",
content: content,
JWT: token,
refresh: refreshToken,
});
} else {
res.status(400).json({ error: "Invalid Password" });
}
} else {
res.status(401).json({ error: "User not found" });
}
} catch (error) {
res.status(500).send("Internal Server Error");
}
}
}; 우리의 로그인 엔드포인트는 user라는 두 개의 매개변수가 있는 POST만 허용합니다. 및 비밀번호 . 먼저 다음을 사용하여 사용자가 Redis 데이터베이스에 있는지 확인합니다.
const user = await redis.hgetall(`user:${req.body.user}`);
사용자가 있으면 암호화된 비밀번호를 비교합니다:
const validPassword = bcrypt.compare(req.body.password, user.password);
이 시점에서 비밀번호가 일치하면 사용자가 인증되었다고 가정하고 JWT와 새로 고침 토큰을 다시 보낼 수 있으며 Redis 인스턴스에 새로 고침 토큰도 저장합니다. 이를 위해 utils.js라는 외부 파일에 있는 일부 기능을 사용합니다.
반환된 토큰을 저장하고, 필요할 때 인증에 사용하고, 만료되면 새로 고치는 것은 클라이언트의 책임입니다.
토큰 generateAccessToken을 생성하는 기능이 있습니다. , 새로 고침 토큰 generateRefreshToken을 생성하기 위한 것 , 하나는 Redis addToList에 새로 고침 토큰을 저장하는 것입니다. . 이 utils.js 이 파일은 다른 모든 유틸리티 기능과 참조(예:Redis 연결, 토큰 확인 및 새로 고침 등)를 유지하는 데에도 사용됩니다.
import { Redis } from "@upstash/redis";
import jwt from "jsonwebtoken";
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
export function generateAccessToken(username, email, level) {
return jwt.sign(
{ user: username, email: email, level: level },
process.env.SECRET_TOKEN,
{
expiresIn: "1h",
},
);
}
export function generateRefreshToken(username, email, level) {
return jwt.sign(
{ user: username, email: email, level: level },
process.env.SECRET_RTOKEN,
{
expiresIn: "30d",
},
);
}
export async function addToList(user, refresher) {
try {
await redis.hset("refresh:" + user, { refresh: refresher });
} catch (error) {
console.log(error);
}
}
export async function tokenRefresh(refreshtoken, res) {
var decoded = "";
try {
decoded = jwt.verify(refreshtoken, process.env.SECRET_RTOKEN);
} catch (error) {
return res.status(401).send("Can't refresh. Invalid Token");
}
if (decoded) {
try {
const rtoken = await redis.hget("refresh:" + decoded.user, "refresh");
console.log(rtoken);
if (rtoken !== refreshtoken) {
return res.status(401).send("Can't refresh. Invalid Token");
} else {
const user = await redis.hgetall(`user:${decoded.user}`);
console.log(user);
const token = generateAccessToken(decoded.user, user.level);
const refreshToken = generateRefreshToken(decoded.user, user.level);
const refresh = await addToList(decoded.user, refreshToken);
const content = {
user: decoded.user,
level: user.level,
};
return {
message: "Token Refreshed",
content: content,
JWT: token,
refresh: refreshToken,
};
}
} catch (error) {
console.log(error);
}
}
}
export async function verifyToken(token, res) {
try {
const decoded = jwt.verify(token, process.env.SECRET_TOKEN);
return decoded;
} catch (err) {
return res.status(405).send("Token is invalid");
}
}
이제 (http://localhost:3000/api/auth/signin에 게시하여 Postman과 같은 도구를 사용하여 서명 프로세스를 테스트할 수 있습니다. 사용자 이름(me@home.org)을 전달합니다. ) 및 비밀번호(password ), JWT 및 새로 고침 토큰과 함께 사용자 세부 정보가 포함된 JSON 개체를 가져와야 합니다.
모든 것이 정확했다면 이제 Redis 데이터베이스에 새로 생성된 새로 고침 토큰에 대한 새 해시 항목이 표시됩니다.
다음으로 토큰 새로 고침 경로 refresh.js를 코딩하여 인증 프로세스를 완료합니다.
import { redis, tokenRefresh } from "../../../utils";
export default async (req, res) => {
if (req.method === "GET") {
res.status(405).send("Not Allowed");
} else {
console.log(req.body.refresh);
const refresp = await tokenRefresh(req.body.refresh, res);
res.status(200).json(refresp);
}
};
tokenRefresh를 사용합니다. utils.js의 함수 토큰이 유효하고 디코딩될 수 있는지 확인하는 것으로 시작합니다. 그런 다음 사용자가 새로 고침 토큰(이전에 addToList로 저장한 토큰)을 받았는지 Redis에서 확인합니다. ), 모든 것이 정확하면 새 JWT, 새 새로 고침 토큰을 생성하고(그리고 이를 Redis에 다시 저장하고) 모든 것을 클라이언트로 다시 보냅니다.
우리 도구를 사용하여 http://localhost:3000/api/auth/refresh에 게시하여 이 엔드포인트를 테스트할 수 있습니다. 새로 고침 토큰을 매개변수로 전달합니다:
이제 가상의 클라이언트가 로그인하여 토큰을 새로 고칠 수 있습니다. 토큰을 사용하여 인증된 요청을 보내는 방법을 살펴보겠습니다.
새 API 경로 생성:api/movies/[[...id]].js 이는 영화 목록을 가져오고 영화 세부 정보를 가져오는 데 사용됩니다:
import { redis, verifyToken } from "../../../utils";
export default async (req, res) => {
var id;
console.log(req.query);
if (req.query.id) {
id = req.query.id[0];
}
var decoded = "";
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
return res.status(403).send("A token is required for authentication");
} else {
decoded = await verifyToken(token, res);
}
if (decoded) {
if (id) {
try {
const result = await redis.hgetall(id);
console.log(result);
return res.status(200).json(result);
} catch (error) {
return res.status(500).send("Internal Server Error");
}
} else {
try {
const result = await redis.scan(0, { match: "movie:*" });
return res.status(200).json(result);
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
}
};
verifyToken 사용 utils.js의 기능 유효한 토큰을 제공하는 사용자로만 API 엔드포인트에 대한 액세스를 제한할 수 있습니다. 우리는 몇 가지 샘플 쿼리를 만들었고, 첫 번째로 영화 목록을 가져왔습니다
const result = await redis.scan(0, { match: ‘movie:*’ });
두 번째는 URL 요청의 id 매개변수를 기반으로 단일 영화의 세부정보를 가져오는 것입니다.
const result = await redis.hgetall(id);
두 요청 모두 verifyToken을 통해 확인되는 사용자 상태에 따라 달라집니다. 예를 들어 목록은 공개하고 세부 정보는 보호할 수 있습니다. 사용자(및 토큰)에도 레벨이 저장되어 있으므로 더 많은 인증 레벨을 가질 수 있습니다. 영화 목록을 가져와 보겠습니다.
단일 영화 세부정보:
클라이언트 관점
이전에 말했듯이 우리는 서버 부분에만 집중했습니다. 이는 API의 주요 범위이며 추상적이어야 하며 웹사이트가 아닙니다. 클라이언트가 데이터(프로그래밍 언어, 라이브러리 등)를 요청하는 방식은 클라이언트 개발자가 선택합니다. 우리는 엔드포인트 목록, 엔드포인트가 기대하는 것, 엔드포인트가 클라이언트에 반환하는 내용을 제공합니다. 모든 데이터 처리, 새로 고침 지연 등은 클라이언트 전략입니다.
다음 단계
이는 보호된 API 워크플로의 기본 예일 뿐입니다. 여기에서는 Redis에 데이터가 저장되는 방식을 최적화하고, 사용자 데이터를 다른 Redis 인스턴스에 저장하여 로그인 보안을 개선하고, 사용자가 보내는 데이터를 먼저 확인 및 검증하고, 더 많은 엔드포인트를 추가하고, GraphQL 형식으로 데이터를 반환하고, API용 클라이언트를 구축하고, 시간당 최대 호출 수로 액세스를 제한하고, 수준을 사용하여 액세스를 제한하는 등 개선할 수 있습니다. 확장 및 개선은 끝이 없습니다!