## 개인기록용
# Spring Boot JWT Tutorial 학습
https://github.com/jennie267/jwt-tutorial
[SpringBoot] JWT Tutorial 1 - JWT
[SpringBoot] JWT Tutorial 2 - 프로젝트 생성
[SpringBoot] JWT Tutorial 3 - Security 설정
[SpringBoot] JWT Tutorial 4 - Data 설정
[SpringBoot] JWT Tutorial 5 - JWT 코드, Security 설정 추가
[SpringBoot] JWT Tutorial 6 - DTO, Repository, 로그인
> [SpringBoot] JWT Tutorial 7 - 회원가입, 권한검증
- 회원가입 API 생성
- 허용권한이 다른 API들을 만들어서 권한검증 확인
0. 기타 추가사항
AuthorityDto.java 생성
package study.cherry.jwttutorial.dto;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthorityDto {
private String authorityName;
}
UserDto.java 아래 내용 추가
...
public class UserDto {
...
private Set<AuthorityDto> authorityDtoSet;
public static UserDto from(User user) {
if(user == null) return null;
return UserDto.builder()
.username(user.getUsername())
.nickname(user.getNickname())
.authorityDtoSet(user.getAuthorities().stream()
.map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())
.collect(Collectors.toSet()))
.build();
}
}
1. 유틸리티 메서드를 만들기 위해 SecurityUtil 클래스 생성
- util 패키지 추가
- getCurrentUsername 메서드 생성 : Security Context의 Authentication 객체를 이용해 username을 리턴해주는 유틸성 메서드
(Security Context에 Authentication 객체가 저장되는 시점은 JwtFilter의 doFilter메서드에서 Request가 들어올때 SecurityContext에 Authentication 객체가 저장됨)
SecurityUtil.java
package study.cherry.jwttutorial.util;
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() {
}
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);
}
}
2. 회원가입, 유저정보조회 등의 메서드를 만들기위해 UserService 클래스 생성
- UserRepository, PasswordEncoder 주입
- signup 메서드 : 회원가입 로직을 수행하는 메서드
username이 DB에 존재하지 않으면 Authority와 User 정보를 생성하여 UserRepository의 save를 통해 DB에 정보 저장
- signup 메서드를 통해 가입한 회원은 ROLE_USER라는 권한을 가지고 있고 data.sql에서 자동생성되는 admin 계정은 USER, ADMIN ROLE을 가지고 있음 -> 허용권한을 다르게 줬으니 이 차이를 가지고 권한검증 테스트
- getUserWithAuthorities : username을 기준으로 권한정보를 가져오는 메서드
- getMyUserWithAuthorities : SecurityContext에 저장된 username에 해당하는 정보만 가져옴
- 위 두 메서드의 허용권한을 다르게해서 테스트
UserService.java
package study.cherry.jwttutorial.service;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.cherry.jwttutorial.dto.UserDto;
import study.cherry.jwttutorial.entity.Authority;
import study.cherry.jwttutorial.entity.User;
import study.cherry.jwttutorial.repository.UserRepository;
import study.cherry.jwttutorial.util.SecurityUtil;
import java.util.Collections;
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public UserDto signup(UserDto userDto) {
if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
throw new RuntimeException("이미 가입되어 있는 유저입니다.");
}
Authority authority = Authority.builder()
.authorityName("ROLE_USER")
.build();
User user = User.builder()
.username(userDto.getUsername())
.password(passwordEncoder.encode(userDto.getPassword()))
.nickname(userDto.getNickname())
.authorities(Collections.singleton(authority))
.activated(true)
.build();
return UserDto.from(userRepository.save(user));
}
@Transactional(readOnly = true)
public UserDto getUserWithAuthorities(String username) {
return UserDto.from(userRepository.findOneWithAuthoritiesByUsername(username).orElse(null));
}
@Transactional(readOnly = true)
public UserDto getMyUserWithAuthorities() {
return UserDto.from(SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername).orElse(null));
}
}
3. UserService의 메서드들을 호출할 UserController 생성
- signup : 회원가입
@PreAuthorize : 권한별로 접근 제어 통제
- getMyUserInfo -> USER, ADMIN 두가지 권한 모두 허용
- getUserInfo -> ADMIN 권한만 허용
UserController.java
package study.cherry.jwttutorial.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import study.cherry.jwttutorial.dto.UserDto;
import study.cherry.jwttutorial.entity.User;
import study.cherry.jwttutorial.service.UserService;
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<UserDto> signup(@Valid @RequestBody UserDto userDto) {
return ResponseEntity.ok(userService.signup(userDto));
}
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public ResponseEntity<UserDto> getMyUserInfo() {
return ResponseEntity.ok(userService.getMyUserWithAuthorities());
}
@GetMapping("/user/{username}")
@PreAuthorize("hasAnyRole('ADMIN')")
public ResponseEntity<UserDto> getUserInfo(@PathVariable String username) {
return ResponseEntity.ok(userService.getUserWithAuthorities(username));
}
}
4. 테스트 (Postman, H2 Console)
4-1. 회원가입
* Postman
* h2 console (http://localhost:8080/h2-console)
4-2. admin 권한만 있는 api 호출
- admin 계정의 token으로 호출하니까 잘 조회됨 (postman에 response저장기능을 이용해서 담아뒀었음)
- cherry 계정의 token으로 호출시에 조회 안됨 (403 Forbidden)
4-3. User권한을 허용한 api를 cherry 계정의 token으로 조회
- cherry 권한으로도 조회가능한 api이므로 잘 조회됨
* 최종 구조
Reference
https://github.com/SilverNine/spring-boot-jwt-tutorial