2/13(금) Spring Security 권한 제어, Validation

dev_joo·2026년 2월 13일

오... 쓰고 나서 보니 오늘이 13일의 금요일이다.
어제 잠을 많이 잤더니 몸 상태가 훨씬 나아진 것 같다.
어제 JWT 로그인 강의를 듣다 말았는데 JWT로그인 부분은 어제 TIL에 합쳐서 작성했다.

코드 카타 2026.02.13

하샤드 수 = 자기 자신의 자릿수를 더한 값으로 자기 자신이 나누어 떨어지는 수

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로 설정해서 무한 반복이 되었다.
다음에는 명확히 구분되는 변수명을 사용해야겠다.

숙련주차 강의

Spring Security 권한 제어

SpringSecurity 권한 설정

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" 으로 변환됨

API 별 권한 제어

@Secured 사용하기

@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

Validation(검증) 은 클라이언트가 보낸 데이터가 정상적인 값인지 자동으로 검사하는 기능이다.

✔ 목적

잘못된 데이터 DB 저장 방지
서버 로직 안전성 확보
프론트 검증을 믿지 않고 서버에서 재검증

예:


회원가입 시 이메일 형식 검사
게시글 제목 길이 제한
나이 값 음수 금지

Validation 방식

① Annotation 기반 (가장 많이 사용)

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별 다른 규칙을 정해줄 수 없다.

② BindingResult 방식 (에러 직접 처리)

에러 메시지 직접 제어 가능
화면 리턴 시 많이 사용

@PostMapping("/users")
public String create(@Valid UserDto dto, BindingResult result) {
    if (result.hasErrors()) {
        return "error";
    }
    return "ok";
}

③ 커스텀 Validator

복잡한 로직 검증할 때 사용

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");
        }
    }
}

④ Exception 방식 처리 (REST API에서 많이 사용)

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handle(Exception e) {
    return ResponseEntity.badRequest().body("validation error");
}

Annotation 기반 검증 사용하기

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;
}

요청 JSON

{
    "name" : "Robbie",
    "email" : "Robbie@gmail.com",
    "price" : 1234,
    "discount" : -1234,
    "link" : "54321",
    "max" : 10,
    "min" : 2
}

도메인 : localhost

Authorization = Bearer...

회원가입에 valid 적용하기

DTO

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 = "";
}

Controller

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;
profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글