오... 쓰고 나서 보니 오늘이 13일의 금요일이다.
어제 잠을 많이 잤더니 몸 상태가 훨씬 나아진 것 같다.
어제 JWT 로그인 강의를 듣다 말았는데 JWT로그인 부분은 어제 TIL에 합쳐서 작성했다.
하샤드 수 = 자기 자신의 자릿수를 더한 값으로 자기 자신이 나누어 떨어지는 수
class Solution {
public boolean solution(int x) {
int sum = 0;
for(int xx = x; xx>0; xx/=10){
sum += xx%10;
}
if(x%sum == 0) return true;
else return false;
}
}
문제를 이해하고 무작정 코드를 치다가 인수로 받은 x를 그대로 for문 변수로 사용하려다가
if문에서 자기 자신을 나눠야 함에 x값을 보존해야한다는 것을 알게되었다.
코드를 치기 전에 생각하는 시간을 좀 더 여유롭게 둬야겠다.
for문 조건절의 변수를 x로 설정해서 무한 반복이 되었다.
다음에는 명확히 구분되는 변수명을 사용해야겠다.
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
public class UserDetailsImpl implements UserDetails {
// ...
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
/* "ROLE_ADMIN" 대신
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
*/
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(adminAuthority);
return authorities;
}
}
ROLE_ 로 시작해야 한다.
그런데 Spring Security는 이걸 바꾸는 설정을 공식적으로 제공한다.
✔️ 방법: Role Prefix 설정 변경
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
// prefix 없애기
// return new GrantedAuthorityDefaults("")
// prefix를 LOL로 변경
return new GrantedAuthorityDefaults("LOL_");
}
설정시 내부 동작:
hasRole("ADMIN")
→ "LOL_ADMIN" 으로 변환됨
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableMethodSecurity(securedEnabled = true) // @Secured 애너테이션 활성화
public class WebSecurityConfig {
@Secured(UserRoleEnum.Authority.ADMIN) // 관리자용
@GetMapping("/products/secured")
public String getProductsByAdmin(@AuthenticationPrincipal UserDetailsImpl userDetails) {
System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
for (GrantedAuthority authority : userDetails.getAuthorities()) {
System.out.println("authority.getAuthority() = " + authority.getAuthority());
}
return "redirect:/";
}
public class WebSecurityConfig {
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 접근 불가 페이지
http.exceptionHandling((exceptionHandling) ->
exceptionHandling
// "접근 불가" 페이지 URL 설정
.accessDeniedPage("/forbidden.html")
);
return http.build();
}
}
Validation(검증) 은 클라이언트가 보낸 데이터가 정상적인 값인지 자동으로 검사하는 기능이다.
✔ 목적
잘못된 데이터 DB 저장 방지
서버 로직 안전성 확보
프론트 검증을 믿지 않고 서버에서 재검증
예:
회원가입 시 이메일 형식 검사
게시글 제목 길이 제한
나이 값 음수 금지
DTO 필드에 어노테이션 붙여서 검증한다.
public class UserDto {
@NotBlank
private String name;
@Email
private String email;
@Min(0)
private int age;
}
// Valid
@PostMapping("/users")
public String create(@Valid @RequestBody UserDto dto) { // 또는 @Validated
return "ok";
}
// Validated 그룹 검증
@PostMapping("/users")
public void create(@Validated(CreateGroup.class) UserDto dto) {
}
// Validated 메서드 파라미터 검증
@Service
@Validated
public class UserService {
public void register(@NotBlank String name) { }
}
그렇다면 persist 시점에도 자동 검증이 일어날 수 있게 엔티티 필드에 어노테이션을 붙여서 검증해도 되지 않을까? 라는 생각을 했다.
Spring Framework 는 Bean Validation(구현체: Hibernate Validator)을 어디서든 실행할 수 있기 때문에 JPA 엔티티에 이렇게 붙여도 동작은 한다.
하지만 엔티티(Entity)에 Validation 어노테이션 붙이는 것은 하지 않는다.
왜냐하면 엔티티 검증은 Repository / flush 시점에서야 이루어지고 계층 책임이 뒤틀리기 때문이다.
게다가 DTO에서 검증을 할 때와 달리 API별 다른 규칙을 정해줄 수 없다.
에러 메시지 직접 제어 가능
화면 리턴 시 많이 사용
@PostMapping("/users")
public String create(@Valid UserDto dto, BindingResult result) {
if (result.hasErrors()) {
return "error";
}
return "ok";
}
복잡한 로직 검증할 때 사용
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return UserDto.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
UserDto dto = (UserDto) target;
if (!dto.getPassword().equals(dto.getConfirm())) {
errors.rejectValue("confirm", "password.mismatch");
}
}
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handle(Exception e) {
return ResponseEntity.badRequest().body("validation error");
}
implementation 'org.springframework.boot:spring-boot-starter-validation'
package com.sparta.springauth.dto;
import jakarta.validation.constraints.*;
import lombok.Getter;
@Getter
public class ProductRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Positive(message = "양수만 가능합니다.")
private int price;
@Negative(message = "음수만 가능합니다.")
private int discount;
@Size(min=2, max=10)
private String link;
@Max(10)
private int max;
@Min(2)
private int min;
}
@PostMapping("/validation")
@ResponseBody
public ProductRequestDto testValid(@RequestBody @Valid ProductRequestDto requestDto) {
return requestDto;
}
{
"name" : "Robbie",
"email" : "Robbie@gmail.com",
"price" : 1234,
"discount" : -1234,
"link" : "54321",
"max" : 10,
"min" : 2
}
도메인 : localhost
Authorization = Bearer...
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@Getter
@Setter
public class SignupRequestDto {
@NotBlank
private String username;
@NotBlank
private String password;
@Email
@NotBlank
private String email;
private boolean admin = false;
private String adminToken = "";
}
BindingResult 는 Spring MVC에서 폼 데이터 바인딩 + 검증 결과를 담아주는 객체다. (@Valid 로 검증한 결과(에러 정보)를 담는 통)
예외가 발생하면 파라미터로 받아온 BindingResult객체에 오류에 대한 정보가 담긴다.
참고로, BindingResult 파라미터는 반드시 @Valid 바로 뒤에 와야 한다.
bindingResult.getFieldErrors()메서드로 발생한 오류들에 대한 정보가 담긴 List<FieldError>를 가져올 수 있다.
@PostMapping("/user/signup")
public String signup(@Valid SignupRequestDto requestDto, BindingResult bindingResult) {
// Validation 예외처리
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
if (fieldErrors.size() > 0) { // bindingResult.hasErrors()
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
}
return "redirect:/api/user/signup";
}
userService.signup(requestDto);
return "redirect:/api/user/login-page";
}
@Pattern(regexp = "^(.+)@(.+)$") // 문자열 중간에 @포함
private String needAtStr
@Pattern(regexp = "^[A-Za-Z0-9+_.-]+@(.+)") // @ 앞에 허용되는 문자 지정
private String letterFrontAt
@Pattern(regexp = "^[a-zA-Z0-9_!#$%'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"
@NotBlank
private String email;