๐Ÿ’ป ์ฝ”๋”ฉ ์ผ๊ธฐ : [์Šคํ”„๋ง ๊ฒŒ์‹œํŒ with React] '๊ถŒํ•œ ์„ค์ • ๋ฐ ํ† ํฐ' ํŽธ

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

spring

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

๐Ÿ”” '๊ถŒํ•œ ์„ค์ • ๋ฐ ํ† ํฐ'์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!


๐Ÿ’Ÿ ํšŒ์›๋ชฉ๋ก admin์ผ ๋•Œ๋งŒ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝ


๊ถŒํ•œ ํ…Œ์ด๋ธ”

## ๊ถŒํ•œ ํ…Œ์ด๋ธ”
CREATE TABLE authority
(
    member_id INT         NOT NULL REFERENCES member (id),
    name      VARCHAR(20) NOT NULL,
    PRIMARY KEY (member_id, name)
);
  • member_id๋ฅผ ๋ง์•„์„œ ๊ถŒํ•œ์„ ์ง€์ •ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ๊ถŒํ•œ ํ…Œ์ด๋ธ”์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

MemberController.java

    @GetMapping("list")
    @PreAuthorize("hasAuthority('SCOPE_admin')")
    public List<Member> list() {
        return service.list();
    }
  • @PreAuthorize("hasAuthority('SCOPE_admin')")์„ ์‚ฌ์šฉํ•ด์„œ admin ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์‚ฌ๋žŒ๋งŒ ํšŒ์› ๋ชฉ๋ก์„ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

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 = "";
                List<String> authority = mapper.selectAuthorityByMemberId(db.getId());
                String authorityString = authority.stream().collect(Collectors.joining(" "));

                // ํ† ํฐ ๋งŒ๋“œ๋Š” ์ฝ”๋“œ
                JwtClaimsSet claims = JwtClaimsSet.builder()
                        .issuer("self")
                        .issuedAt(Instant.now())
                        .expiresAt(Instant.now().plusSeconds(60 * 60 * 24 * 7)) //์ผ์ฃผ์ผ
                        .subject(db.getId().toString())
                        .claim("scope", authorityString) //๊ถŒํ•œ
                        .claim("nickName", db.getNickName())
                        .build();
                token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
                result.put("token", token);
            }
        }
        return result;
    }
  • mapper.selectAuthorityByMemberId(db.getId()) : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ฃผ์–ด์ง„ ๋ฉค๋ฒ„ ID์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • ๊ฐ€์ ธ์˜จ ๊ถŒํ•œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„๋œ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ํ•ฉ์นฉ๋‹ˆ๋‹ค.

MemberMapper.java

    @Select("SELECT name FROM authority WHERE member_id = #{memberId}")
    List<String> selectAuthorityByMemberId(Integer memberId);
  • ๊ถŒํ•œ ํ…Œ์ด๋ธ”์—์„œ member์˜ id๋กœ ํ•ด๋‹น ๊ถŒํ•œ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’Ÿ admin ๊ถŒํ•œ ์„ค์ •


ํšŒ์› ๋ชฉ๋ก admin ๊ถŒํ•œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ

LoginProviedr.jsx

const [authority, setAuthority] = useState([]);
  function isAdmin() {
    return authority.includes("admin");
  }
  • authority ๋ฐฐ์—ด์— admin ๊ถŒํ•œ์ด ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

Navbar.jsx(React)

{account.isAdmin() && (
        <Box
          onClick={() => navigate("/member/list")}
          cursor={"pointer"}
          _hover={{ bgColor: "gray.200" }}
        >
          ํšŒ์›๋ชฉ๋ก
        </Box>
      )}
  • admin์ผ ๊ฒฝ์šฐ์—๋งŒ Navbar์—์„œ ํšŒ์›๋ชฉ๋ก์„ ๋ณด์ด๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

MemberService.java

    public boolean hasAccess(Integer id, Authentication authentication) {
        boolean self = authentication.getName().equals(id.toString());
        boolean isAdmin = authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("SCOPE_admin"));
        return self || isAdmin;

    }
  • ๊ณ„์ • ์ •๋ณด๋ฅผ ์ž์‹ ๋งŒ ๋ณด๊ฑฐ๋‚˜ admin๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค.

MemberView.jsx

{account.hasAccess(member.id) && (
        <Box mt={"30px"}>
          <Button
            ml={"10px"}
            onClick={() => navigate(`/member/edit/${member.id}`)}
            colorScheme={"green"}
          >
            ์ˆ˜์ •
          </Button>
          <Button ml={"10px"} onClick={onOpen} colorScheme={"red"}>
            ํƒˆํ‡ด
          </Button>
        </Box>
      )}
  • member์˜ id๊ฐ€ ํ•ด๋‹น ๊ณ„์ •์˜ id๊ฐ€ ๋งž์„ ๋•Œ๋งŒ ์ •๋ณด ๋ณด๊ธฐ์—๊ฒŒ๋งŒ ์ˆ˜์ •/ํƒˆํ‡ด ๋ฒ„ํŠผ ๋ณด์ด๊ธฐ

๐Ÿ’Ÿ ๋‹‰๋„ค์ž„ ์ˆ˜์ • ํ›„ navbar์— ๋ฐ˜์˜


MemberController.java

    @PutMapping("modify")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity modify(@RequestBody Member member, Authentication authentication) {
        if (service.hasAccessModify(member, authentication)) {
            Map<String, Object> result = service.modify(member, authentication);
            return ResponseEntity.ok(result);
        } else {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
    }
  • ์ˆ˜์ • ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด ์ˆ˜์ • ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ˆ˜์ •๋œ ํšŒ์› ์ •๋ณด์™€ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ result์— ๋‹ด์•„ ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

MemberService.java

   public Map<String, Object> modify(Member member, Authentication authentication) {
        ~~~~

        String token = "";

        Jwt jwt = (Jwt) authentication.getPrincipal();
        Map<String, Object> claims = jwt.getClaims();
        JwtClaimsSet.Builder jwtClaimsSetBuilder = JwtClaimsSet.builder();
        claims.forEach(jwtClaimsSetBuilder::claim);
        jwtClaimsSetBuilder.claim("nickName", member.getNickName());

        JwtClaimsSet jwtClaimsSet = jwtClaimsSetBuilder.build();
        token = jwtEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet)).getTokenValue();
        return Map.of("token", token);
    }
  • ํšŒ์› ์ •๋ณด ์ˆ˜์ •์ด ์™„๋ฃŒ๋œ ํ›„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ํ† ํฐ์„ Map์œผ๋กœ ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  1. Jwt jwt = (Jwt) authentication.getPrincipal();: ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด JWT๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  2. Map<String, Object> claims = jwt.getClaims();: JWT์—์„œ ํด๋ ˆ์ž„(claim) ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ํด๋ ˆ์ž„์€ ํ† ํฐ์— ์ €์žฅ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
  3. JwtClaimsSet.Builder jwtClaimsSetBuilder = JwtClaimsSet.builder();: JWT ํด๋ ˆ์ž„ ์…‹์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๋นŒ๋”๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
  4. claims.forEach(jwtClaimsSetBuilder::claim);: ๊ฐ€์ ธ์˜จ ํด๋ ˆ์ž„ ์ •๋ณด๋ฅผ ๋ฐ˜๋ณตํ•˜์—ฌ ํด๋ ˆ์ž„ ๋นŒ๋”์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  5. jwtClaimsSetBuilder.claim("nickName", member.getNickName());: ํšŒ์› ์ •๋ณด ์ค‘ ๋‹‰๋„ค์ž„์„ ์ƒˆ๋กœ์šด ํด๋ ˆ์ž„์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  6. JwtClaimsSet jwtClaimsSet = jwtClaimsSetBuilder.build();: ํด๋ ˆ์ž„ ๋นŒ๋”๋กœ๋ถ€ํ„ฐ ์ตœ์ข…์ ์ธ JWT ํด๋ ˆ์ž„ ์…‹์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  7. token = jwtEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet)).getTokenValue();: ์ƒ์„ฑํ•œ ํด๋ ˆ์ž„ ์…‹์„ ์ด์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด ํ† ํฐ์„ ์ธ์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ํ† ํฐ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์™€ ๋ณ€์ˆ˜์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  8. return Map.of("token", token);: ์ƒ์„ฑ๋œ ํ† ํฐ์„ "token" ํ‚ค์™€ ํ•จ๊ป˜ ๋งต ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ† ํฐ์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ƒˆ๋กœ์šด ์ธ์ฆ์— ์‚ฌ์šฉ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

MemberEdit.jsx(React)

function handleClickSave() {
    axios
      .put("/api/member/modify", { ...member, oldPassword })
      .then((res) => {
        toast({
          status: "success",
          description: "ํšŒ์› ์ •๋ณด๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
          position: "top",
        });
        account.login(res.data.token);
        navigate(`/member/${id}`);
      })
      ~~~
  }
  • account.login(res.data.token) : ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋ฉด ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ƒˆ๋กœ์šด ํ† ํฐ์„ ๋ฐ›์œผ๋ฉด ํ•ด๋‹น ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธํ•˜๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋Š” ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ ์„œ๋น„์Šค๋ฅผ ๊ณ„์† ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
profile
๊ฐœ๋ฐœ์ž ์ค€๋น„์ƒ~

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