Spring security Role Hierarchy - 역할 계층 구현하기
https://github.com/syakuis/role-hierarchy
Spring security 에서 효율적인 역할 (Role) 을 관리하기 위해 역할 계층(Role Hierarchy) 을 사용할 수 있다. 일반적으로 접근에 필요한 역할을 모두 사용자가 소유해야 한다.
즉 ROLE_ADMIN 관리자 역할이라고 역할 명을 정하더라도 이건 이름일뿐 제대로된 의미를 가지지 못한다.
ROLE_POST 가 필요한 접근 경로에는 ROLE_POST 를 무조건 사용자가 소유해야 한다. 만약 이런 역할들이 무수히 많다면 한 개의 계정에 무수히 많은 역할을 부여해야 한다. 역할을 관리해야 하는 기능에서 불편한 부분을 제공하게 된다.
그리고 역할을 그룹화 할 수 있지만 결국 이 또한 그룹이 많아지면 그만큼 그룹 역할을 부여해야 하는 불편함이 생긴다. 그래서 Spring Security 에서는 역할 계층 이라는 기능을 제공한다.
// 역할
ROLE_POST, ROLE_COMMENT, ROLE_FILE
// 역할 상속
ROLE_MANAGER > ROLE_POST,
ROLE_MANAGER > ROLE_COMMENT,
ROLE_MANAGER > ROLE_FILE
ROLE_ADMIN > ROLE_MANAGER
- ROLE_MANAGER 은 ROLE_POST, ROLE_COMMENT, ROLE_FILE 역할들이 소유한 모든 역할을 가진다.
- ROLE_ADMIN 은 ROLE_MANAGER 이 소유한 역할을 모두 가진다.
개발 스펙
- java 8
- spring boot 2.1.2
- gradle 5, junit 4, log4j2
구현
gradle.build
buildscript {
apply plugin: 'java'
ext {
springBootVersion = "2.1.2.RELEASE"
lombokVersion = "1.18.2"
}
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
mavenCentral()
jcenter()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
}
}
apply plugin: "java"
apply plugin: "idea"
apply plugin: "org.springframework.boot"
apply plugin: "io.spring.dependency-management"
// intelliJ setting
idea {
module {
inheritOutputDirs = true
outputDir = compileJava.destinationDir
testOutputDir = compileTestJava.destinationDir
}
}
repositories {
jcenter()
mavenCentral()
}
group 'org.syaku.security'
version '1.0.0'
sourceCompatibility = 1.8
targetCompatibility = 1.8
configurations {
providedRuntime
compile.exclude module: "spring-boot-starter-tomcat"
compile.exclude module: "spring-boot-starter-logging"
}
dependencies {
testCompile "junit:junit:4.12"
implementation "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
testImplementation "org.projectlombok:lombok:${lombokVersion}"
testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}"
implementation "org.springframework.boot:spring-boot-starter-web"
testImplementation "org.springframework.boot:spring-boot-starter-test"
implementation "org.springframework.boot:spring-boot-starter-log4j2"
implementation "org.springframework.boot:spring-boot-starter-jetty"
implementation "org.springframework.boot:spring-boot-starter-security"
testImplementation "org.springframework.security:spring-security-test"
}
wrapper {
gradleVersion = '5.4.1'
}
설정
@Log4j2
@EnableWebSecurity
public class WebSecurityConfiguration {
/*
준비해야할 설정을 구성할때 사용하기 위한 공간
준비가 필요없다면 SecurityConfiguration 클래스만 구현하면 된다.
*/
@Log4j2
@Configuration
static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
Map<String, List<String>> roleHierarchyMap = new HashMap<>();
roleHierarchyMap.put("ROLE_ADMIN", Arrays.asList("ROLE_MANAGER"));
roleHierarchyMap.put("ROLE_MANAGER", Arrays.asList("ROLE_POST", "ROLE_COMMENT", "ROLE_FILE"));
roleHierarchyMap.put("ROLE_USER", Arrays.asList("ROLE_POST"));
String roles = RoleHierarchyUtils.roleHierarchyFromMap(roleHierarchyMap);
log.debug(roles);
roleHierarchy.setHierarchy(roles);
// 혹은 아래와 같이 작성할 수 있다.
// roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER\nROLE_MANAGER > ROLE_POST\nROLE_MANAGER > ROLE_COMMENT\nROLE_MANAGER > ROLE_FILE\nROLE_USER > ROLE_POST");
return roleHierarchy;
}
@Bean
public SecurityExpressionHandler<FilterInvocation> expressionHandler() {
DefaultWebSecurityExpressionHandler webSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
webSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
return webSecurityExpressionHandler;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.and().authorizeRequests()
// 인가 설정에서 순서도 중요하다. 먼저 판단하기 위해 상위에 배치해야 한다.
.antMatchers(HttpMethod.POST, "/api/blog").hasRole("POST")
.antMatchers("/api/blog/**").hasRole("MANAGER")
.anyRequest()
.authenticated()
.expressionHandler(expressionHandler());
}
}
}
인증(Authentication) 과 인가(Authorization)가 있는 데, 에버랜드를 비유하여 설명하겠다.
에버랜드에 입장하기 위해 티켓이 있어야 하고 티켓 종류도 다양할 것이다. 티켓에 따란 내가 입장할 수 있는 입장 권한도 다를 것이다. 이런 것들을 판단하여 최종적으로 입장 허가증을 발급받는 데 이런 일련의 과정과 최종 입장 허가증을 발급받는 것 까지가 인증이다. 즉 인증에서는 인증 값 들과 접근할 수 있는 권한을 부여받게 된다.
입장 허가증이 있다고 해서 에버랜드 모든 곳을 들어갈 수 있는 것은 아니다. 티켓 종류에 따라 놀이기구나 관람 가능한 곳이 다르며 에버랜드를 관리 및 운영하는 스텝 일 수도 있는 것이다. 즉 어떠한 곳을 들어가기 위해 입장 가능 여부를 판다고 허가 해주는 것 까지를 인가라고 한다.
테스트
데모
@RestController
@RequestMapping("/api/blog")
public class BlogController {
@GetMapping
public void get() {
}
@PostMapping
public void post() {
}
@PutMapping
public void put() {
}
@DeleteMapping
public void delete() {
}
}
테스트
@Log4j2
@RunWith(SpringRunner.class)
@WebMvcTest(BlogController.class)
public class WebSecurityConfigurationTest {
private String URL = "/api/blog";
@Autowired private MockMvc mvc;
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void admin() throws Exception {
mvc.perform(get(URL)).andExpect(status().isOk());
}
@Test
@WithMockUser(username = "manager", roles = "MANAGER")
public void manager() throws Exception {
mvc.perform(put(URL).with(csrf())).andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = "USER")
public void user() throws Exception {
mvc.perform(post(URL).with(csrf())).andExpect(status().isOk());
}
}
'Back-end' 카테고리의 다른 글
Java 8 정리 - java 8 lambda Stream Optional Null LocalDate Time example 자바 람다 스트림 타입 패키지 (0) | 2018.11.22 |
---|---|
spring boot RestDocs 코드 리팩토링과 Spring RestDocs 적용 (0) | 2018.11.01 |
spring boot security 스프링 부트 시큐리티 설정과 사용자 인증 #1 (0) | 2018.10.29 |
spring boot jpa 스프링 부트 블로그 만들기 #2 - blog (0) | 2018.10.20 |