import { Navigate } from 'react-router-dom'
import { useContext } from 'react'
import AuthContext from 'context/AuthContext'
const PrivateRoute = ({children}) => {
let { user } = useContext(AuthContext) //1번
return !user ? <Navigate to="/login" /> : children // 2번
}
export default PrivateRoute
🤗
- user에 대한 자세한 설명은 아래의 AuthContext.js에서 확인 가능
- children으로 전달되는 컴포넌트가 무엇인지는 App.js에서 확인 가능하다.
잠깐)
props.children ??
Go 👉 https://velog.io/@pjh1011409/React-props.children
useContext ??
Go 👉 https://velog.io/@pjh1011409/React-Hooks-useContext#-context
참조
https://www.zerocho.com/category/React/post/5fa63fc6301d080004c4e32b
import PrivateRoute from 'utils/PrivateRoute'
const App = () =>{
return(
<Routes>
<Route
exact
path="/studyWrite"
element={
<PrivateRoute> //1번
<StudyWrite/>
</PrivateRoute>
}
/>
<Route
exact
path="/studyUpdate/:id"
element={
<PrivateRoute> //1번
<StudyUpdate />
</PrivateRoute>
}
/>
</Routes>
)
}
loginUser에 대한 설명은 AuthContext에서!
import AuthContext from 'context/AuthContext'
function LoginPage() {
let { loginUser } = useContext(AuthContext) //1번
return (
<form onSubmit={loginUser}> // 2번
<input
type="text" //3번
name="username"
placeholder="Enter Username"
/>
<input
type="password" // 4번
name="password"
placeholder="Enter Password"
/>
<input
className={styles.signInBtn}
type="submit" // 5번
value="Sign In"
/>
</form>
)
}
잠깐)
form 태그
- 웹페이지에서의 입력 양식을 의미
- 로그인 창, 회원가입 폼 등이 해당
- 텍스트 필드에 글자를 입력하거나, 체크박스나 라디오 버튼을 클릭하고 제출 버튼을 누르면 서버에 양식이 전달되어 정보를 처리
- form 태그는 전체 양식을 의미하고 화면에 보이지 않는 추상적인 태그
- form 태그 동작 방법: https://www.nextree.co.kr/p8428/
input 태그
- 실제로 사용자가 양식을 입력하기 위한 태그
- type 속성을 통해 종류를 나타내며, name을 통해 데이터의 이름, value를 통해 기본값을 지정
참조
import AuthContext from 'context/AuthContext'
const header = () =>{
let { user, logoutUser } = useContext(AuthContext) //1번
return(
...
{user ? (
<div onClick={logoutUser}> Logout</div> ) //2번
: (<Link to="/login">Login </Link>) //3번
}
{user && (<div >{user.username}님 환영합니다</div> // 4번
)}
...
)
}
잠깐) React에서 사용하는 조건문
: 리액트 조건문
createContext
Provider
💡 중요!
즉, createContext는 Context 객체를 만들 수 있고, Provider를 이용하여 Context 변경 사항을 자손들에게 제공할 수 있다. Provider의 Value는 하위의 모든 Consumer(소비자)에서 사용할 수 있으며, Provider 하위의 모든 Consumer는 Provider의 value가 변경될 때마다 재렌더링 된다.
createContext()를 통하여 생성된 Context
const AuthContext = createContext()
export default AuthContext
AuthContext의 변화를 알리고 값을 전달하는 Provider
<AuthContext.Provider value={contextData}>
{loading ? null : children}
</AuthContext.Provider>
Provider에 의해 하위 컴포넌트들(Consumer들)에게 전달되는 value
let contextData = {
user:user,
authTokens:authTokens,
setAuthTokens:setAuthTokens,
setUser:setUser,
loginUser:loginUser,
logoutUser:logoutUser,
}
지금까지 존재했던 Consumer들(= context를 구독하는 컴포넌트들)
let { user } = useContext(AuthContext)
let { loginUser } = useContext(AuthContext)
let { user, logoutUser } = useContext(AuthContext)
정리
1. 로그인,로그아웃 위해서 클라이언트단에서 서버에 인증을 받고 토큰을 부여받기 위한 데이터 통신 기능들이 AuthContext.js에 존재한다.
2. AuthContext.js 안에 존재하는 state값, 함수값들이 저장되어 있는 값들의 상태에 따라서 login.js, header.js, providerRoute.js의 기능들이 좌우된다.
3. 따라서, 그 값들을 공유하기 위해서 createContext와 Provider를 이용하였고, 각각 페이지에서는 useContext를 통해 값을 사용할 수 있게 된다.
참조: https://ko.reactjs.org/docs/context.html#reactcreatecontext
ex) a태그는 href를 통해 특정 사이트로 이동하거나, submit태그는 값을 전송하면서 창이 새로고침(reload)된다.
-> 이런 태그의 이벤트 기능을 preventDefault를 통하여 동작하지 않도록 막을 수 있다.
Why? 왜 이 코드가 필요할까?
앞서 말했듯이 이 함수는 onSumit속성에 의해 실행되고 있다. 따라서 loginUser가 실행될 것이다. 하지만, onSubmit은 값을 전송하면 창이 새로고침되는 동작을 발생시키게 되어, loginUser 함수 내의 코드들이 완벽히 실행될 수 없게 된다.
따라서, e.preventDefault()를 사용하여 새로고침을 막는 것이다!
- prevent(방지하다) + default(기본) = 기본동작을 방지(막다)
✏️ await
✏️ method
✏️ headers
✏️ body
✏️ response.json()
✏️ response === 200
✏️ else
let loginUser = async (e) => {
e.preventDefault()
let response = await fetch('http://127.0.0.1:8000/api/token/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// 실시간으로 입력한 아이디, 비번을 JSON형식으로 담는다.
body: JSON.stringify({
username: e.target.username.value,
password: e.target.password.value,
}),
})
let data = await response.json()
if (response.status === 200) {
setAuthTokens(data)
setUser(jwt_decode(data.access))
localStorage.setItem('authTokens', JSON.stringify(data))
navigate('/')
} else {
alert('Please enter your ID or password')
}
}
참조
useEffect의 deps로 지정된 authTokens 또는 loading의 변화 시에 렌더링 되게 되어있다
✏️ if(authTokens)
✏️ setLoading(false)
<AuthContext.Provider value={contextData}>
{loading ? null : children}
</AuthContext.Provider>
✏️ authTokens
로그인 시 토큰을 저장하지만, 로그인이 된 상태에서 access token의 만료로 authTokens에 값이 없어지는 상황이 생긴다. 이때 refresh token을 통해 재발급을 받게 된다.
-> AxiosInstance를 확인하자.
let [authTokens, setAuthTokens] = useState(() =>
localStorage.getItem('authTokens')
? JSON.parse(localStorage.getItem('authTokens'))
: null
)
...
✏️ user
loginUser가 실행될 때도, useEffect에서도 토큰이 존재할 시 user에는 디코딩된 access token 값이 저장된다. 말 그대로 사용자가 로그인하여 얻는 access token을 디코딩하면 다음과 같은 payload를 얻을 수 있다.
let [user, setUser] = useState(() =>
localStorage.getItem('authTokens')
? jwt_decode(localStorage.getItem('authTokens'))
: null
)
user.username 님 환영합니다. // houya님 환영합니다.
!user ? <Navigate to="/login" /> : children
지금까지 위에서 다룬 단계는 로그인을 하면 access token과 refresh token을 발급받는 것까지였다.
그렇다면 만료된 access token을 가지고 데이터를 요청하게 된다면?
서버에서는 access token을 검증하여 만료되었다는 것을 확인하고 401Error를 보낼 것이다. 그렇다면 우리는 발급받았던 refresh token을 가지고 새로운 access token을 발급해달라고 요청할 것이다.
이와 같이 토큰의 손상 또는 토큰의 만료 날짜 등의 상황으로 우리는 refresh token을 사용하여 단순히 업데이트하도록 백엔드에 요청을 호출한다.
이때 백엔드에서 요청에 대해 실패한다면 사용자는 네트워크 탭을 살펴보지 않고서는 실제로 무슨 일이 일어나는지 확인이 불가하다.
프론트엔드에서는 토큰을 업데이트 하지 않는다. 따라서 토큰이 전송되기전에 간단하게 확인을 하고 업데이트 한다면 네트워크 호출이 실패하지 않을 것이다.
바로 API를 중심으로 자체 인터셉더를 구축하는 것이다.
axios interceptors는 then이나 catch로 처리되기 전에 요청(request)나 응답(response)을 가로채 어떠한 작업을 수행할 수 있게 한다.
JWT 인증의 관점에서 보면, 토큰을 토큰을 주기적으로 업데이트 하는 대신 단순히 요청을 실행하고 해당 요청이 전송되기 전에 해당 토큰에 문제가 있을 경우 해당 토큰의 수명을 확인하는 함수라고 할 수 있다.
크게 3가지 부분으로 구성 (인스턴스, request 설정, response설정)
다시 말해, 인증이 필요한 서버로 데이터를 보내거나 조작하는 API를 호출할 때 보안을 위해 interceptor를 사용해서 HTTP Authorizaition 요청 헤더에 jwt-token을 보내서 서버 측 해당 API에 요청을 하기 전에 서버 측의 미들웨어에서 이를 확인한 후 검증이 완료되면 API에 요청을 하게 한다.
해당 interceptor를 인증이 필요한 API를 호출하는 게시판 쓰기, 수정, 삭제와 댓글 쓰기, 수정, 삭제에서 불러와서 사용하면 된다!
const baseURL = 'http://127.0.0.1:8000'
let authTokens = localStorage.getItem('authTokens')
? JSON.parse(localStorage.getItem('authTokens'))
: null
기본값인 bareer 옆에 변수(authTokens)를 전달하여 인증 토큰을 얻을 수 있다.
const axiosInstance = axios.create({
baseURL,
headers:{Authorization: `Bearer ${authTokens?.access}`}
});
💡 잠깐 Baerer?
axiosInstance.interceptors.request.use(async req => {
...
return req
})
if(!authTokens){
authTokens = localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null
req.headers.Authorization = `Bearer ${authTokens?.access}`
}
const user = jwt_decode(authTokens.access)
payload의 exp가 바로 만료시간에 대한 정보
const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1;
if(!isExpired) return req
const response = await axios.post(`${baseURL}/api/token/refresh/`, {
refresh: authTokens.refresh
});
localStorage.setItem('authTokens', JSON.stringify(response.data))
req.headers.Authorization = `Bearer ${response.data.access}`
Token 만료 시, 서버에서 만료 신호(401)를 반환한다. 이 점을 보완하자!
Interceptor를 통하여 토큰의 만료유무를 미리 판별하여, 만료 시 Refresh Token을 통한 새로운 Acess Token을 받는다.