서버리스 앱에 인증 추가하기 - Amazon Cognito 활용
Cognito를 사용하여 서버리스 앱에 인증 추가하기
이전 챕터에서 서버리스 앱에 인증을 추가하는 기본 사항을 살펴봤습니다. 이번 챕터에서는 Amazon Cognito를 사용하여 서버리스 API에 인증을 추가하는 방법을 알아보겠습니다. 또한 React.js 앱에서 AWS Amplify를 사용하여 이 API에 연결하는 방법도 살펴보겠습니다.
이를 더 잘 이해하기 위해 이 가이드를 위해 생성된 GitHub의 예제 SST 애플리케이션을 참조할 것입니다.
https://github.com/sst/sst/tree/master/examples/react-app-auth-cognito
이 예제 SST 앱에는 몇 가지 주요 부분이 있습니다:
stacks/
디렉토리: 서버리스 앱의 인프라를 설명하는 코드가 포함되어 있습니다. AWS CDK를 활용하여 인프라를 생성합니다. 여기에는 API, Cognito 서비스, 프론트엔드 정적 사이트가 포함됩니다.src/
디렉토리: 애플리케이션 코드가 위치한 곳입니다. API가 호출될 때 실행될 코드입니다.frontend/
디렉토리: 프론트엔드 React.js 애플리케이션이 위치한 곳입니다. 여기서 API에 연결됩니다.
또한 환경 구성 정보를 포함하는 sst.json
설정 파일이 있습니다. 파일 내용은 다음과 같습니다:
{
"name": "react-app-auth-cognito",
"stage": "dev",
"region": "us-east-1",
"lint": true
}
위 설정은 앱이 us-east-1
리전의 dev
라는 개발 환경에 배포됨을 의미합니다.
이제 앱에 Cognito User Pool을 추가하는 방법부터 살펴보겠습니다.
Cognito 추가하기
이전 장에서 Cognito의 다양한 부분(User Pools와 Identity Pools)에 대해 이야기했습니다.
SST는 이를 여러분의 애플리케이션에 쉽게 추가할 수 있도록 도와줍니다. stacks/MyStack.js
파일에서 다음과 같은 코드를 확인할 수 있습니다.
// 인증 관리를 위해 Cognito User Pool 생성
const auth = new sst.Cognito(this, "Auth", {
cognito: {
userPool: {
// 사용자는 이메일과 비밀번호로 로그인
signInAliases: { email: true, phone: true },
},
},
});
이 코드는 SST의 Cognito
구성을 사용하여 Cognito User Pool과 Identity Pool을 생성합니다.
별칭
이 경우 사용자가 이메일과 전화번호를 사용자 이름으로 로그인할 수 있도록 허용합니다.
또한 선택적으로 사용자가 사용자 이름을 생성하고 이를 사용하여 로그인할 수 있도록 허용할 수 있습니다.
const auth = new sst.Cognito(this, "Auth", {
cognito: {
userPool: {
signInAliases: {
email: true,
phone: true,
username: true,
preferredUsername: true,
},
},
},
});
소셜 로그인
이 예제에서는 소셜 로그인을 설정하지 않습니다. 이 부분은 다음 장에서 다룰 예정입니다. 하지만 간단히 살펴보면, 다른 소셜 로그인 프로바이더를 추가하는 방법은 대략 다음과 같습니다:
new Cognito(this, "Auth", {
facebook: { appId: "419718329085014" },
apple: { servicesId: "com.myapp.client" },
amazon: { appId: "amzn1.application.24ebe4ee4aef41e5acff038aee2ee65f" },
google: {
clientId:
"38017095028-abcdjaaaidbgt3kfhuoh3n5ts08vodt3.apps.googleusercontent.com",
},
});
Cognito 트리거
인증 전후에 특정 작업을 트리거하고 싶을 수도 있습니다. Cognito 트리거를 사용하면 특정 이벤트에 대해 실행될 Lambda 함수를 정의할 수 있습니다.
new Cognito(this, "Auth", {
cognito: {
triggers: {
preAuthentication: "src/preAuthentication.main",
postAuthentication: "src/postAuthentication.main",
},
},
});
API 추가하기
이제 Cognito를 사용해 API를 보호하는 방법을 살펴보겠습니다. 예제의 stacks/MyStack.js
파일에서 SST Api
정의를 확인할 수 있습니다.
// HTTP API 생성
const api = new Api(stack, "Api", {
// IAM 인증으로 보안 설정
defaultAuthorizationType: sst.ApiAuthorizationType.AWS_IAM,
routes: {
"GET /private": "src/private.handler",
// 공개 엔드포인트 설정
"GET /public": {
function: "src/public.handler",
authorizationType: sst.ApiAuthorizationType.NONE,
},
},
});
// 인증된 사용자가 API를 호출할 수 있도록 허용
auth.attachPermissionsForAuthUsers(stack, [api]);
랜덤 숫자를 생성하는 간단한 API를 만들어 보겠습니다. 이 API는 공개 라우트와 비공개 라우트를 갖습니다. 공개 라우트에서는 누구나 랜덤 숫자를 생성할 수 있지만, 비공개 라우트에서는 로그인한 사용자만 랜덤 숫자를 생성할 수 있습니다.
defaultAuthorizationType: sst.ApiAuthorizationType.AWS_IAM
을 주목하세요. 이 설정은 기본적으로 유효한 AWS_IAM
권한을 가진 사용자만 라우트에 접근할 수 있도록 보장합니다.
또한 공개 라우트에서는 authorizationType
을 NONE
으로 설정해 앞서 설명한 기본 동작을 재정의합니다.
마지막으로, auth.attachPermissionsForAuthUsers(stack, [api])
는 Cognito 사용자 풀에 인증된 사용자가 방금 정의한 API에 접근할 수 있도록 AWS에 알립니다.
Lambda 함수 추가하기
이제 API를 구동할 Lambda 함수를 간단히 살펴보겠습니다. src/
디렉토리 안에는 랜덤 숫자를 생성하는 파일이 몇 개 있습니다.
예를 들어, src/private.js
파일은 다음과 같습니다.
export async function handler() {
const rand = Math.floor(Math.random() * 10);
return {
statusCode: 200,
headers: { "Content-Type": "text/json" },
body: JSON.stringify({ message: `Private Random Number: ${rand}` }),
};
}
React 정적 사이트 추가하기
이제 애플리케이션의 프론트엔드 부분에 집중할 차례입니다. stacks/MyStack.js
파일에서 SST의 ReactStaticSite
정의를 확인해 보세요.
// React 앱 배포
const site = new ReactStaticSite(this, "ReactSite", {
path: "frontend",
// 환경 변수 전달
environment: {
REACT_APP_API_URL: api.url,
REACT_APP_REGION: scope.region,
REACT_APP_USER_POOL_ID: auth.cognitoUserPool.userPoolId,
REACT_APP_IDENTITY_POOL_ID: auth.cognitoCfnIdentityPool.ref,
REACT_APP_USER_POOL_CLIENT_ID: auth.cognitoUserPoolClient.userPoolClientId,
},
});
여기서 중요한 점은 백엔드의 출력을 React의 환경 변수로 설정한다는 것입니다. 구체적으로 다음을 전달합니다:
- API 엔드포인트
- 서버리스 앱의 리전
- Cognito 사용자 풀의 ID
- Cognito Identity Pool의 ID
- Cognito 사용자 풀 클라이언트의 ID
참고로 stacks/MyStack.js
파일의 나머지 부분도 확인할 수 있습니다.
이제 React 앱을 만들 준비가 되었습니다.
React 앱 만들기
이 예제에서는 Create React App을 사용합니다. 유일한 차이점은 SST 앱에서 환경 변수를 로드하기 위해 @serverless-stack/static-site-env
CLI를 사용한다는 점입니다.
이 부분은 frontend/package.json
에서 확인할 수 있습니다.
"scripts": {
"start": "sst-env -- react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
또한 이 예제에서는 Bootstrap, React Bootstrap, 그리고 React Router를 사용하지만, 여기서는 이들에 대해 자세히 다루지 않습니다.
대신, 위에서 정의한 API에 연결하기 위해 AWS Amplify를 어떻게 사용하는지 살펴보겠습니다.
AWS Amplify 설정하기
시작하기 위해 frontend/src/index.js
파일에서 설정을 진행합니다.
// Amplify 초기화
Amplify.configure({
Auth: {
mandatorySignIn: true,
region: process.env.REACT_APP_REGION,
userPoolId: process.env.REACT_APP_USER_POOL_ID,
identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID,
userPoolWebClientId: process.env.REACT_APP_USER_POOL_CLIENT_ID,
},
API: {
endpoints: [
{
name: "random-api",
region: process.env.REACT_APP_REGION,
endpoint: process.env.REACT_APP_API_URL,
},
],
},
});
위에서 설정한 환경 변수를 사용하고 있음을 확인할 수 있습니다.
API 로딩
우리의 간단한 React 앱은 이전에 생성한 두 개의 API 라우트를 로드할 것입니다. 앱의 홈페이지를 렌더링하는 컴포넌트가 있습니다.
frontend/src/components/Home.js
에서 랜덤 숫자 생성 API를 로드하는 것을 확인할 수 있습니다.
import React, { useState, useEffect } from "react";
import { API } from "aws-amplify";
import "./Home.css";
export default function Home({ isAuthenticated }) {
const [publicMessage, setPublic] = useState(null);
const [privateMessage, setPrivate] = useState(null);
useEffect(() => {
// 공개 및 비공개 API 로드
async function onLoad() {
try {
const response = await loadPublic();
setPublic(response.message);
} catch (e) {
setPublic(false);
}
try {
const response = await loadPrivate();
setPrivate(response.message);
} catch (e) {
setPrivate(false);
}
}
onLoad();
}, [isAuthenticated]);
function loadPublic() {
return API.get("random-api", "/public");
}
function loadPrivate() {
return API.get("random-api", "/private");
}
return (
<div className="Home">
<h3>{publicMessage}</h3>
<h3>
{privateMessage === false
? "비공개 메시지를 로드할 수 없습니다"
: privateMessage}
</h3>
</div>
);
}
이 코드는 공개 또는 비공개 API 엔드포인트를 로드할 수 있는지 여부를 보여줍니다. Amplify의 API
패키지를 사용하여 이러한 호출을 수행합니다. 이 호출은 현재 세션을 사용하여 인증된 요청을 만듭니다.
사용자가 비공개 엔드포인트에 연결하려면 인증을 받아야 하며, 그 전에 회원가입을 할 수 있어야 합니다!
회원가입 처리
사용자가 애플리케이션에 가입할 수 있도록 하기 위해 frontend/src/components/Signup.js
파일을 살펴보겠습니다.
먼저, React Bootstrap을 사용하여 폼을 만들었습니다.
function renderForm() {
return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="email" size="lg">
<Form.Label>이메일</Form.Label>
<Form.Control
autoFocus
type="email"
value={fields.email}
onChange={handleFieldChange}
/>
</Form.Group>
<Form.Group controlId="password" size="lg">
<Form.Label>비밀번호</Form.Label>
<Form.Control
type="password"
value={fields.password}
onChange={handleFieldChange}
/>
</Form.Group>
<Form.Group controlId="confirmPassword" size="lg">
<Form.Label>비밀번호 확인</Form.Label>
<Form.Control
type="password"
onChange={handleFieldChange}
value={fields.confirmPassword}
/>
</Form.Group>
<Button
block
size="lg"
type="submit"
variant="success"
disabled={isLoading || !validateForm()}
>
가입
</Button>
</Form>
);
}
이 폼을 제출하면 Amplify Auth 패키지를 사용하여 사용자를 가입시킵니다.
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
// 사용자 가입
const newUser = await Auth.signUp({
username: fields.email,
password: fields.password,
});
setIsLoading(false);
setNewUser(newUser);
} catch (e) {
alert(e);
setIsLoading(false);
}
}
가입 후, 사용자에게 확인 코드가 전송됩니다. 따라서 사용자가 코드를 입력할 수 있는 폼을 제공합니다.
function renderConfirmationForm() {
return (
<Form onSubmit={handleConfirmationSubmit}>
<Form.Group controlId="confirmationCode" size="lg">
<Form.Label>확인 코드</Form.Label>
<Form.Control
autoFocus
type="tel"
onChange={handleFieldChange}
value={fields.confirmationCode}
/>
<Form.Text muted>이메일로 전송된 코드를 확인해주세요.</Form.Text>
</Form.Group>
<Button
block
size="lg"
type="submit"
variant="success"
disabled={isLoading || !validateConfirmationForm()}
>
확인
</Button>
</Form>
);
}
마지막으로, 코드를 확인하고 사용자를 로그인시킵니다.
async function handleConfirmationSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
// 사용자의 확인 코드 확인
await Auth.confirmSignUp(fields.email, fields.confirmationCode);
// 사용자 로그인
await Auth.signIn(fields.email, fields.password);
userHasAuthenticated(true);
// 홈페이지로 리다이렉트
nav("/");
} catch (e) {
alert(e);
setIsLoading(false);
}
}
참고로 frontend/src/components/Signup.js
파일의 나머지 부분을 확인할 수 있습니다.
사용자 로그인
이제 사용자가 Cognito로 회원가입할 수 있게 되었습니다. 회원가입한 사용자가 로그인도 할 수 있는지 확인해 보겠습니다.
frontend/src/components/Login.js
파일에는 간단한 로그인 폼이 있습니다.
<div className="Login">
<Form onSubmit={handleSubmit}>
<Form.Group size="lg" controlId="email">
<Form.Label>이메일</Form.Label>
<Form.Control
autoFocus
type="email"
value={fields.email}
onChange={handleFieldChange}
/>
</Form.Group>
<Form.Group size="lg" controlId="password">
<Form.Label>비밀번호</Form.Label>
<Form.Control
type="password"
value={fields.password}
onChange={handleFieldChange}
/>
</Form.Group>
<Button
block
size="lg"
type="submit"
disabled={isLoading || !validateForm()}
>
로그인
</Button>
</Form>
</div>
사용자가 이 폼을 제출하면, Amplify에 요청을 보내 사용자를 로그인시킵니다. 회원가입 과정 마지막에 했던 호출과 동일한 것을 확인할 수 있습니다.
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
// 사용자 로그인
await Auth.signIn(fields.email, fields.password);
userHasAuthenticated(true);
// 홈페이지로 리다이렉트
nav("/");
} catch (e) {
alert(e);
setIsLoading(false);
}
}
사용자가 로그인되면, 홈페이지로 리다이렉트됩니다.
참고로 frontend/src/components/Login.js
파일의 나머지 부분도 확인해 보세요.
세션 로딩
이 모든 것을 연결하기 위해 앱이 로드될 때 세션이 로드되도록 해야 합니다. 사용자가 다시 로그인하지 않아도 되고, 앱의 모든 컴포넌트가 사용자가 인증되었음을 인지할 수 있도록 해야 합니다.
frontend/src/App.js
에서 Amplify로부터 현재 세션을 가져옵니다.
useEffect(() => {
async function onLoad() {
try {
// 사용자가 인증되었는지 확인
await Auth.currentSession();
userHasAuthenticated(true);
} catch (e) {
if (e !== "No current user") {
alert(e);
}
}
setIsAuthenticating(false);
}
onLoad();
}, []);
userHasAuthenticated
와 setIsAuthenticating
는 정의한 상태 변수입니다.
// 인증이 진행 중인지 추적
const [isAuthenticating, setIsAuthenticating] = useState(true);
// 사용자가 인증되었는지 추적
const [isAuthenticated, userHasAuthenticated] = useState(false);
마지막으로 이 변수들을 앱의 컴포넌트에 전달합니다.
// 모든 라우트에 전달될 props
const routeProps = { isAuthenticated, userHasAuthenticated };
<Routes>
<Route path="/" element={<Home {...routeProps} />}>
<Route path="/login" element={<Login {...routeProps} />}>
<Route path="/signup" element={<Signup {...routeProps} />}>
</Routes>
또한 사용자가 로그아웃할 수 있도록 합니다.
async function handleLogout() {
// 사용자 로그아웃
await Auth.signOut();
userHasAuthenticated(false);
}
참고를 위해 frontend/src/App.js
의 나머지 부분도 확인해 보세요.
앱 테스트하기
SST는 Live Lambda Development 환경을 제공하여 서버리스 앱을 실시간으로 작업할 수 있습니다.
예제를 테스트하려면 다음 명령어를 실행하세요.
$ npm install
$ npm start
이 명령어를 처음 실행하면 환경을 설정하는 데 몇 분 정도 걸립니다.
완료되면 다음과 같은 내용이 표시됩니다.
===============
앱 배포 중
===============
SST 앱 준비 중
소스 코드 변환 중
소스 코드 린트 중
스택 배포 중
dev-react-app-auth-cognito-my-stack: 배포 중...
dev-react-app-auth-cognito-my-stack
스택 dev-react-app-auth-cognito-my-stack
상태: 배포 완료
출력:
ApiEndpoint: https://gcnapdpral.execute-api.us-east-1.amazonaws.com
SiteUrl: https://d24wffw7qyqjnm.cloudfront.net
ReactSite:
REACT_APP_API_URL: https://gcnapdpral.execute-api.us-east-1.amazonaws.com
REACT_APP_IDENTITY_POOL_ID: us-east-1:ecfb817c-a5a8-43ef-9eba-b4a95fbe9ab0
REACT_APP_REGION: us-east-1
REACT_APP_USER_POOL_CLIENT_ID: 6fe8mgiaslpgrd8bphfsg634fe
REACT_APP_USER_POOL_ID: us-east-1_xN4Qv2SQR
React 애플리케이션도 시작합니다.
$ cd frontend
$ npm run start
로드가 완료되면 공개 API는 로드되지만 비공개 API는 실패하는 것을 확인할 수 있습니다.
이제 회원가입을 진행해 보겠습니다.
확인 코드를 입력하라는 메시지가 표시됩니다.
홈페이지로 리디렉션되면 비공개 랜덤 숫자 API가 이제 로드됩니다!
페이지를 새로고침해도 API가 이전과 동일하게 로드되는지 테스트할 수 있습니다.
로그아웃 버튼을 클릭하면 세션이 초기화되고 비공개 API를 더 이상 로드할 수 없습니다. 로그인 페이지도 테스트할 수 있습니다.
마무리
마지막으로, 여러분의 앱을 프로덕션 환경에 배포하려면 다음 명령어를 실행하세요.
$ npx sst deploy --stage prod
작업이 끝나면, 생성한 모든 리소스를 제거할 수 있습니다. 다음 명령어를 실행하세요.
$ npx sst remove
$ npx sst remove --stage prod
GitHub에 있는 예제 저장소를 확인해 보세요.
https://github.com/sst/sst/tree/master/examples/react-app-auth-cognito
다음 장에서는 또 다른 인증 프로바이더를 다룰 예정입니다!
For help and discussion
Comments on this chapter