@Secured 기반으로 작업된 사이트에서 필요에 따라 임시권한을 줄 수 있도록 설계된 것이 RunAs
기능
특정 상황에서 특정 권한을 가진 사람은 특별히 임시권한
권한 체크가 복잡한 시스템일수록 특정 메소드에 진입하는데 여러가지 조건식이 붙을 수 있는데 이때 RunAsManager를 사용한다면 이런 조건식을 단순하게 만들어 줄 수 있다
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
return super.afterInvocation(token, result);
}
Before Invocation이 호출되고 Access Denied Exception이 발생하지 않았다면 메서드를 Proceed
-> proceed에서 Exception 발생 여부와 상관없이 이후 finallyInvocation을 처리
super에 해당하는 AbstractSecurityInterceptor에서 의 코드
protected InterceptorStatusToken beforeInvocation(Object object) {
... // 앞단에서 권한 체크 진행
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs != null) {
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
protected void finallyInvocation(InterceptorStatusToken token) {
if (token != null && token.isContextHolderRefreshRequired()) {
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
앞단에서 권한 체크를 진행 한 뒤 Run_As로 되어있는 attribute(String)가 있다면 이를 가지고 RunAS가 포함된 Authority가 있는 RunAsAuthenticationToken을 발급
이렇게 임시로 권한이 추가된 AuthenticationToken을 넘겨주면 이후 Security Context를 새로 만들어서 Context를 비운 다음 RunAs 토큰으로 채워서 넘겨줌
run AS 토큰이 들어오지 않았다면 iscontextHolderRefreshRequired가 false
RunAsManagerImpl
public class RunAsManagerImpl implements RunAsManager, InitializingBean {
public Authentication buildRunAs(
Authentication authentication,
Object object,
Collection<ConfigAttribute> attributes
) {
List<GrantedAuthority> newAuthorities = new ArrayList<>();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
GrantedAuthority extraAuthority = new SimpleGrantedAuthority(
getRolePrefix() + attribute.getAttribute());
newAuthorities.add(extraAuthority);
}
}
if (newAuthorities.size() == 0) {
return null;
}
// Add existing authorities
newAuthorities.addAll(authentication.getAuthorities());
return new RunAsUserToken(this.key, authentication.getPrincipal(), authentication.getCredentials(),
newAuthorities, authentication.getClass());
}
}
buildRunAS에서는 attribute Collection을 돌면서 RunAs로 시작하는지 검사
-> Run_AS라는 PREFIX가 있으면 그 PREFIX에 대해서 ROLE을 앞에 붙여서 GrantedAuthority의 리스트인 newAuthorities에 추가
ex)ROLE_RUN_AS_PRIMARY
MethodSecurityConfiguration
...
@Override
protected RunAsManager runAsManager() {
RunAsManagerImpl runas = new RunAsManagerImpl();
runas.setKey("runas");
return runas;
}
...
Controller
@Secured({"ROLE_USER", "RUN_AS_PRIMARY"}) // Affirmative 기반의 Access Decision Manager이기에
// AttemptAuthorize를 넘어가기 위해 "ROLE_USER"를 추가하였음
@GetMapping("/allpapers")
public List<Paper> allPapers(@AuthenticationPrincipal User user){
return paperService.getAllPapers();
}
PaperService
@Secured({"ROLE_PRIMARY", "ROLE_RUN_AS_PRIMARY"})
public List<Paper> getAllPapers() {
return paperDB.values().stream().collect(Collectors.toList());
}
테스트 코드
@DisplayName("6. user1이 임시로 교장선생님 권한을 얻어서 모든 시험지를 가져온다")
@Test
void test_6() {
paperService.setPaper(paper1);
paperService.setPaper(paper2);
paperService.setPaper(paper3);
client = new TestRestTemplate("user1", "1111");
ResponseEntity<List<Paper>> response = client.exchange(uri("/paper/allpapers"),
HttpMethod.GET, null, new ParameterizedTypeReference<List<Paper>>() {
});
assertEquals(200, response.getStatusCodeValue());
assertEquals(3, response.getBody().size());
System.out.println(response.getBody());
}