5 인증 프론트엔드 통합
프론트엔드에서의 인증
로그인
로그인 후 백엔드 서비스로부터 받은 토큰을 로컬 스코리지에 저장해 놓고 요청을 보낼 때마다 헤더에 Bearer 토큰으로 지정
회원가입
리디렉션
백엔드에 HTTP 요청을 보냈을 때 403이 리턴되면 로그인 페이지로 리디렉트
5.1 라우팅
5.1.1 react-router-dom
react-router-dom
npm install react-router-dom
5.1.2 react-router-dom 라이브러리가 필요한 이유
서버 사이드 라우팅 vs 클라이언트 사이드 라우팅
5.1.3 로그인 컴포넌트
로그인 컴포넌트
AppRouter 컴포넌트
index.js 수정
5.1.4 접근 거부 시 로그인 페이지로 라우팅하기
ApiService
import { API_BASE_URL } from "../app-config";
const ACCESS_TOKEN = "ACCESS_TOKEN";
export function call(api, method, request) {
let headers = new Headers({
"Content-Type": "application/json",
});
const accessToken = localStorage.getItem("ACCESS_TOKEN");
if (accessToken && accessToken !== null) {
headers.append("Authorization", "Bearer " + accessToken);
}
let options = {
headers: headers,
url: API_BASE_URL + api,
method: method,
};
if (request) {
// GET method
options.body = JSON.stringify(request);
}
return fetch(options.url, options)
.then((response) =>
response.json().then((json) => {
if (!response.ok) {
// response.ok가 true이면 정상적인 리스폰스를 받은것, 아니면 에러 리스폰스를 받은것.
return Promise.reject(json);
}
return json;
})
)
.catch((error) => {
console.log(error.status);
if (error.status === 403) {
window.location.href = "/login";
}
return Promise.reject(error);
});
}
5.2 로그인 페이지
5.2.1 로그인을 위한 API 서비스 메서드 작성
ApiService
signin 함수
export function signin(userDTO) {
return call("/auth/signin", "POST", userDTO).then((response) => {
console.log("response : ", response);
alert("로그인 토큰" + response.token);
});
}
Login.js
import React from "react";
import { signin } from "./service/ApiService";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Container, Link } from "@material-ui/core";
class Login extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
const email = data.get("email");
const password = data.get("password");
signin({ email: email, password: password });
}
render() {
return (
<Container component="main" maxWidth="xs" style={{ marginTop: "8%" }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography component="h1" variant="h5">
로그인
</Typography>
</Grid>
</Grid>
<form noValidate onSubmit={this.handleSubmit}>
{" "}
{/* submit 버튼을 누르면 handleSubmit이 실행됨. */}
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="이메일 주소"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="패스워드"
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
<Grid item xs={12}>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
>
로그인
</Button>
</Grid>
</Grid>
</form>
</Container>
);
}
}
export default Login;
5.2.2 로그인에 성공
ApiService
signin 함수
export function signin(userDTO) {
return call("/auth/signin", "POST", userDTO).then((response) => {
if (response.token) {
window.location.href = "/";
}
});
}
5.3 로컬 스토리지를 이용한 액세스 토큰 관리
5.3.1 로컬 스토리지
웹 스토리지
사용자의 브라우저에 데이터를 kwy-value 형태로 저장 가능 (쿠키와 비슷)
종류
세션 스토리지
브라우저를 닫으면 사라짐
로컬 스토리지
브라우저를 닫아도 사라지지 않음
개발자 도구 → Application → Storage → Local Storage, Session Storage, IndexedDB, Web SQL, Cookies
5.3.2 액세스 토큰 저장
로그인 시 받은 토큰을 로컬 스토리지에 저장
ApiService
signin function
export function signin(userDTO) {
return call("/auth/signin", "POST", userDTO).then((response) => {
if (response.token) {
localStorage.setItem(ACCESS_TOKEN, response.token);
window.location.href = "/";
}
});
}
모든 API의 헤더에 액세스 토큰을 추가
로그인에 관련되지 않은 모든 API 콜은 call 메서드를 통해 이루어지므로, 반복을 피하기 위해서는 call에 토큰이 존재하는 경우 헤더에 추가하는 로직 작성
ApiService
Header
import { API_BASE_URL } from "../app-config";
const ACCESS_TOKEN = "ACCESS_TOKEN";
export function call(api, method, request) {
let headers = new Headers({
"Content-Type": "application/json",
});
const accessToken = localStorage.getItem("ACCESS_TOKEN");
if (accessToken && accessToken !== null) {
headers.append("Authorization", "Bearer " + accessToken);
}
let options = {
headers: headers,
url: API_BASE_URL + api,
method: method,
};
if (request) {
// GET method
options.body = JSON.stringify(request);
}
return fetch(options.url, options)
.then((response) =>
response.json().then((json) => {
if (!response.ok) {
// response.ok가 true이면 정상적인 리스폰스를 받은것, 아니면 에러 리스폰스를 받은것.
return Promise.reject(json);
}
return json;
})
)
.catch((error) => {
console.log(error.status);
// if (error.status === 403) {
// window.location.href = "/login";
// }
window.location.href = "/login";
return Promise.reject(error);
});
}
export function signin(userDTO) {
return call("/auth/signin", "POST", userDTO).then((response) => {
if (response.token) {
localStorage.setItem(ACCESS_TOKEN, response.token);
window.location.href = "/";
}
});
}
5.4 로그아웃과 글리치 해결
5.4.1 로그아웃 서비스
ApiService
signout
export function signout() {
localStorage.setItem(ACCESS_TOKEN, null);
window.location.href = "/login";
}
5.4.2 내비게이션 바와 로그아웃
Add NavigationBar at App component
import React from "react";
import "./App.css";
import Todo from "./Todo.js";
import AddTodo from "./AddTodo.js";
import {
Paper,
List,
Container,
Grid,
Button,
AppBar,
Toolbar,
Typography,
} from "@material-ui/core";
import { call, signout } from "./service/ApiService.js";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
};
}
componentDidMount() {
call("/todo", "GET", null).then((response) =>
this.setState({ items: response.data })
);
}
add = (item) => {
call("/todo", "POST", item).then((response) =>
this.setState({ items: response.data })
);
};
delete = (item) => {
call("/todo", "DELETE", item).then((response) =>
this.setState({ items: response.data })
);
};
update = (item) => {
call("/todo", "PUT", item).then((response) =>
this.setState({ items: response.data })
);
};
render() {
var todoItems = this.state.items.length > 0 && (
<Paper style={{ margin: 16 }}>
<List>
{this.state.items.map((item, idx) => (
<Todo
item={item}
key={item.id}
delete={this.delete}
update={this.update}
/>
))}
</List>
</Paper>
);
var navigationBar = (
<AppBar position="static">
<Toolbar>
<Grid justify="space-between" container>
<Grid item>
<Typography variant="h6">오늘의 할일</Typography>
</Grid>
<Grid>
<Button color="inherit" onClick={signout}>
로그아웃
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
);
return ( <div className="App">
{navigationBar}
<Container maxWidth="md">
<AddTodo add={this.add} />
<div className="TodoList">{todoItems}</div>
</Container></div>
);
}
}
export default App;
5.4.3 UI 글리치 해결
App.js
loading . .
import React from "react";
import "./App.css";
import Todo from "./Todo.js";
import AddTodo from "./AddTodo.js";
import {
Paper,
List,
Container,
Grid,
Button,
AppBar,
Toolbar,
Typography,
} from "@material-ui/core";
import { call, signout } from "./service/ApiService.js";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
loading: true,
};
}
componentDidMount() {
call("/todo", "GET", null).then((response) =>
this.setState({ items: response.data, loading: false })
);
}
add = (item) => {
call("/todo", "POST", item).then((response) =>
this.setState({ items: response.data })
);
};
delete = (item) => {
call("/todo", "DELETE", item).then((response) =>
this.setState({ items: response.data })
);
};
update = (item) => {
call("/todo", "PUT", item).then((response) =>
this.setState({ items: response.data })
);
};
render() {
var todoItems = this.state.items.length > 0 && (
<Paper style={{ margin: 16 }}>
<List>
{this.state.items.map((item, idx) => (
<Todo
item={item}
key={item.id}
delete={this.delete}
update={this.update}
/>
))}
</List>
</Paper>
);
var navigationBar = (
<AppBar position="static">
<Toolbar>
<Grid justify="space-between" container>
<Grid item>
<Typography variant="h6">오늘의 할일</Typography>
</Grid>
<Grid>
<Button color="inherit" onClick={signout}>
로그아웃
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
);
var todoListPage = (
<div>
{navigationBar}
<Container maxWidth="md">
<AddTodo add={this.add} />
<div className="TodoList">{todoItems}</div>
</Container>
</div>
);
var loadingPage = <h1>Loading . . .</h1>;
var content = loadingPage;
if (!this.state.loading) {
content = todoListPage;
}
return <div className="App">{content}</div>;
}
}
export default App;
5.5 계정 생성 페이지
5.5.1 계정 생성로직
ApiService
signup function
export function signup(userDTO) {
return call("/auth/signup", "POST", userDTO);
}
SignUp.js
import React from "react";
import {
Button,
TextField,
Link,
Grid,
Container,
Typography,
} from "@material-ui/core";
import { signup } from "./service/ApiService";
class SignUp extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
// 오브젝트에서 form에 저장된 데이터를 맵의 형태로 바꿔줌.
const data = new FormData(event.target);
const username = data.get("username");
const email = data.get("email");
const password = data.get("password");
signup({ email: email, username: username, password: password }).then(
(response) => {
// 계정 생성 성공 시 login페이지로 리디렉트
window.location.href = "/login";
}
);
}
render() {
return (
<Container component="main" maxWidth="xs" style={{ marginTop: "8%" }}>
<form noValidate onSubmit={this.handleSubmit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography component="h1" variant="h5">
계정 생성
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
autoComplete="fname"
name="username"
variant="outlined"
required
fullWidth
id="username"
label="유저 이름"
autoFocus
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="이메일 주소"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="패스워드"
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
<Grid item xs={12}>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
>
계정 생성
</Button>
</Grid>
</Grid>
<Grid container justify="flex-end">
<Grid item>
<Link href="/login" variant="body2">
이미 계정이 있습니까? 로그인 하세요.
</Link>
</Grid>
</Grid>
</form>
</Container>
);
}
}
export default SignUp;
AppRouter
SignUp
import React from "react";
import "./index.css";
import App from "./App";
import Login from "./Login";
import SignUp from "./SignUp";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
function Copyright() {
return (
<Typography variant="body2" color="textSecondary" align="center">
{"Copyright"}
fsoftwareengineer, {new Date().getFullYear()}
{"."}
</Typography>
);
}
class AppRouter extends React.Component {
render() {
return (
<div>
<Router>
<div>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<App />} />
<Route path="/signup" element={<SignUp />} />
</Routes>
</div>
<Box mt={5}>
<Copyright />
</Box>
</Router>
</div>
);
}
}
export default AppRouter;
Login.js
render() {
return (
<Container component="main" maxWidth="xs" style={{ marginTop: "8%" }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography component="h1" variant="h5">
로그인
</Typography>
</Grid>
</Grid>
<form noValidate onSubmit={this.handleSubmit}>
{" "}
{/* submit 버튼을 누르면 handleSubmit이 실행됨. */}
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="이메일 주소"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="패스워드"
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
<Grid item xs={12}>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
>
로그인
</Button>
</Grid>
<Link href="/signup" variant="body2">
<Grid item>계정이 없습니까 ? 여기서 가입하세요</Grid>
</Link>
</Grid>
</form>
</Container>
);
}
Last updated