Spring Security RBAC ์„ค์ •, @PreAuthorize ๋กœ ์Œˆ@๋ฝ•ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ

1. ๊ฐœ์š”

์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์˜ api ๊ฐœ์ˆ˜๊ฐ€ ์–ด๋Š์ƒˆ 90๊ฐœ๊ฐ€ ๋‹ค๋˜๊ฐ„๋‹ค.

 

Spring Security๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ RBAC(Role-Based Access Control)์„ ๊ตฌํ˜„ ํ•˜๊ณ ์žˆ๊ณ , ์ด ์„ค์ •์€ Security Config์—์„œ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค. ์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด์˜ ์„ค์ •๊ณผ ์ž˜ ๋งค์นญ๋˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ–ˆ๊ณ , ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ์ถ”๊ฐ€์ ์ธ ์„ค์ •์ด ํ•„์š”ํ–ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ ๊ณผ์ • ๋•Œ๋ฌธ์— ์ง€๊ธˆ๊นŒ์ง€ ๊ฐœ๋ฐœ๋œ API๋“ค์— RBAC์ด ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ต๊ณ , ๋†“์นœ ๋ถ€๋ถ„์„ ์ ๊ฒ€ํ•˜๋Š” ๊ฒƒ๋„ ๋ฒˆ๊ฑฐ๋กœ์šด ์ž‘์—…์ด ํ•„์š”ํ–ˆ๋‹ค. 

 

URL์— prefix๋กœ ROLE์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ผ๊ด„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ณ ๋ คํ•ด ๋ณด์•˜์ง€๋งŒ, ๋ณด์•ˆ์ƒ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์•„๋‹ˆ๋ผ๋Š” ํ”ผ๋“œ๋ฐฑ์„ ์—ฌ๋Ÿฌ ์ฐจ๋ก€ ๋ฐ›์•˜๋‹ค. 

 

 

 

๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ ์•Œ์•„๋ณด๋‹ค, ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ์จ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ  ํ˜„์žฌ ๋งˆ์ฃผํ•œ ๋ถˆํŽธํ•œ ์ ๋“ค์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„ ์ •๋ฆฌ ํ›„ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๊ณ ์ž ํ•œ๋‹ค. 

 

 

 

 

 

 

 

2. @Secured

@Secured๋Š” ํŠน์ • ๊ถŒํ•œ๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๋‚˜ํƒ€๋‚ด๋Š” Annotation์ด๋‹ค. 

๊ฐ„๋‹จํ•˜๊ฒŒ ํŠน์ • ๊ธฐ๋Šฅ์€ Admin๋งŒ ์ด์šฉํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ ์œ„์˜ Annotation์„ ๋ถ™์—ฌ์ฃผ๋ฉด ๋œ๋‹ค. 

 

public class ProjectController {

    private final ProjectService projectService;

    @PostMapping("/api/v1/project")
    @Secured("ROLE_ADMIN")
    public String save(@RequestBody ProjectSaveRequestDto requestDto) throws DuplicateException {

        return projectService.save(requestDto);
    }
}

 

์ผ๋ฐ˜์ ์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋ฉ”์†Œ๋“œ ๋ ˆ๋ฒจ์— ๋ถ™์—ฌ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, @Secured ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” ์ปจํŠธ๋กค๋Ÿฌ์— ๋งคํ•‘๋œ URI๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „, @Secured์— value์˜ ๊ถŒํ•œ์„ ๊ฐ–๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๊ณ , ๊ถŒํ•œ์„ ๊ฐ–๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. 

 

 

 

 

 

 

 

3. @PreAuthorize

public class ProjectController {

    private final ProjectService projectService;

    @PostMapping("/api/v1/project")
    @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
    public String save(@RequestBody ProjectSaveRequestDto requestDto) throws DuplicateException {

        return projectService.save(requestDto);
    }
}

 

@Secured ์™€ ๋‹ฌ๋ฆฌ ์กฐ๊ฑด์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์ ‘๊ทผ ๋ ˆ๋ฒจ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. 

๋˜ํ•œ ์กฐ๊ฑด์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— AND, OR ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ ‘๊ทผ ๋ ˆ๋ฒจ์„ ์—ฌ๋Ÿฌ ๊ฐœ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. 

 

 

 

# ํ‘œํ˜„์‹ ์ข…๋ฅ˜

  • hasRole([role]) : ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์ด ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๊ถŒํ•œ๊ณผ ๋™์ผํ•œ ๊ฒฝ์šฐ true
  • hasAnyRole([role1,role2]) : ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ๋”” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๊ถŒํ•œ ์ค‘ ์ผ์น˜ํ•˜๋Š” ๊ฒƒ์ด ์žˆ๋Š” ๊ฒฝ์šฐ true
  • principal : ์‚ฌ์šฉ์ž๋ฅผ ์ฆ๋ช…ํ•˜๋Š” ์ฃผ์š”๊ฐ์ฒด(User)๋ฅผ ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • authentication : SecurityContext์— ์žˆ๋Š” authentication ๊ฐ์ฒด์— ์ ‘๊ทผ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • permitAll : ๋ชจ๋“  ์ ‘๊ทผ ํ—ˆ์šฉ
  • denyAll : ๋ชจ๋“  ์ ‘๊ทผ ๋น„ํ—ˆ์šฉ
  • isAnonymous() : ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ต๋ช…(๋น„๋กœ๊ทธ์ธ)์ธ ์ƒํƒœ์ธ ๊ฒฝ์šฐ true
  • isRememberMe() : ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ RememberMe ์‚ฌ์šฉ์ž๋ผ๋ฉด true
  • isAuthenticated() : ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ต๋ช…์ด ์•„๋‹ˆ๋ผ๋ฉด (๋กœ๊ทธ์ธ ์ƒํƒœ๋ผ๋ฉด) true
  • isFullyAuthenticated() : ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ต๋ช…์ด๊ฑฐ๋‚˜ RememberMe ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด true

 

 

hasRole ๋กœ๋„ ๊ถŒํ•œ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ Method๋กœ๋„ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜๋‹ค. 

 

webSecurity๋ผ๋Š” Bean/Component์— ์ •์˜๋œ public boolean ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด ๊ถŒํ•œ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•  ์ˆ˜ ์žˆ๋‹ค.

@PreAuthorize("@webSecurity.checkAuthority(authentication, #loginId)")
public ResponseEntity<?> getUserInfo(@AuthenticationPrincipal PrincipalDetails principalDetails,
                        @PathVariable(name = "loginId") String loginId)

 

@Component("webSecurity")
public class WebSecurity {

    public boolean checkAuthority(Authentication authentication, String loginId) {
        if(authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_COMPANY"))
                || loginId.equals("me")) {
            return true;
        }else {
            return false;
        }

    }
}

 

 

 

 

 

 

 

 

4. ๋ฆฌํŽ™ํ† ๋ง ์ง„ํ–‰

๊ธฐ์กด์˜ security config ํŒŒ์ผ์˜ RBAC ์„ค์ • ๋ถ€๋ถ„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. ์ผ๋ถ€๋ฅผ ์ œ์™ธํ•˜๊ณค ์—ฌ๊ธฐ์„œ ์„ค์ •ํ–ˆ๋˜ ๊ถŒํ•œ์„ PreAuthorize ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณ€๊ฒฝํ•  ์˜ˆ์ •์ด๋‹ค. 

 

 

PreAuthorize๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•˜๋Š”๋ฐ String ํƒ€์ž…์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐœ๋ฐœ์ž๋“ค์˜ ์‹ค์ˆ˜๋ฅผ ์ปดํŒŒ์ผ ๋‹จ๊ณ„์—์„œ ์žก์„ ์ˆ˜ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ๋ณ„๋„์˜ ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ ์šฉํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. 

 

 

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('COMPANY')")
public @interface CompanyRoleRequired {

}

 

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("isAuthenticated()")
public @interface AuthenticatedRequired {

}

 

@RestController
@RequiredArgsConstructor
@Slf4j
@CompanyRoleRequired // ๊ถŒํ•œ ์„ค์ •
public class ProjectController {

    private final ProjectService projectService;

    // ์ƒ๋žต .. 
}

 

์œ„์™€ ๊ฐ™์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ์ƒ์„ฑํ•ด์ฃผ์—ˆ๊ณ  ํด๋ž˜์Šค, ๋ฉ”์†Œ๋“œ ๋ ˆ๋ฒจ์— ๊ฐ๊ฐ ์ ์šฉ์‹œ์ผœ์ฃผ์—ˆ๋‹ค. 

 

 

 

 

ํ•˜์ง€๋งŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

 

์ธ์ฆ, ์ธ๊ฐ€์— ๊ด€๋ จํ•œ ์˜ˆ์™ธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹œํ๋ฆฌํ‹ฐ ํ•„ํ„ฐ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•˜์—ฌ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ๊ณ  ์žˆ์—ˆ๋‹ค. 

@Component
@RequiredArgsConstructor
@Slf4j
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
        AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8"); // JSON ์‘๋‹ต์„ UTF-8๋กœ ์„ค์ •
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType(APPLICATION_JSON_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(new Response<String>("์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.")));
        response.getWriter().flush();
        response.getWriter().close();
    }
}
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthenticationEntryPointHandlerImpl implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) throws IOException, ServletException {
        log.error("์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8"); // JSON ์‘๋‹ต์„ UTF-8๋กœ ์„ค์ •
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(APPLICATION_JSON_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(new Response<String>("์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

 

 

๊ธฐ์กด์—” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ํ•„ํ„ฐ ์ฒด์ธ์„ ํƒ€๋ฉด์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ, ControllerAdvice์—์„  ์˜ˆ์™ธ๋ฅผ ์žก์ง€ ๋ชปํ–ˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ @PreAuthorize ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, Controller ๋‹จ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๊ณ , ControllerAdvice์˜ ์•„๋ž˜ ๋กœ์ง์—์„œ ์˜ˆ์™ธ๊ฐ€ ์žกํ˜”๋‹ค. 

 

 

์˜ˆ์™ธ๊ฐ€ ํ„ฐ์ง€๋ฉด ์žกํž ๋•Œ ๊นŒ์ง€ ์•ž์œผ๋กœ throw๋ฅผ ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ControllerAdvice๋Š” Servlet์—์„œ ํ„ฐ์ง„ ์˜ˆ์™ธ๋ฅผ ์žก๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. 

 

 

@ControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class ExceptionController {

    private final SlackService slackService;
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleUnhandledException(Exception e) {
        logError(e);
        sendSlackNotification(e);
        return ResponseEntity.badRequest().body(new Response<>(e.getMessage()));
    }
}

 

๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ Exception์œผ๋กœ ์žก์•„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ถŒํ•œ์ด ์—†๋Š” ์ƒํƒœ์—์„œ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด AccessDeniedException์ด ๋ฐœ์ƒํ–ˆ๊ณ , ์ด ์˜ˆ์™ธ๋Š” ์ธ์ฆ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ๋ฐœ์ƒํ–ˆ๋‹ค.

 

401๊ณผ 403 ์—๋Ÿฌ๊ฐ€ ๊ฐ๊ฐ ๋‹ค๋ฅธ ์˜ˆ์™ธ๋กœ ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด ControllerAdvice์—์„œ ๊ฐ„๋‹จํžˆ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๊ฒ ์ง€๋งŒ, ๊ฐ™์€ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณ„๋„๋กœ ์˜ˆ์™ธ๋ฅผ ์žก์•„ ๊ทธ๋Œ€๋กœ throw ํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€๋‹ค.

 

// ์‹œํ๋ฆฌํ‹ฐ ํ•„ํ„ฐ์—์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•˜๋„๋ก
@ExceptionHandler(AccessDeniedException.class)
public void handleOptimisticLockException(AccessDeniedException e) {
    throw e;
}

 

 

์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ ์„œ๋ธ”๋ › ๋ณด๋‹ค ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ๋” ์•ž๋‹จ์— ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋˜ ExceptionHandler๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.