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๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์๋ค.