[SpringBoot] 스프링부트 로그인 구현- (5) 회원가입, 권한 검증

01. 회원가입 API 생성

1. util 패키지 생성


2. 간단한 유틸리티 메소드를 만들기 위해 SecurityUtil 클래스 생성

util > SecurityUtil.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Optional;

public class SecurityUtil {

    private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    private SecurityUtil() {
    }

    /* SecurityContext의 Authentication 객체를 이용해 username을 리턴 */
    public static Optional<String> getCurrentUsername() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null) {
            logger.debug("Security Context에 인증 정보가 없습니다.");
            return Optional.empty();
        }

        String username = null;
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
            username = springSecurityUser.getUsername();
        } else if (authentication.getPrincipal() instanceof String) {
            username = (String) authentication.getPrincipal();
        }

        return Optional.ofNullable(username);
    }
}

SecurityContext에 Authentication 객체가 저장되는 시점은 JwtFilter의 doFilter 메소드에서 Request가 들어올 때이다.


3. UserService 클래스 작성

회원가입, 유저정보조회 등의 메소드를 만들기 위함

service > UserService.java

import java.util.Collections;
import java.util.Optional;
import com.example.tutorial.dto.UserDto;
import com.example.tutorial.entity.Authority;
import com.example.tutorial.entity.User;
import com.example.tutorial.repository.UserRepository;
import com.example.tutorial.util.SecurityUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    // UserRepository, PasswordEncoder를 주입 받음
    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    /* 회원 가입 로직 수행 */
    @Transactional
    public User signup(UserDto userDto) {
        // 파라미터로 받은 UserDto의 username을 기준으로 해서
        // 이미 DB에 존재하는지 찾음 
        if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
            throw new RuntimeException("이미 가입되어 있는 유저입니다.");
        }

        // DB에 존재하지 않으면 Authority와 
        Authority authority = Authority.builder()
                .authorityName("ROLE_USER") // (*) signup 메소드를 통해 가입한 회원은 USER ROLE을 가짐
                .build();                   // +) data.sql에서 자동생성되는 admin 계정은
                                            // USER, ADMIN ROLE을 가지고 있었음

        // User 정보를 생성해서
        User user = User.builder()
                .username(userDto.getUsername())
                .password(passwordEncoder.encode(userDto.getPassword()))
                .nickname(userDto.getNickname())
                .authorities(Collections.singleton(authority))
                .activated(true)
                .build();

        // UserRepository의 save 메소드를 통해 DB에 정보를 저장함
        return userRepository.save(user);
    }

    /* 어떠한 username을 이든 username에 해당하는 유저 객체와 권한정보 가져옴 */
    @Transactional(readOnly = true)
    public Optional<User> getUserWithAuthorities(String username) {
        return userRepository.findOneWithAuthoritiesByUsername(username);
    }

    /* 현재 SecurityContext에 저장된 username에 해당하는 유저 객체와 권한정보만 가져옴 */
    @Transactional(readOnly = true)
    public Optional<User> getMyUserWithAuthorities() {
        return SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername);
    }
}


02. 권한검증 확인

1. UserService의 메소드를 호출할 UserController 작성

controller > UserController.java 생성

import com.example.tutorial.dto.UserDto;
import com.example.tutorial.entity.User;
import com.example.tutorial.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;


@RestController
@RequestMapping("/api")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping("/signup")
    public ResponseEntity<User> signup(
            @Valid @RequestBody UserDto userDto
    ) {
        return ResponseEntity.ok(userService.signup(userDto));
    }

    @GetMapping("/user")
    @PreAuthorize("hasAnyRole('USER','ADMIN')") // USER, ADMIN 권한 모두 허용
    public ResponseEntity<User> getMyUserInfo() {
        return ResponseEntity.ok(userService.getMyUserWithAuthorities().get());
    }

    @GetMapping("/user/{username}")
    @PreAuthorize("hasAnyRole('ADMIN')") // ADMIN 권한만 호출 가능
    public ResponseEntity<User> getUserInfo(@PathVariable String username) {
        return ResponseEntity.ok(userService.getUserWithAuthorities(username).get());
    }
}

2. 포스트맨, H2 콘솔을 이용한 3개의 API 테스트

  • signup Request

    http://localhost:8080/api/signup으로 POST 요청을 보내면

    회원가입 API에 대한 응답이 정상적으로 리턴된다.

    가입정보를 H2 콘솔에서 확인해보면 cgh 유저가 잘 등록된 것을 볼 수 있다.

    cgh 유저의 권한이 USER로 잘 설정되었다. admin 유저는 USER, ADMIN 두 가지 권한을 소유하고 있다.


  • getUserInfo Request

    먼저 Authorization 탭에서 token 정보를 넣어준다.
    jwt_tutorial_token은 authenticate Request의 Tests 탭에서 response를 파싱해서 토큰 정보를 담아놓았던 변수이다. 이 변수를 다른 Request에서 사용할 수 있다.

    http://localhost:8080/api/user/cgh로 GET 요청을 보내면 admin 계정의 토큰으로 cgh 유저의 정보를 가져올 수 있음을 확인할 수 있다.

    이번에는 cgh 유저의 토큰으로 같은 API를 재호출 해보자.


    http://localhost:8080/api/authenticate로 다음과 같이 POST 요청을 보내 cgh 유저의 토큰을 생성한다.

    다시 http://localhost:8080/api/user/cgh로 GET 요청을 보내면 다음과 같이 403 Forbidden 에러가 리턴된다.
    cgh 유저의 토큰으로는 이 API를 호출할 수 있는 권한이 없기 때문이다.

    +) 403 Forbidden 에러가 리턴됨으로써 JwtAccessDeniedHandler가 잘 작동하였음을 알 수 있다.


  • getMyUserInfo Request

    USER 권한을 허용해줬던 API를 똑같이 cgh 유저의 토큰으로 호출해볼 것이다.


    http://localhost:8080/api/user로 GET 요청을 보내면 다음과 같이 호출이 잘 되는 것을 확인할 수 있다.

좋은 웹페이지 즐겨찾기