
MemberLogin.jsx(React)
export function MemberLogin() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const toast = useToast();
const navigate = useNavigate();
function handleLogin() {
axios
.post("/api/member/token", { email, password })
.then((res) => {
localStorage.setItem("token", res.data.token);
toast({
status: "success",
description: "๋ก๊ทธ์ธ ์ฑ๊ณตํ์์ต๋๋ค.",
position: "top-right",
duration: 1000,
});
navigate("/");
})
.catch((err) => {
localStorage.removeItem("token");
toast({
status: "error",
description: "๋ก๊ทธ์ธ ์คํจํ์์ต๋๋ค.",
position: "top-right",
duration: 1000,
});
})
}
return (
<Box>
<Box>
<FormControl>
<FormLabel>์ด๋ฉ์ผ</FormLabel>
<Input onChange={(e) => setEmail(e.target.value)} />
</FormControl>
</Box>
<Box>
<FormControl>
<FormLabel>๋น๋ฐ๋ฒํธ</FormLabel>
<Input type={"password"} onChange={(e) => setPassword(e.target.value)} />
</FormControl>
</Box>
<Box>
<Button onClick={handleLogin} colorScheme={"blue"}>
๋ก๊ทธ์ธ
</Button>
</Box>
</Box>
);
}
localStorage์ ํ ํฐ ์ ์ฅ ํ ๋ฉ์ธ์ผ๋ก ์ด๋ํ๊ณ ์คํจํ๋ฉด localStorage์์ ํ ํฐ์ ์ญ์ ํฉ๋๋ค.MemberController.java
@PostMapping("token")
public ResponseEntity token(@RequestBody Member member) {
Map<String, Object> map = service.getToken(member);
if (map == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
return ResponseEntity.ok(map);
}
service.getToken(member)๋ฅผ ํธ์ถํ์ฌ member ๊ฐ์ฒด์ ๋ํ ํ ํฐ์ ์์ฑํ๊ฑฐ๋ ๊ฐ์ ธ์ต๋๋ค. ์ด ๊ฒฐ๊ณผ๋ Map<> ํํ๋ก map ๋ณ์์ ์ ์ฅํฉ๋๋ค.MemberService.java
public Map<String, Object> getToken(Member member) {
Map<String, Object> result = null;
Member db = mapper.selectByEmail(member.getEmail());
if (db != null) {
if (passwordEncoder.matches(member.getPassword(), db.getPassword())) {
result = new HashMap<>();
String token = "";
// ํ ํฐ ๋ง๋๋ ์ฝ๋
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(60 * 60 * 24 * 7)) //์ผ์ฃผ์ผ
.subject(db.getId().toString())
.claim("scope", "") //๊ถํ
.claim("nickName", db.getNickName())
.build();
token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
result.put("token", token);
}
}
return result;
}
public Map<String, Object> getToken(Member member) ๋ฉ์๋๋ ์ฃผ์ด์ง member ๊ฐ์ฒด์ ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฆํ์ฌ, ์ธ์ฆ์ ์ฑ๊ณตํ๋ฉด JWT ํ ํฐ์ ์์ฑํ๊ณ ๋ฐํํ๋ ๋ก์ง์ ๊ตฌํํ ๋ฉ์๋์
๋๋ค.
Map<String, Object> result = null;:
null์
๋๋ค.Member db = mapper.selectByEmail(member.getEmail());:
mapper.selectByEmail ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฃผ์ด์ง ์ด๋ฉ์ผ๋ก ํ์ ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค.Member ๊ฐ์ฒด๋ db์ ์ ์ฅ๋ฉ๋๋ค.if (db != null) {:
result๋ null ์ํ๋ก ๋ฐํ๋ฉ๋๋ค.if (passwordEncoder.matches(member.getPassword(), db.getPassword())) {:
passwordEncoder.matches ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์
๋ ฅ๋ ๋น๋ฐ๋ฒํธ(member.getPassword())์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ๋น๋ฐ๋ฒํธ(db.getPassword())๊ฐ ์ผ์นํ๋์ง ํ์ธํฉ๋๋ค.result = new HashMap<>();:
JWT ํ ํฐ ์์ฑ:
JwtClaimsSet.builder()๋ฅผ ์ฌ์ฉํ์ฌ JWT ํด๋ ์ ์ธํธ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.issuer("self"): ํ ํฐ ๋ฐํ์๋ฅผ "self"๋ก ์ค์ ํฉ๋๋ค.issuedAt(Instant.now()): ํ ํฐ ๋ฐํ ์๊ฐ์ ํ์ฌ ์๊ฐ์ผ๋ก ์ค์ ํฉ๋๋ค.expiresAt(Instant.now().plusSeconds(60 * 60 * 24 * 7)): ํ ํฐ ๋ง๋ฃ ์๊ฐ์ ์ผ์ฃผ์ผ ํ๋ก ์ค์ ํฉ๋๋ค.subject(db.getId().toString()): ํ ํฐ ์ฃผ์ ๋ฅผ DB์ ๋ค์ด ์๋ member์ id๋ก ์ค์ ใ
ํฉ๋๋ค.claim("scope", ""): ๊ถํ ์ ๋ณด๋ฅผ ๋น ๋ฌธ์์ด๋ก ์ค์ ํฉ๋๋ค. (์ค์ ์ ํ๋ฆฌ์ผ์ด์
์์๋ ํ์ํ ๊ถํ ์ ๋ณด๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.)claim("nickName", db.getNickName()): ๋๋ค์์ ํด๋ ์์ ์ถ๊ฐํฉ๋๋ค.token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();:
jwtEncoder.encode ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ JWT ํ ํฐ์ ์ธ์ฝ๋ฉํ๊ณ , getTokenValue() ๋ฉ์๋๋ก ํ ํฐ ๊ฐ์ ๊ฐ์ ธ์ต๋๋ค.result.put("token", token);:
return result;:
null์ ๋ฐํํฉ๋๋ค.์ด ๋ฉ์๋๋ ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฆํ์ฌ, ์ธ์ฆ์ ์ฑ๊ณตํ๋ฉด JWT ํ ํฐ์ ์์ฑํ๊ณ ์ด๋ฅผ ๊ฒฐ๊ณผ ๋งต์ ๋ด์ ๋ฐํํฉ๋๋ค. ์ธ์ฆ์ ์คํจํ๋ฉด null์ ๋ฐํํฉ๋๋ค. ์ด JWT ํ ํฐ์ ์ฃผ๋ก ํด๋ผ์ด์ธํธ๊ฐ ์ดํ์ ์์ฒญ์์ ์ธ์ฆ์ ์ํด ์ฌ์ฉํ ์ ์์ต๋๋ค.
Navbar.jsx(React)
<Button
onClick={() => {
localStorage.removeItem("token");
navigate("/login");
}}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
colorScheme="teal"
>
๋ก๊ทธ์์
</Button>
localStorage์์ ํ ํฐ์ ์ ๊ฑฐํ๋ฉด ๋ฉ๋๋ค.LoginProvider ์ปดํฌ๋ํธ๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ํ์ ๊ด๋ จ๋ ๋ค์ํ ํจ์์ ์ํ๋ฅผ Context๋ก ๊ด๋ฆฌํฉ๋๋ค. LoginContext๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ค๋ฅธ ์ปดํฌ๋ํธ์์ ๋ก๊ทธ์ธ ์ํ์ ๊ด๋ จ๋ ์ ๋ณด์ ํจ์๋ฅผ ์ฝ๊ฒ ์ ๊ทผํ ์ ์์ต๋๋ค.
LoginProvider.jsx(React)
import React, { createContext, useEffect, useState } from "react";
import { jwtDecode } from "jwt-decode";
export const LoginContext = createContext(null);
export function LoginProvider({ children }) {
const [id, setId] = useState("");
const [nickName, setNickName] = useState("");
const [expired, setExpired] = useState(0);
const [authority, setAuthority] = useState([]);
useEffect(() => {
const token = localStorage.getItem("token");
if (token === null) {
return;
}
login(token);
}, []);
// isLoggedIn
function isLoggedIn() {
return Date.now() < expired * 1000;
}
// ๊ถํ ์๋ ์ง? ํ์ธ
function hasAccess(param) {
return id == param;
}
function isAdmin() {
return authority.includes("admin");
}
// login
function login(token) {
localStorage.setItem("token", token);
const payload = jwtDecode(token);
setExpired(payload.exp);
setId(payload.sub);
setNickName(payload.nickName);
setAuthority(payload.scope.split(" ")); // "admin manager user"
}
// logout
function logout() {
localStorage.removeItem("token");
setExpired(0);
setId("");
setNickName("");
setAuthority([]);
}
return (
<LoginContext.Provider
value={{
id
nickName
login
logout
isLoggedIn
hasAccess
isAdmin
}}
>
{children}
</LoginContext.Provider>
);
}
isLoggedIn : ํ์ฌ ๋ก๊ทธ์ธ์ด ๋ ์ํ์ธ์ง ํ์ธํฉ๋๋ค. ํ์ฌ ์๊ฐ์ด ํ ํฐ ๋ง๋ฃ ์๊ฐ๋ณด๋ค ์์์ง ๋น๊ตํฉ๋๋ค.login : ๋ก๊ทธ์ธ์ ํ ๋๋ JWT ํ ํฐ์ localStorage์ ์ ์ฅํ๊ณ ํ ํฐ์ ํ์ด๋ก๋๋ฅผ ๋์ฝํ
ํ์ฌ ๋์ฝ๋ฉ๋ ํ์ด๋ก๋์์ ๋ง๋ฃ์๊ฐ, ์์ด๋, ๋๋ค์์ ๊ฐ์ ธ์ต๋๋ค.logout : ๋ก๊ทธ์์์ ํ ๋๋ ์์ด๋, ๋ง๋ฃ์๊ฐ, ๋๋ค์ ์ํ๋ฅผ ์ด๊ธฐํ์ํต๋๋ค.hasEmail : ํ์ฌ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์์ ์์ด๋์ด ํน์ ์์ด๋์ ์ผ์นํ๋์ง ํ์ธํฉ๋๋ค. useEffect() : useEffect ํ
์ ์ฌ์ฉํ์ฌ ์ปดํฌ๋ํธ๊ฐ ์ฒ์ ๋ ๋๋ง๋ ๋ localStorage์์ JWT ํ ํฐ์ ๊ฐ์ ธ์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ค์ ํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ๋ค์ ๋ฐฉ๋ฌธํ๋๋ผ๋ ๋ก๊ทธ์ธ ์ํ๊ฐ ์ ์ง๋ ์ ์์ต๋๋ค.MemberLogin.java
function handleLogin() {
axios
.post("/api/member/token", { email, password })
.then((res) => {
account.login(res.data.token);
toast({
status: "success",
description: "๋ก๊ทธ์ธ ์ฑ๊ณตํ์์ต๋๋ค.",
position: "top-right",
duration: 1000,
});
navigate("/");
})
.catch((err) => {
account.logout();
toast({
status: "error",
description: "๋ก๊ทธ์ธ ์คํจํ์์ต๋๋ค.",
position: "top-right",
duration: 1000,
});
});
}
localStorage.setItem("token", res.data.token)) ์์ ๋ ์ฝ๋์์๋ LoginProvider์ ์์ฑ๋ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ ธ์์ ์์ฑํฉ๋๋ค.Navbar.jsx
import { useNavigate } from "react-router-dom";
import { Box, Button, ButtonGroup, Flex, Spacer } from "@chakra-ui/react";
import React, { useContext } from "react";
import { LoginContext } from "./LoginProvider.jsx";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser } from "@fortawesome/free-regular-svg-icons";
export function Navbar() {
const navigate = useNavigate();
const account = useContext(LoginContext);
return (
<Flex minWidth="max-content" alignItems="center" gap="3">
<Box
onClick={() => navigate("/")}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
>
Home
</Box>
{account.isLoggedIn() && (
<Box
onClick={() => navigate("/write")}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
>
๊ธ์ฐ๊ธฐ
</Box>
)}
<Spacer />
{account.isLoggedIn() && (
<Box>
<FontAwesomeIcon icon={faUser} />
{account.nickName}
</Box>
)}
{account.isLoggedIn() || (
<Box
onClick={() => navigate("/member/list")}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
>
ํ์๋ชฉ๋ก
</Box>
)}
<Spacer />
<ButtonGroup gap="1">
{account.isLoggedIn() || (
<Button
onClick={() => navigate("/signup")}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
colorScheme="teal"
>
ํ์๊ฐ์
</Button>
)}
{account.isLoggedIn() || (
<Button
onClick={() => navigate("/login")}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
colorScheme="teal"
>
๋ก๊ทธ์ธ
</Button>
)}
{account.isLoggedIn() && (
<Button
onClick={() => {
account.logout();
navigate("/login");
}}
cursor={"pointer"}
_hover={{ bgColor: "gray.200" }}
colorScheme="teal"
>
๋ก๊ทธ์์
</Button>
)}
</ButtonGroup>
</Flex>
);
}
Navbar.jsx์์ LoginProvider์์ ์์ฑํ isLoggedIn์ ์ฌ์ฉํด์ ํ๋ฉด์ ๋ณด์ด๊ฒ ํฉ๋๋ค.account.isLoggedIn() && ~ : ๋ก๊ทธ์ธ์ด ๋ ์ํaccount.isLoggedIn() || ~ : ๋ก๊ทธ์ธ์ด ์๋ ์ํ