개인 프로젝트/블로그

댓글에 유저 정보를 담아보자 feat.@AuthenticationPrincipal

공주맛밤 2022. 8. 2. 21:14

새로 알게된 @AuthenticationPrincipal 덕분에 앞서 이전 코드들이 넓게 넓게 변화한부분들이 있다.  @AuthenticationPrincipal 어노테이션은 간단히 말해서 authentication객체에 접근할 수 있는 어노테이션이다. 즉, 필터에서 인증을 통해 authentication객체를 담았다면(나는 jwt인증을 통해서 담아주었다. 밑 코드 참고) 해당 어노테이션을 통해 객체에 접근하여 원하는 데이터를 사용할 수 있다.

      -생략-
if (username != null) { //유효한 jwt토큰이 들어왔다면
                System.out.println("jwt토큰이 유효함");

                User userEntity = userRepository.findByUsername(username);

                PrincipalDetails principalDetails = new PrincipalDetails(userEntity);

                //jwt 토큰 서명에 근거해서 만들어진 authentication 객체
                Authentication authentication =
                        new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities()); //RoleType지정에 필요함

                //강제로 시큐리티 세션에 접근하여 authentication객체를 저장.
                SecurityContextHolder.getContext().setAuthentication(authentication);

            }
            -생략-

<결과: admin으로 로그인 한 경우>

게시글, 모든 댓글에 대해서 삭제할 수 있음

<결과2: user계정으로 로그인 한 경우>

본인이 작성한 댓글만 수정 삭제 가능한 모습
다른 유저계정으로 로그인한 모습


<SecurityConfig> : 접근가능한 권한 설정 조금 수정

.authorizeRequests(authorize->authorize
                        .antMatchers("/admin/**", "/admin/**", "/admin/**")
                        .access("hasRole('ROLE_TNUT')")
                        .antMatchers("/user/**")
                        .access("hasRole('ROLE_USER') or hasRole('ROLE_TNUT')")
                        .antMatchers("/authority")
                        .access("hasRole('ROLE_USER') or hasRole('ROLE_TNUT')")
                        .anyRequest().permitAll())

<AdminReplyApiController>

package tnut.blogback.controller.api.admin;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import tnut.blogback.service.ReplyService;

@RequiredArgsConstructor
@RestController
public class AdminReplyApiController {

    private ReplyService replyService;

    @Autowired
    public AdminReplyApiController(ReplyService replyService) {
        this.replyService = replyService;
    }

    @DeleteMapping("/admin/api/reply/{id}/delete") // 관리자 댓글 삭제
    public String replyDelete (@PathVariable Long id) {
        return replyService.replyDelete(id);
    }
}

<UserReplyApiController>: 시작에서 설명한 @AuthenticationPrincipal로 유저 정보를 댓글에 담아줌, 댓글 수정, 삭제는 왜 필요하냐? -> 유효한 accessToken + localStorage에 담긴 username을 본인이 아닌 다른 사람의 이름으로 담아서 보내면 요청이 승인됨 따라서 accessToken에 담긴 유저 정보와 댓글의 유저정보를 비교해서 다른 사람이 요청한 내 댓글에 대한 삭제, 수정을 막을 수 있음

package tnut.blogback.controller.api.user;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import tnut.blogback.config.auth.PrincipalDetails;
import tnut.blogback.dto.replyDTO.ReplySaveRequestDto;
import tnut.blogback.dto.replyDTO.ReplyUpdateDto;
import tnut.blogback.dto.replyDTO.RereplySaveRequestDto;
import tnut.blogback.dto.ResponseDto;
import tnut.blogback.service.ReplyService;

@RequiredArgsConstructor
@RestController
public class UserReplyApiController {

    private ReplyService replyService;

    @Autowired
    public UserReplyApiController(ReplyService replyService) {
        this.replyService = replyService;
    }

    @PostMapping("/user/api/reply/save") //댓글 내용 받아서 저장
    public ResponseDto<?> replySave (@RequestBody ReplySaveRequestDto replySaveRequestDto, @AuthenticationPrincipal PrincipalDetails principal) {
        return new ResponseDto<>(HttpStatus.OK.value(), replyService.replySave(replySaveRequestDto, principal.getUser()));
    }

    @PostMapping("/user/api/reReply/save") //대댓글 내용 받아서 저장
    public ResponseDto<?> reReplySave (@RequestBody RereplySaveRequestDto reReplySaveRequestDto, @AuthenticationPrincipal PrincipalDetails principal) {
        return new ResponseDto<>(HttpStatus.OK.value(), replyService.reReplySave(reReplySaveRequestDto, principal.getUser()));
    }

    @DeleteMapping("/user/api/reply/{id}/delete") //댓글 삭제
    public String replyDelete (@PathVariable Long id, @AuthenticationPrincipal PrincipalDetails principal) {
        return replyService.replyDelete(id, principal.getUser());
    }

    @PutMapping("/user/api/reply/{id}/update") // 댓글 수정
    public ResponseDto<?> replyUpdate (@RequestBody ReplyUpdateDto replyUpdateDto, @AuthenticationPrincipal PrincipalDetails principal) {
        return new ResponseDto<>(HttpStatus.OK.value(), replyService.replyUpdate(replyUpdateDto, principal.getUser()));
    }
}

<AuthorityController> : react에서 admin과 user에 따라 삭제나, 수정 버튼을 렌더링 하도록 해줘야 해서 authority와 username을 localStorage에 담아서 적용할 때 필요함

package tnut.blogback.controller.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import tnut.blogback.config.auth.PrincipalDetails;
import tnut.blogback.dto.ResponseDto;
import tnut.blogback.model.User;
import tnut.blogback.repository.UserRepository;

import java.util.HashMap;
import java.util.Map;

@RequiredArgsConstructor
@RestController
public class AuthorityController {

    private UserRepository userRepository;

    @Autowired
    public AuthorityController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/authority")
    public ResponseDto<?> authority(@AuthenticationPrincipal PrincipalDetails principal) {

        User user = userRepository.findByUsername(principal.getUser().getUsername());

        Map<String, String> userAuthority = new HashMap<>();
        userAuthority.put("role", user.getRoleType().toString());
        userAuthority.put("username", user.getUsername());

        return new ResponseDto<>(HttpStatus.OK.value(), userAuthority);
    }

}

<User 와 Reply의 관계 설정>

<Reply>
@ManyToOne
    @JsonIgnoreProperties(value = {"password", "email", "provider", "providerId", "roleType", "createDate"})
    @JoinColumn(name = "user_id")
    private User user;
----------------------------------------------------------------------------
<User>
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    @OrderBy("id desc") //최신 댓글 순으로 정렬
    @JsonIgnore //무한참조 방지
    private List<Reply> replies = new ArrayList<>();

<ReplyRepository> : 위에서 요청한 user정보와 댓글의 user정보를 비교한다는 얘기는 사실, principal의 유저와 댓글의 id를 함께 가진 댓글을 찾아낸다는 소리였음, 올바른 요청이라면 당연히 찾아질 것이고, 다른 사람이 다른 유저의 댓글을 삭제 요청한다면 찾을 수 없을 것임

package tnut.blogback.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import tnut.blogback.model.Reply;
import tnut.blogback.model.User;

import java.util.Optional;

public interface ReplyRepository extends JpaRepository<Reply, Long> {
    Optional<Reply> findByIdAndUser(Long id, User user);
}

<ReplyService> : 삭제 시 user를 굳이 비교할 필요가 없는 관리자용 삭제 로직이 추가되고, user비교로 수정됨

@Transactional
    public String replyDelete(Long id, User user) { //유저 댓글 삭제 DeleteMapping
        Reply replyEntity = replyRepository.findByIdAndUser(id, user)
                .orElseThrow(() -> new IllegalArgumentException("이미 삭제되거나 권한이 없는 이용자 입니다."));

        if (!replyEntity.getSubReplies().isEmpty()) { //자식이 있을경우
            replyEntity.setContent(null); //내용을 비우고
            replyEntity.setDeletable(true); //삭제 가능상태로 업데이트
        } else { //자식이 없을 경우
            replyRepository.deleteById(id); //삭제 대상은 삭제하고
            replyRepository.flush(); //플러쉬 하지 않으면 삭제가 맨 마지막에 DB에서 처리 되기 때문에 부모댓글의 대댓글이 존재한 채로 다음 로직이 먼저 실행
            if (replyEntity.getParentReply() == null) {
                return "success delete!";
            } else {
                if (replyEntity.getParentReply().isDeletable()) {
                    replyDelete(replyEntity.getParentReply().getId(), replyEntity.getParentReply().getUser()); //부모에 대한 삭제도 진행
                }
            }
        }
        return "success delete!";
    }

    @Transactional
    public String replyDelete(Long id) { //관리자 댓글 삭제
        Reply replyEntity = replyRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("이미 삭제된 댓글입니다."));

        if (!replyEntity.getSubReplies().isEmpty()) {
            replyEntity.setContent(null);
            replyEntity.setDeletable(true);
        } else {
            replyRepository.deleteById(id);
            replyRepository.flush();
            if (replyEntity.getParentReply() == null) {
                return "success delete!";
            } else {
                if (replyEntity.getParentReply().isDeletable()) {
                    replyDelete(replyEntity.getParentReply().getId());
                }
            }
        }
        return "success delete!";
    }

    @Transactional
    public Reply replyUpdate(ReplyUpdateDto replyUpdateDto, User user) {
        Reply replyEntity = replyRepository.findByIdAndUser(replyUpdateDto.getId(), user)
                .orElseThrow(() -> new IllegalArgumentException("이미 삭제된 댓글이거나 권한이 없습니다."));

        replyEntity.setContent(replyUpdateDto.getContent());

        return replyEntity;
    }
728x90
반응형