티스토리 뷰
Spring Advanced : FootBall Club example REST 작성 - 2. Skill Entity CRUD 구현 및 전역 에러 처리, 기본 인증 로직 등록
Korean Eagle 2020. 7. 5. 21:250. 이 포스트는 이전 Fooball club예제를 REST로 다시 작성하는 시리즈의 일부이다.
1. 적용된 테크닉
1-1 전역에러 처리 클래스 설정 @ControllerAdvice
1-2 Rest Controller @Validated, @Valid 처리
1-3 Optional 처리하기 map - orElseThrow/orElseGet 처리
2. 이 포스트에서 작성할 내용
2-1 인증관련 연결
2-2 전역 예외처리 클래스 정의
2-3 Skill CRUD 구현하기
3. 인증 관련 연결하기
3-1 SecurityConfig
3-1-1 기본적인 인증을 UserDetailsService를 바로 이용하는 방식으로 정의
3-1-2 PasswordEncoder를 BCrypt방식으로 적용
3-1-3 csrf disable하기 - 현재 모든 경로를 허용하기로 하더라도 post로 전송시 403 권한없음이 발생한다.
3-1-3-0 disable처리를 하면 정상적으로 메시지 전송이 가능해 진다.
3-1-3-1 서버에서 csrf를 기다리기 때문인데, 강제로 아래처럼 disable를 해주거나 아니면 post에 csrf를 보내야한다.
package pe.pilseong.footballserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll()
.and()
.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3-2 UserDetailsService 구현하기
3-2-1 위의 보안 설정에서 UserDetailsService를 사용하고 있기 때문에 Service구현하여 제공해야 한다.
3-2-2 코드는 필수 메소드 loadUserByUsername을 구현하고 있고 UserRepository를 사용하고 있다.
3-2-3 유저를 찾을 수 없는 경우, UserDetailsService의 스펙에 맞게 예외를 발생시키고 있다.
package pe.pilseong.footballserver.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import pe.pilseong.footballserver.repository.UserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails userDetails = this.userRepository.findByUsername(username);
if (userDetails != null) {
return userDetails;
}
throw new UsernameNotFoundException("User '" + username + "' not found");
}
}
3-3 UserRepository 작성하기
3-3-1 UserDetailsService에서 username으로 검색 요구하기 때문에 메소드 정의를 추가했다. 구현은 JPA가 해준다.
package pe.pilseong.footballserver.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pe.pilseong.footballserver.model.User;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
4. Entity Repository 작성하기
4-1 다 동일한 방식이어서 하나 코드로 붙여놓았다. 모두 별도의 클래스를 만들어야 한다.
package pe.pilseong.footballserver.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pe.pilseong.footballserver.model.Skill;
public interface SkillRepository extends JpaRepository<Skill, Long> {
}
package pe.pilseong.footballserver.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pe.pilseong.footballserver.model.Player;
public interface PlayerRepository extends JpaRepository<Player, Long> {
}
package pe.pilseong.footballserver.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pe.pilseong.footballserver.model.Team;
public interface TeamRepository extends JpaRepository<Team, Long> {
}
package pe.pilseong.footballserver.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pe.pilseong.footballserver.model.Role;
public interface RoleRepository extends JpaRepository<Role, Long> {
}
5. Skill REST CRUD 작성하기
5-0 Optional을 사용한 깔끔한 코딩방식을 선택하였고, 모든 예외사항은 전역 예외핸들러에서 처리하였다.
5-1 (GET) findOne메소드@Min 검증자를 사용하여 최소 숫자값 1이상의 값을 입력해야 한다.
5-1-1 @Min을 사용하려면 @Validated라는 annotation을 클래스 레벨에 붙여야 한다.
5-1-1-1 검증을 통과하지 못하면 ConstraintViolationException 이 발생한다.
5-1-1-2 데이터베이스에 없는 id가 들어오면 SkillNotFoundException을 발생시킨다. 사용자 정의 예외이다.
5-2 (POST) saveSkill 메소드는 @Valid를 통해 Skill로 변환되는 객체를 검증하고 있다.
5-2-1 Type의 enum에 없는 값이 들어 올 경우 HttpMessageNotReadableException이 발생한다.
5-2-2 필수항목이 빠져 있는 경우 MethodArgumentNotValidException이 발생한다.
5-3 (PATCH) updateSkill 메소드에는 Map으로 키:값 쌍을 받아 있는 정보만 업데이트한다
5-3-1 업데이트할 Type enum이 없는 값인 경우 illegalArgumentExceptionHandler이 발생한다.
5-4 (PUT) saveOrUpdate는 saveSkill와 비슷하게 예외가 발생한다.
5-4-1 필수항목이 빠진 patch는 MethodArgumentNotValidException 발생
5-4-2 Type의 없는 값이 들어오면 HttpMessageNotReadableException 발생
package pe.pilseong.footballserver.controller;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.footballserver.exception.SkillNotFoundException;
import pe.pilseong.footballserver.model.Skill;
import pe.pilseong.footballserver.model.Skill.Type;
import pe.pilseong.footballserver.repository.SkillRepository;
@RestController
@RequestMapping("/api/skills")
@Validated
@Slf4j
public class SkillController {
@Autowired
private SkillRepository skillRepository;
@GetMapping
@ResponseStatus(code = HttpStatus.OK)
public List<Skill> findAll() {
return this.skillRepository.findAll();
}
@GetMapping("/{id}")
public Skill findOne(@PathVariable @Min(1) Long id) {
return this.skillRepository.findById(id)
.orElseThrow(()-> new SkillNotFoundException(id));
}
@PostMapping
public Skill saveSkill(@Valid @RequestBody Skill skill) {
log.info(skill.toString());
return this.skillRepository.save(skill);
}
@DeleteMapping("/{id}")
@ResponseStatus(code = HttpStatus.NO_CONTENT)
public void deleteSkillById(@PathVariable @Min(1) Long id) {
this.skillRepository.deleteById(id);
}
@PutMapping("/{id}")
public Skill saveOrUpdate(@Valid @RequestBody Skill newSkill, @PathVariable Long id) {
return this.skillRepository.findById(id)
.map(skill-> {
skill.setName(newSkill.getName());
skill.setType(newSkill.getType());
return this.skillRepository.save(skill);
})
.orElseGet(()-> {
newSkill.setId(id);
return this.skillRepository.save(newSkill);
});
}
@PatchMapping("/{id}")
public Skill updateSkill(@RequestBody Map<String, String> update, @PathVariable Long id) {
return this.skillRepository.findById(id)
.map(skill-> {
if (update.containsKey("name")) {
skill.setName(update.get("name"));
}
if (update.containsKey("type")) {
skill.setType(Type.valueOf(update.get("type")));
}
return this.skillRepository.save(skill);
})
.orElseGet(() -> {
throw new SkillNotFoundException(id);
});
}
}
5-5 검증을 위해 Skill 클래스를 수정
5-5-1 name, type 둘 다 필수로 지정하였다.
5-5-2 type의 enum이기 때문에 NotEmpty나 NotBlank가 지정되지 않는다. NotNull로 충분하다.
package pe.pilseong.footballserver.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Entity
@Table(name = "skills")
@Data
@EqualsAndHashCode(callSuper=false)
public class Skill extends AbstractEntity {
@Column
@NotEmpty(message = "name is requried")
private String name;
@Column
@Enumerated(EnumType.STRING)
@NotNull(message = "type is required")
private Type type;
public static enum Type {
SHOOTING, PASSING, DEFENSING, PHYSICAL;
}
}
6. 위의 다양한 예외를 처리하기 위한 전역 예외처리
6-1 SkillController에서 발생한 다양한 예외를 여기에서 처리한다.
6-2 ResponseEntityExceptionHandler는 기본적인 예외를 전부 처리하고 있다.
6-2-1 필요한 경우 특정예외를 Override할 수 있다.
6-2-2 없는 경우는 임의 메소드로 처리해야 한다.
6-2-3 처리가 동일한 경우는 내부 메소드로 옮겼다.
6-2-4 클라이언트로 예외정보를 전달하는 클래스로 ExceptionResponse를 사용하고 있다.
package pe.pilseong.footballserver.exception;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import lombok.extern.slf4j.Slf4j;
@ControllerAdvice
@Slf4j
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Custom exception
@ExceptionHandler(value = SkillNotFoundException.class)
public ResponseEntity<Map<String, String>> skillNotFoundException(SkillNotFoundException ex,
WebRequest reqeust) {
log.info("SkillNotFoundException is concerned");
return notFoundHandler(ex);
}
// for DeleteMapping
// updateSkill when not a valid enum type
@ExceptionHandler(value = EmptyResultDataAccessException.class)
public ResponseEntity<Map<String, String>> deleteTargetNotFoundHandler(EmptyResultDataAccessException ex,
WebRequest reqeust) {
log.info("EmptyResultDataAccessException is concerned");
return notFoundHandler(ex);
}
// to check valid enum type when udpating
@ExceptionHandler(value = IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> illegalArgumentExceptionHandler(IllegalArgumentException ex,
WebRequest reqeust) {
log.info("illegalArgumentExceptionHandler is concerned");
Map<String, String> body = new LinkedHashMap<>();
body.put("timestamp", new Date().toString());
body.put("status", HttpStatus.BAD_REQUEST.toString());
body.put("errors", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
private ResponseEntity<Map<String, String>> notFoundHandler(RuntimeException ex) {
Map<String, String> body = new LinkedHashMap<>();
body.put("timestamp", new Date().toString());
body.put("status", HttpStatus.NOT_FOUND.toString());
body.put("errors", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
// @Validated @Min check - to check index is at least larger than 0
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex,
WebRequest request) {
log.info("ConstraintViolationException is concerned");
List<String> errors = new ArrayList<String>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(
violation.getRootBeanClass().getName() + " " + violation.getPropertyPath() + ": "
+ violation.getMessage());
}
ExceptionResponse apiError = new ExceptionResponse(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(),
new Date().toString(), errors);
return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
// to check required fields are missing
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
log.info("handleMethodArgumentNotValid method is invoked");
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date().toString());
body.put("status", status.value());
List<String> errors = ex.getBindingResult().getFieldErrors().stream().map(error -> error.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
// @Valid check to avoid having invalid enum type when posting
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
log.info("handleHttpMessageNotReadable method is invoked");
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date().toString());
body.put("status", status.value());
String error = ex.getMostSpecificCause().getMessage();
body.put("error", error);
return new ResponseEntity<>(body, headers, status);
}
}
6-3 에러를 담아 전달하는 클래스로 ExceptionResponse로 작성하였다.
package pe.pilseong.footballserver.exception;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpStatus;
import lombok.Data;
@Data
public class ExceptionResponse {
private HttpStatus status;
private String message;
private String timestamp;
private List<String> errors;
public ExceptionResponse(HttpStatus status, String message, String timestamp, List<String> errors) {
super();
this.status = status;
this.message = message;
this.timestamp = timestamp;
this.errors = errors;
}
public ExceptionResponse(HttpStatus status, String message, String timestamp, String error) {
super();
this.status = status;
this.message = message;
this.timestamp = timestamp;
errors = Arrays.asList(error);
}
}
6-4 Skill을 데이터베이스에서 찾지 못할 경우 발생시키는 SkillNotFoundException 클래스 작성
package pe.pilseong.footballserver.exception;
public class SkillNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public SkillNotFoundException(Long id) {
super("Skill id :: '" + id + "' not found" );
}
}
'Demos > Football Club' 카테고리의 다른 글
Spring Advanced : FootBall Club example REST 작성 - 4. JWT 회원가입 작성하기 (0) | 2020.07.14 |
---|---|
Spring Advanced : FootBall Club example REST 작성 - 3. JWT 보안토큰 설정 (0) | 2020.07.06 |
Spring Advanced : FootBall Club example REST 작성 - 1. 데이터베이스 작성 (0) | 2020.07.05 |
Spring Security : FootBall Club example에 Security 적용하기 (0) | 2020.07.01 |
Spring Boot : JPA를 이용한 FootBall Club example (0) | 2020.06.29 |
- Total
- Today
- Yesterday
- 도커 개발환경 참고
- AWS ARN 구조
- Immuability에 관한 설명
- 자바스크립트 멀티 비동기 함수 호출 참고
- WSDL 참고
- SOAP 컨슈머 참고
- MySql dump 사용법
- AWS Lambda with Addon
- NFC 드라이버 linux 설치
- electron IPC
- mifare classic 강의
- go module 관련 상세한 정보
- C 메모리 찍어보기
- C++ Addon 마이그레이션
- JAX WS Header 관련 stackoverflow
- SOAP Custom Header 설정 참고
- SOAP Custom Header
- SOAP BindingProvider
- dispatcher 사용하여 설정
- vagrant kvm으로 사용하기
- git fork, pull request to the …
- vagrant libvirt bridge network
- python, js의 async, await의 차이
- go JSON struct 생성
- Netflix Kinesis 활용 분석
- docker credential problem
- private subnet에서 outbound IP 확…
- 안드로이드 coroutine
- kotlin with, apply, also 등
- 안드로이드 초기로딩이 안되는 경우
- navigation 데이터 보내기
- 레이스 컨디션 navController
- raylib
- Validation
- RestTemplate
- one-to-many
- 스프링부트
- hibernate
- 외부파일
- XML
- form
- jsp
- mapping
- spring boot
- WebMvc
- Spring
- crud
- Angular
- Many-To-Many
- one-to-one
- 하이버네이트
- 설정하기
- Rest
- MYSQL
- 설정
- Security
- 상속
- 로그인
- 자바
- 매핑
- Spring Security
- 스프링
- login