๐Ÿ’ป ์ฝ”๋”ฉ ์ผ๊ธฐ : [์Šคํ”„๋ง ๊ฒŒ์‹œํŒ with React] '๋กœ๊ทธ์ธ ๋ฐ ๋กœ๊ทธ์•„์›ƒ ๊ตฌํ˜„' ํŽธ

ybkยท2024๋…„ 5์›” 23์ผ

spring

๋ชฉ๋ก ๋ณด๊ธฐ
39/55
post-thumbnail

๐Ÿ”” '๋กœ๊ทธ์ธ ๋ฐ ๋กœ๊ทธ์•„์›ƒ ๊ตฌํ˜„'์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!


๐Ÿ’Ÿ ๋กœ๊ทธ์ธ


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);

}
  • ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ member ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์™€์„œ 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 ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

  1. Map<String, Object> result = null;:

    • ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์„ ๋งต ๊ฐ์ฒด๋ฅผ ์„ ์–ธํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๊ฐ’์€ null์ž…๋‹ˆ๋‹ค.
  2. Member db = mapper.selectByEmail(member.getEmail());:

    • mapper.selectByEmail ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ฃผ์–ด์ง„ ์ด๋ฉ”์ผ๋กœ ํšŒ์› ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
    • ์กฐํšŒ๋œ Member ๊ฐ์ฒด๋Š” db์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  3. if (db != null) {:

    • ์กฐํšŒ๋œ ํšŒ์› ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด result๋Š” null ์ƒํƒœ๋กœ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
  4. if (passwordEncoder.matches(member.getPassword(), db.getPassword())) {:

    • passwordEncoder.matches ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ(member.getPassword())์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ(db.getPassword())๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
    • ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜๋ฉด JWT ํ† ํฐ ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  5. result = new HashMap<>();:

    • ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์„ ๋งต ๊ฐ์ฒด๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
  6. 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()): ๋‹‰๋„ค์ž„์„ ํด๋ ˆ์ž„์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  7. token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();:

    • jwtEncoder.encode ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ JWT ํ† ํฐ์„ ์ธ์ฝ”๋”ฉํ•˜๊ณ , getTokenValue() ๋ฉ”์„œ๋“œ๋กœ ํ† ํฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  8. result.put("token", token);:

    • ๊ฒฐ๊ณผ ๋งต์— ์ƒ์„ฑ๋œ JWT ํ† ํฐ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  9. 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์—์„œ ํ† ํฐ์„ ์ œ๊ฑฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’Ÿ Context ์‚ฌ์šฉํ•ด์„œ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๊ตฌํ˜„

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,
        });
      });
  }
  • ๋กœ๊ทธ์ธ : ๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” POST ์š”์ฒญ ํ›„ ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ•˜๋ฉด ๋ฐ”๋กœ localStorage์—์„œ ํ† ํฐ์„ ์ €์žฅํ–ˆ์ง€๋งŒ(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() || ~ : ๋กœ๊ทธ์ธ์ด ์•ˆ๋œ ์ƒํƒœ
profile
๊ฐœ๋ฐœ์ž ์ค€๋น„์ƒ~

0๊ฐœ์˜ ๋Œ“๊ธ€