Search

React x JWT x SpringSecurity

React x JWT x SpringSecurity

Client (React)

Server (Spring Boot)

React

Preview
라이브러리
react-router-dom
axios
js-cookie
sweetalert2
sweetalert2-react-content
package.json
client 프로젝트 구조
contexts
apis
components
pages
Root
App.css
App.js

Preview

메인 화면
회원가입 화면
회원가입 성공
로그인 화면
로그인 성공
로그인 실패
로그아웃
로그아웃 성공
마이 페이지
회원 정보 수정 성공
회원 가입 성공

메인 화면

회원가입 화면

회원가입 성공

로그인 화면

로그인 성공

로그인 실패

로그아웃

로그아웃 성공

마이 페이지

회원 정보 수정 성공

회원 가입 성공

라이브러리

react-router-dom
axios
js-cookie
sweetalert2
sweetalert2-react-content
# React X JWT X Spring Security ## 라이브러리 설치 ### router npm install react-router-dom ### axios npm install axios ### cookie npm install js-cookie ### sweetalert2 npm install sweetalert2 npm install sweetalert2-react-content
Markdown
복사

package.json

{ "name": "client", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.2", "js-cookie": "^3.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.0", "react-scripts": "5.0.1", "sweetalert2": "^11.12.0", "sweetalert2-react-content": "^5.0.7", "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "proxy": "http://localhost:8080" }
JSON
복사

client 프로젝트 구조

contexts

LoginContextProvider.jsx
LoginContextConsumer.jsx

LoginContextProvider.jsx

import React, { createContext, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import Cookies from 'js-cookie' import api from '../apis/api' import * as auth from '../apis/auth' import * as Swal from '../apis/alert' // 📦컨텍스트 생성 export const LoginContext = createContext() const LoginContextProvider = ({ children }) => { /* -----------------------[State]-------------------------- */ // 로그인 여부 const [isLogin, setLogin] = useState(false); // 유저 정보 const [userInfo, setUserInfo] = useState(null) // 권한 정보 const [roles, setRoles] = useState({isUser : false, isAmdin : false}) /* -------------------------------------------------------- */ // 페이지 이동 const navigate = useNavigate() // 🍪➡💍 로그인 체크 const loginCheck = async () => { // 🍪 accessToken 쿠키 확인 const accessToken = Cookies.get("accessToken") console.log(`accessToken : ${accessToken}`); // 💍in🍪 ❌ if( !accessToken ) { console.log(`쿠키에 accessToken(jwt) 가 없음`); // 로그아웃 세팅 logoutSetting() return } // 💍in🍪 ⭕ console.log(`쿠키에 JWT(accessToken) 이 저장되어 있음`); // axios common header 에 등록 api.defaults.headers.common.Authorization = `Bearer ${accessToken}` // 👩‍💼 사용자 정보 요청 let response let data try { response = await auth.info() } catch (error) { console.log(`error : ${error}`); console.log(`status : ${response.status}`); return } data = response.data // data = 👩‍💼 사용자 정보 console.log(`data : ${data}`); // 인증 실패 ❌ if( data == 'UNAUTHORIZED' || response.status == 401 ) { console.log(`accessToek(jwt) 이 만료되었거나 인증에 실패하였습니다.`); return } // 인증 성공 ✅ console.log(`accessToken(jwt) 토큰으로 사용자 정보 요청 성공!`); // 로그인 세팅 loginSetting( data, accessToken ) } // 🔐 로그인 const login = async (username, password) => { console.log(`username: ${username}`); console.log(`password: ${password}`); try { const response = await auth.login(username, password) const data = response.data const status = response.status const headers = response.headers const authorization = headers.authorization // 💍 JWT const accessToken = authorization.replace("Bearer ", "") console.log(`data : ${data}`); console.log(`status : ${status}`); console.log(`headers : ${headers}`); console.log(`jwt : ${accessToken}`); // 로그인 성공 ✅ if( status == 200 ) { Cookies.set("accessToken", accessToken) // 로그인 체크 loginCheck() Swal.alert("로그인 성공", "메인 화면으로 이동합니다", "success", () => { navigate("/") } ) // 메인 페이지로 이동 // navigate("/") } } catch (error) { Swal.alert("로그인 실패", "아이디 또는 비밀번호가 일치하지 않습니다", "error") console.log(`로그인 실패`); } } // 🔐 로그인 세팅 // 👩‍💼 userData, 💍 accessToken(jwt) const loginSetting = (userData, accessToken) => { const { no, userId, authList } = userData // 👩‍💼 Users (DTO) [JSON] const roleList = authList.map((auth) => auth.auth) // 💳 [ROLE_USER,ROLE_ADMIN] console.log(`no : ${no}`); console.log(`userId : ${userId}`); console.log(`authList : ${authList}`); console.log(`roleList : ${roleList}`); // axios common header - Authorizaion 헤더에 jwt 등록 api.defaults.headers.common.Authorization = `Bearer ${accessToken}` // 📦 Context 에 정보 등록 // 🔐 로그인 여부 세팅 setLogin(true) // 👩‍💼 유저 정보 세팅 const updatedUserInfo = {no, userId, roleList} setUserInfo(updatedUserInfo) // 👮‍♀️ 권한 정보 세팅 const updatedRoles = { isUser : false, isAdmin : false } roleList.forEach( (role) => { if( role == 'ROLE_USER' ) updatedRoles.isUser = true if( role == 'ROLE_ADMIN' ) updatedRoles.isAdmin = true }) setRoles(updatedRoles) } // 로그아웃 세팅 const logoutSetting = () => { // 🚀❌ axios 헤더 초기화 api.defaults.headers.common.Authorization = undefined; // 🍪❌ 쿠키 초기화 Cookies.remove("accessToken") // 🔐❌ 로그인 여부 : false setLogin(false) // 👩‍💼❌ 유저 정보 초기화 setUserInfo(null) // 👮‍♀️❌ 권한 정보 초기화 setRoles(null) } // 🔓 로그아웃 const logout = (force=false) => { if( force ) { // 로그아웃 세팅 logoutSetting() // 페이지 이동 ➡ "/" (메인) navigate("/") return } Swal.confirm("로그아웃하시겠습니까?", "로그아웃을 진행합니다.", "warning", (result) => { // isConfirmed : 확인 버튼 클릭 여부 if( result.isConfirmed ) { Swal.alert("로그아웃 성공", "", "success") logoutSetting() // 로그아웃 세팅 navigate("/") // 메인 페이지로 이동 } } ) // const check = window.confirm("정말로 로그아웃하시겠습니까?") // if( check ) { // // 로그아웃 세팅 // logoutSetting() // // 메인 페이지로 이동 // navigate("/") // } } // Mount / Update useEffect( () => { // 로그인 체크 loginCheck() // 1️⃣ 🍪 쿠키에서 jwt💍 을 꺼낸다 // 2️⃣ jwt 💍 있으면, 서버한테 👩‍💼 사용자정보를 받아온다 // 3️⃣ 로그인 세팅을 한다. (📦 로그인여부, 사용자정보, 권한정보 등록) }, []) return ( // 컨텍스트 값 지정 ➡ value={ ?, ? } <LoginContext.Provider value={ {isLogin, userInfo, roles, login, loginCheck, logout} }> {children} </LoginContext.Provider> ) } export default LoginContextProvider
JavaScript
복사

LoginContextConsumer.jsx

import React, { useContext } from 'react' import { LoginContext } from './LoginContextProvider' const LoginContextConsumer = () => { const { isLogin } = useContext(LoginContext) return ( <div> <h3>로그인 여부 : {isLogin ? '로그인' : '로그아웃'} </h3> </div> ) } export default LoginContextConsumer
JavaScript
복사

apis

api.js
auth.js
alert.js

api.js

import axios from 'axios' // axios 객체 생성 const api = axios.create() export default api
JavaScript
복사

auth.js

import api from './api'; // 로그인 export const login = (username, password) => api.post(`/login?username=${username}&password=${password}`) // 사용자 정보 export const info = () => api.get(`/users/info`) // 회원 가입 export const join = (data) => api.post(`/users`, data) // 회원 정보 수정 export const update = (data) => api.put(`/users`, data) // 회원 탈퇴 export const remove = (userId) => api.delete(`/users/${userId}`)
JavaScript
복사

alert.js

import Swal from 'sweetalert2'; import withReactContent from 'sweetalert2-react-content'; /** * icon : success, error, warning, info, question */ const MySwal = withReactContent(Swal) // 기본 alert ⚠ export const alert = (title, text, icon, callback) => { MySwal.fire({ title: title, text: text, icon: icon }) .then( callback ) // 경고창 출력 이후 실행할 콜백함수 } // confirm 👩‍🏫 export const confirm = (title, text, icon, callback) => { MySwal.fire({ title: title, text: text, icon: icon, showCancelButton: true, cancelButtonColor: "#d33", cancelButtonText: "No", confirmButtonColor: "#3085d6", confirmButtonText: "Yes", }) .then( callback ) }
JavaScript
복사

components

Header
Header.jsx
Header.css
Join
JoinForm.jsx
JoinForm.css
Login
LoginForm.jsx
LoginForm.css
User
UserForm.jsx
UserForm.css

Header

Header.jsx

import React, { useContext } from 'react' import { Link } from 'react-router-dom' import './Header.css' import { LoginContext } from '../../contexts/LoginContextProvider' const Header = () => { // 📦 LoginContext 가져오기 // 🧊 isLogin // 🌞 logout const { isLogin, logout } = useContext(LoginContext) return ( <header> <div className="logo"> <Link to="/"> <img src="https://i.imgur.com/fzADqJo.png" alt='logo' className='logo' /> </Link> </div> <div className="util"> <ul> {/* 로그인 여부(isLogin)에 따라서 조건부 렌더링 */} { isLogin ? <> <li><Link to="/user">마이페이지</Link></li> <li><button className='link' onClick={ () => logout() }>로그아웃</button></li> </> : <> <li><Link to="/login">로그인</Link></li> <li><Link to="/join">회원가입</Link></li> <li><Link to="/about">소개</Link></li> </> } </ul> </div> </header> ) } export default Header
JavaScript
복사

Header.css

header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #000; } .logo { width: 80px; } .util ul { display: flex; justify-content: space-between; align-items: center; column-gap: 12px; }
CSS
복사

Join

JoinForm.jsx

import React from 'react' const JoinForm = ({ join }) => { const onJoin = (e) => { e.preventDefault() // submit 기본 동작 방지 const form = e.target const userId = form.username.value const userPw = form.password.value const name = form.name.value const email = form.email.value console.log(userId, userPw, name, email); join( {userId, userPw, name, email} ) } return ( <div className="form"> <h2 className="login-title">Join</h2> <form className='login-form' onSubmit={ (e) => onJoin(e) }> <div> <label htmlFor="name">username</label> <input type="text" id='username' placeholder='username' name='username' autoComplete='username' required /> </div> <div> <label htmlFor="password">password</label> <input type="password" id='passowrd' placeholder='password' name='password' autoComplete='password' required /> </div> <div> <label htmlFor="name">Name</label> <input type="text" id='name' placeholder='name' name='name' autoComplete='name' required /> </div> <div> <label htmlFor="name">Email</label> <input type="text" id='email' placeholder='email' name='email' autoComplete='email' required /> </div> <button type='submit' className='btn btn--form btn-login'> Join </button> </form> </div> ) } export default JoinForm
JavaScript
복사

JoinForm.css

.form { width: 400px; margin: auto; padding: 36px 48px 48px 48px; background-color: #f2efee; border-radius: 11px; box-shadow: 0 2.4rem 4.8rem rgba(0, 0, 0, 0.15); } .login-title { padding: 15px; font-size: 22px; font-weight: 600; text-align: center; } .login-form { display: grid; grid-template-columns: 1fr; row-gap: 16px; } .login-form label { display: block; margin-bottom: 8px; } .login-form input { width: 100%; padding: 1.2rem; border-radius: 9px; border: none; } .login-form input:focus { outline: none; box-shadow: 0 0 0 4px rgba(253, 242, 233, 0.5); } .btn--form { background-color: #f48982; color: #fdf2e9; align-self: end; padding: 8px; } .btn, .btn:link, .btn:visited { display: inline-block; text-decoration: none; font-size: 20px; font-weight: 600; border-radius: 9px; border: none; cursor: pointer; font-family: inherit; transition: all 0.3s; } .btn-login:hover { outline: 1px solid #f48982; } .btn--form:hover { background-color: #fdf2e9; color: #f48982; }
CSS
복사

Login

LoginForm.jsx

import React, { useContext } from 'react' import { LoginContext } from '../../contexts/LoginContextProvider' import './LoginForm.css' const LoginForm = () => { const { login } = useContext(LoginContext) // 📦 LoginContext 의 login 함수 const onLogin = (e) => { e.preventDefault() // 기본 이벤트 방지 const form = e.target // <form> 요소 const username = form.username.value // 아이디 - <form> 아래 input name="username" 의 value const password = form.password.value // 비밀번호 - <form> 아래 input name="passwword" 의 value login( username, password ) // 로그인 처리 요청 } return ( <div className="form"> <h2 className="login-title">Login</h2> <form className='login-form' onSubmit={ (e) => onLogin(e) }> <div> <label htmlFor="name">username</label> <input type="text" id='username' placeholder='username' name='username' autoComplete='username' required /> </div> <div> <label htmlFor="password">password</label> <input type="password" id='passowrd' placeholder='password' name='password' autoComplete='password' required /> </div> <button type='submit' className='btn btn--form btn-login'> Login </button> </form> </div> ) } export default LoginForm
JavaScript
복사

LoginForm.css

.form { width: 400px; margin: auto; padding: 36px; background-color: beige; border-radius: 12px; box-shadow: 2px 2px 2px rgba(0,0,0,0.15); } .login-title { text-align: center; padding: 15px; font-size: 22px; font-weight: 600; } .login-form label { display: block; margin-bottom: 8px; } .login-form input { width: 100%; padding: 1.2rem; border-radius: 10px; border: none; } .login-form input:focus { outline: none; box-shadow: 0 0 0 4px rgba(253, 242, 233, 0.5); } .btn--form { background-color: #f48982; color: #fdf2e9; padding: 8px; } .btn { display: inline-block; text-decoration: none; font-size: 20px; font-weight: 600; border-radius: 10px; border: none; cursor: pointer; transition: all 0.3s; width: 100%; margin-top: 12px; } .btn-login:hover { outline: 1px solid #f48982; }
CSS
복사

User

UserForm.jsx

import React from 'react' const UserForm = ({ userInfo, updateUser, deleteUser }) => { const onUpdate = (e) => { e.preventDefault() const form = e.target const userId = form.username.value const userPw = form.password.value const name = form.name.value const email = form.email.value console.log(userId, userPw, name, email); updateUser( {userId, userPw, name, email } ) } return ( <div className="form"> <h2 className="login-title">UserInfo</h2> <form className='login-form' onSubmit={ (e) => onUpdate(e) }> <div> <label htmlFor="name">username</label> <input type="text" id='username' placeholder='username' name='username' autoComplete='username' required readOnly defaultValue={ userInfo?.userId } /> </div> <div> <label htmlFor="password">password</label> <input type="password" id='passowrd' placeholder='password' name='password' autoComplete='password' required /> </div> <div> <label htmlFor="name">Name</label> <input type="text" id='name' placeholder='name' name='name' autoComplete='name' required defaultValue={ userInfo?.name } /> </div> <div> <label htmlFor="name">Email</label> <input type="text" id='email' placeholder='email' name='email' autoComplete='email' required defaultValue={ userInfo?.email } /> </div> <button type='submit' className='btn btn--form btn-login'> 정보 수정 </button> <button type='button' className='btn btn--form btn-login' onClick={ () => deleteUser(userInfo.userId)} > 회원 탈퇴 </button> </form> </div> ) } export default UserForm
JavaScript
복사

UserForm.css

CSS
복사

pages

Home.jsx
Login.jsx
Join.jsx
User.jsx
Admin.jsx
About.jsx

Home.jsx

import React from 'react' import Header from '../components/Header/Header' import LoginContextConsumer from '../contexts/LoginContextConsumer' const Home = () => { return ( <> <Header /> <div className="container"> <h1>Home</h1> <hr /> <h2>메인 페이지</h2> <LoginContextConsumer /> </div> </> ) } export default Home
JavaScript
복사

Login.jsx

import React from 'react' import Header from '../components/Header/Header' import LoginForm from '../components/Login/LoginForm' const Login = () => { return ( <> <Header /> <div className="container"> <LoginForm /> </div> </> ) } export default Login
JavaScript
복사

Join.jsx

import React from 'react' import Header from '../components/Header/Header' import JoinForm from '../components/Join/JoinForm' import * as auth from '../apis/auth' import { useNavigate } from 'react-router-dom' import * as Swal from '../apis/alert'; const Join = () => { const navigate = useNavigate() // 회원가입 요청 const join = async ( form ) => { console.log(form); let response let data try { response = await auth.join(form) } catch (error) { console.error(`${error}`); console.error(`회원가입 요청 중 에러가 발생하였습니다.`); return } data = response.data const status = response.status console.log(`data : ${data}`); console.log(`status : ${status}`); if( status === 200 ) { console.log(`회원가입 성공!`); Swal.alert("회원가입 성공", "메인 화면으로 이동합니다.", "success", () => { navigate("/login") }) // alert(`회원가입 성공!`) // navigate("/login") } else { console.log(`회원가입 실패!`); // alert(`회원가입에 실패하였습니다.`) Swal.alert("회원가입 실패", "회원가입에 실패하였습니다.", "error" ) } } return ( <> <Header /> <div className="container"> <JoinForm join={ join } /> </div> </> ) } export default Join
JavaScript
복사

User.jsx

import React, { useContext, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import * as auth from '../apis/auth' import Header from '../components/Header/Header' import UserForm from '../components/User/UserForm' import { LoginContext } from '../contexts/LoginContextProvider' import * as Swal from '../apis/alert'; const User = () => { const { isLogin, roles, logout } = useContext(LoginContext) const [ userInfo, setUserInfo ] = useState() const navigate = useNavigate() // 회원 정보 조회 - /user/info const getUserInfo = async () => { // 비로그인 또는 USER 권한이 없으면 ➡ 로그인 페이지로 이동 if( !isLogin || !roles.isUser ) { console.log(`isLogin : ${isLogin}`); console.log(`roles.isUser : ${roles.isUser}`); navigate("/login") return } const response = await auth.info() const data = response.data console.log(`getUserInfo`); console.log(data); setUserInfo(data) } // 회원 정보 수정 const updateUser = async ( form ) => { console.log(form); let response let data try { response = await auth.update(form) } catch (error) { console.error(`${error}`); console.error(`회원정보 수정 중 에러가 발생하였습니다.`); return } data = response.data const status = response.status console.log(`data : ${data}`); console.log(`status : ${status}`); if( status === 200 ) { console.log(`회원정보 수정 성공!`); // alert(`회원정보 수정 성공!`) // logout() Swal.alert("회원수정 성공", "로그아웃 후, 다시 로그인해주세요.", "success", () => { logout(true) }) } else { console.log(`회원정보 수정 실패!`); // alert(`회원정보 수정 실패!`) Swal.alert("회원수정 실패", "회원수정에 실패하였습니다.", "error" ) } } // 회원 탈퇴 const deleteUser = async (userId) => { console.log(userId); let response let data try { response = await auth.remove(userId) } catch (error) { console.error(`${error}`); console.error(`회원삭제 중 에러가 발생하였습니다.`); return } data = response.data const status = response.status console.log(`data : ${data}`); console.log(`status : ${status}`); if( status === 200 ) { console.log(`회원삭제 성공!`); // alert(`회원삭제 성공!`) // logout() Swal.alert("회원탈퇴 성공", "그동안 감사했습니다:)", "success", () => { logout(true) }) } else { console.log(`회원삭제 실패!`); // alert(`회원삭제 실패!`) Swal.alert("회원탈퇴 실패", "회원탈퇴에 실패하였습니다.", "error" ) } } useEffect( () => { if( !isLogin ) { return } getUserInfo() }, [isLogin]) return ( <> <Header /> <div className="container"> <UserForm userInfo={userInfo} updateUser={updateUser} deleteUser={deleteUser} /> </div> </> ) } export default User
JavaScript
복사

Admin.jsx

import React, { useContext, useEffect } from 'react' import Header from '../components/Header/Header' import { LoginContext } from '../contexts/LoginContextProvider' import { useNavigate } from 'react-router-dom'; import * as Swal from '../apis/alert'; const Admin = () => { const { isLogin, userInfo, roles } = useContext(LoginContext); const navigate = useNavigate() useEffect( () => { if( !isLogin || !userInfo ) { // alert(`로그인이 필요합니다.`) // navigate("/login") Swal.alert("로그인이 필요합니다.", "로그인 화면으로 이동합니다.", "warning", () => { navigate("/login") }) return } if( !roles.isAdmin ) { // alert(`권한이 없습니다`) // navigate(-1) Swal.alert("권한이 없습니다.", "이전 화면으로 이동합니다.", "warning", () => { navigate(-1) }) return } }, [userInfo]) return ( <> { isLogin && roles.isAdmin && <> <Header /> <div className="container"> <h1>Admin</h1> <hr /> <h2>관리자 페이지</h2> <center> <img src="/img/loading.webp" alt="loading" /> </center> </div> </> } </> ) } export default Admin
JavaScript
복사

About.jsx

import React from 'react' import Header from '../components/Header/Header' import LoginContextConsumer from '../contexts/LoginContextConsumer' const About = () => { return ( <> <Header /> <div className="container"> <h1>About</h1> <hr /> <h2>소개 페이지</h2> <LoginContextConsumer /> </div> </> ) } export default About
JavaScript
복사

Root

App.css
App.js

App.css

.container { width: 960px; margin: 50px auto; } hr { margin-bottom: 50px; }
CSS
복사

App.js

import { BrowserRouter, Route, Routes } from 'react-router-dom'; import './App.css'; import Home from './pages/Home'; import Login from './pages/Login'; import Join from './pages/Join'; import User from './pages/User'; import About from './pages/About'; import LoginContextProvider from './contexts/LoginContextProvider'; import Admin from './pages/Admin'; function App() { return ( <BrowserRouter> <LoginContextProvider> <Routes> <Route path="/" element={ <Home /> }></Route> <Route path="/login" element={ <Login /> }></Route> <Route path="/join" element={ <Join /> }></Route> <Route path="/user" element={ <User /> }></Route> <Route path="/about" element={ <About /> }></Route> <Route path="/admin" element={ <Admin /> }></Route> </Routes> </LoginContextProvider> </BrowserRouter> ); } export default App;
JavaScript
복사