Spring Security OAuth를 이용해 토큰 발급 서버를 https://github.com/gnu-gnu/spring-boot-oauth-authserver 에 만들어 보았다.
리소스 서버도 구현 중인데, 일단 필요한 기능을 임시로 구현하면서 이미 완성된 토큰 발급 서버에 추가해서 구현하고 있다.
현재 구상하고 있는 모습은
1. Web Security 는 각자 서비스에서 구현하도록 한다.
2. Spring Security OAuth의 Resource Server는 JAR를 Dependency에 넣으면 자동으로 구성한다.
인데
2와 같이 구현할 경우 JAR를 의존성에 넣기만 하면 리소스 서버가 구성되어 편리할 수는 있겠으나 @EnableResourceServer를 이용한 Resource Server를 설정 부분이 의존성 JAR에 종속된다.
그러므로 추가적으로 인가와 관련된 세부 설정이 필요한 경우 Scope 혹은 Token의 특정 속성을 이용하여 인가 프로세스를 직접 구현할 수 있도록 하기 위해 Custom Expression을 만들어 보았다.
Method Expression 기반 인가 프로세스가 이루어지는 순서를 살펴본 결과
1. FilterChainProxy에 있는 Web Security 관련 Filter 를 모두 통과한다..
2. DispatcherServlet을 거쳐 RequestMappingHandlerAdapter의 내용을 따라 컨트롤러를 찾아간다.
3. 그 과정에서 MethodSecurityInterceptor를 거치게 되고, 이 과정에서 SpElExpression을 해석한 결과를 통해 인가 여부를 결정한다.
의 순서인 것 같다.
그러므로 당연한 이야기지만 WebSecurityConfigurerAdapter 서 설정 해 놓은 Filter를 모두 통과하지 못 했을 경우에는 의미가 없다.
(해당 필터들은 https://docs.spring.io/spring-security/site/docs/current/reference/html/security-filter-chain.html#filter-ordering 참조)
Method Expression 기반 ACL은 Web Security를 이미 통과한 경우 사용자가 메소드를 통해 부가적인 인증 로직을 적용하거나, 혹은 Web의 Endpoint가 아닌 Service Layer등에서 인가 프로세스가 필요할 때 유용하다.
1차적인 접근 제어는 Web security에 맡기고, OAuth token을 이용한 부가적인 접근 제어를 위에서 말한 것과 조합하여 Method에 어노테이션으로 할 생각이다.
구현 해 주어야 하는 부분은 다음과 같다
1. SecurityExpressionRoot : 스프링 시큐리티 표현식을 연산하는 기본 객체
2. MethodSecurityExpression : 표현식을 실제로 처리하는 메소드를 담고 있는 클래스
3. MethodSecurityExpressionHandler : GlobalMethodSecurityConfiguration에 핸들러로 등록될 클래스
4. GlobalMethodSecurityConfiguration : Expression-based security를 설정하는 Bean
5. 실제로 표현식을 사용하는 컨트롤러
구현한 소스코드는 아래와 같다
아래에 언급되는 부분은 스프링 부트 1.5.10을 기준으로 작성한 코드이다.
package com.gnu.AuthServer.method;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
/**
*
* MethodSecurityExpressionRoot는 modifier가 왜 public이 아닌지 알 수가 없음.
* 그냥 바로 써도 될 것 같은데 인스턴스 생성이 불가능하므로 SecurityExpressionRoot를 상속하여 구현
* 아래에 위 클래스의 modifier 관련 이슈가 있음
* @see https://github.com/spring-projects/spring-security/pull/4266
* @author gnu-gnu(geunwoo.j.shim@gmail.com)
*
*/
public class AuthServerSecurityExpressionRoot extends SecurityExpressionRoot {
public AuthServerSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
}
SecurityExpressionRoot를 구현한 클래스이다. 이 클래스는 스프링 시큐리티 표현식을 연산하는 기본 객체가 된다.
SecurityExpressionRoot 클래스만으로도 hasAuthority, hasRole, permitAll, denyAll, isAnonymous, isFullyAuthenticated 등 평소 많이 보아왔던 Security Exresspion 에 해당하는 메소드들을 담고 있다. 그러나 이 클래스는 추상클래스라 인스턴스를 생성할 수가 없다.
약간 의아한 것이 이미 이 클래스를 구현한 MethodSecurityExpressionRoot 클래스가 있는데, 이 클래스는 public class가 아니라서 내가 만든 클래스에서는 new로 불러올 수가 없다. 이 클래스를 public으로 바꿔 달라는 이슈가 https://github.com/spring-projects/spring-security/issues/2251, https://github.com/spring-projects/spring-security/pull/4266 에 제기되어 있는 것을 확인하였다. 6년 전부터 제기된 이슈인데 바꿔주지 않는 것을 보건데 의도가 있는 것 같은데 의도는 알 수가 없다.
package com.gnu.AuthServer.method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import com.gnu.AuthServer.config.AuthServerWebSecurityConfig;
/**
*
* 이 클래스의 public boolean 메소드들이 MethodSecurityExpression 으로 쓰임(eg : PreAuthorize, PostAuthorize)
*
* @author gnu-gnu(geunwoo.j.shim@gmail.com)
*
*/
public class AuthServerMethodSecurityExpression {
private Authentication auth;
private static final Logger logger = LoggerFactory.getLogger(AuthServerWebSecurityConfig.class);
public AuthServerMethodSecurityExpression(Authentication auth) {
this.auth = auth;
}
/**
* #auth.isOk() expression을 호출할 경우 이 메소드를 call하게 된다. 이 메소드의 결과가 true, false 냐에 따라 인가 여부가 결정됨
* @return
*/
public boolean isOk(boolean bool) {
logger.info(auth.toString());
return bool;
}
}
실제로 표현식이 처리되는 MethodSecurityExpression이다.
컨트롤러에서는 StandardEvalutationContext의 prefix+이 클래스의 public boolean 메소드의 이름 (예 : auth.isOk)을 컨트롤러에서 표현식으로 사용한다. 또한 이 메소드는 예시를 위해 boolean을 파라미터로 받고 있다. 이 메소드의 리턴 결과 true/false에 따라 인증 성공여부가 결정된다. 그리고 sec에서 auth 정보를 생성자에 넣어주었기 때문에 이 클래스에서 authentication 정보를 활용할 수 있다. 필요시 authentication 객체를 활용하여 인증 처리를 한다.
package com.gnu.AuthServer.method;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.core.Authentication;
/**
* MethodSecurityExpression을 처리하는 핸들러
* @author gnu-gnu(geunwoo.j.shim@gmail.com)
*
*/
public class AuthServerMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler{
@Override
public StandardEvaluationContext createEvaluationContextInternal(Authentication auth, MethodInvocation mi) {
AuthServerSecurityExpressionRoot root = new AuthServerSecurityExpressionRoot(auth);
root.setTrustResolver(getTrustResolver());
root.setPermissionEvaluator(getPermissionEvaluator());
root.setRoleHierarchy(getRoleHierarchy());
StandardEvaluationContext sec = super.createEvaluationContextInternal(auth, mi);
sec.setRootObject(root);
sec.setVariable("auth", new AuthServerMethodSecurityExpression(auth));
return sec;
}
}
MehodSecurityExpressionHandler이다 createEvaultationContextInternal을 override하여 authentication 정보와 표현식으로 넘어오는 MethodInvocation (이 예제에서는 isok) 정보를 처리한다. 위에서 구현한 SecurityExpressionRoot에 auth를 설정해주고, 기타 잡다한 resolver, evauluator등을 설정 해 준다. 그리고 EvaluationContext를 설정해주는데, 이때 StandardEvalutationContext를 super로 넘기지 않고 직접 new StandardEvalutationContext(auth); 로 지정해주면 MethodInvovation 정보를 넘길 수 없고, super에 구현되어 있는 ParameterNameDiscoverer를 넘길 수가 없기 때문에 super.createEvaluationContextInternal을 해주어 MethodSecurityEvaluationContext 를 생성하도록 한다.
그리고 root 를 설정해주고, 표현식으로 사용할 메소드와 prefix를 지정해 준다. 여기선 auth
package com.gnu.AuthServer.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import com.gnu.AuthServer.method.AuthServerMethodSecurityExpressionHandler;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class AuthServerMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new AuthServerMethodSecurityExpressionHandler();
}
}
GlobalMethodSecurityConfiguration 클래스를 상속한 Bean을 만들고 @EnableGlobalMethodSecurity 로 메소드 시큐리티 설정을 활성화 시킨다. prePostEnabled=true를 통해 @PreAuthorize @PostAuthorize 를 활성화 시킨다. 해당 어노테이션에는 Java5부터 도입된 @Secured 및 JSR-250 표준 롤기반 Authorize 어노테이션인 @RolesAllowed 를 사용할 수 있는 옵션이 있다. 해당 부분에 대해서는 http://www.baeldung.com/spring-security-method-security 를 참조한다.
지금까지 작성한 ExpressionHandler를 createExpressionHandler()를 override하여 등록해 준다.
이제 커스텀 어노테이션을 사용할 준비가 되었다.
/**
* 이 endpoint는 websecurity에서 permitAll로 오픈되어 있지만, method security가 적용된 메소드
* 현재 isOk(boolean bool)는 bool= 값으로 들어온 true/false 에 따라 인증 성공 / 실패를 보여준다.
* @return 인증이 성공할 경우 hello? 라는 문자열 출력
*/
@RequestMapping("/isok")
@PreAuthorize("#auth.isOk(#bool)")
public @ResponseBody String isok(boolean bool) {
return "hello?";
}
컨트롤러를 하나 만들고 /isok 경로에 매핑되는 메소드를 하나 작성한다.
처음에 말한 것과 같이 Web security에서 막힐 경우 Method expression까지 넘어가지 않고 필터에서 권한 없음을 돌려준다.
어노테이션 기반 인가 프로세스는 Web Security에서 permitAll 된 경로에 부가적인 인가 프로세스를 부여하거나, 이미 Web Security에서 인가된 Endpoint에 대해 token등으로 부가적인 인가 프로세스를 추가할 경우에 사용한다.
@PreAuthorize("#auth.isOk(#bool)") 는 컨트롤러에 진입하기 전 컨트롤러의 파라미터 bool을 #auth (=AuthServerMethodSecurityExpression 클래스)의 isOk 메소드에 컨트롤러의 bool 파라미터를 인자로 전달한다. 이 메소드의 실행결과가 true면 컨트롤러에 진입하고, false면 권한이 없을 경우 지정된 행동 (에러 메시지, 로그인 페이지 이동, 401 unauthorized 반환 등)을 수행할 것이다.
https://github.com/gnu-gnu/spring-boot-oauth-authserver.git 의 예제를 수행할 경우
웹브라우저에서 http://localhost:9099/apps/isok?bool=true 을 접속할 경우 정상적으로 접속이 인가되어 hello? 를 표시하고
http://localhost:9099/apps/isok?bool=false 를 접속할 경우 인가를 받지 못 해 초기 페이지로 이동을 수행할 것이다
p.s. 사실 간단하게 하려면 @Component로 Bean을 만든 클래스에 public boolean 메소드를 만들고 컨트롤러에서
@RequestMapping("/open")
@PreAuthorize("@customChecker.isChecked(#auth)")
public @ResponseBody String open(Authentication auth) {
logger.info("/open is PermitAll");
return "open";
}
과 같이 @[Bean 이름].[메소드 및 파라미터]를 @PreAuthorize에 넣어주어도 된다. 그러면 @Component Bean 클래스의 메소드 반환 결과에 따라 인가 여부가 결정된다. github 예제 프로젝트에서 AuthServerMethodSecurityConfig 의 override를 주석처리하고http://localhost:9099/apps/open 을 접속한 후 콘솔 로그를 확인 해 보면 알 수 있다.
'dev > Java&Spring' 카테고리의 다른 글
netflix hystrix-dashboard 가 뜨지 않을 때 (0) | 2019.05.08 |
---|---|
Spring에서 Client Authentication (two-way TLS/SSL) 구현하기 (1) | 2019.04.07 |
Spring에서 insecure SSL 요청(RestTemplate, WebClient) (1) | 2019.04.06 |
스프링마이크로서비스 2/e 책 리뷰 (0) | 2019.02.23 |
SRPING BOOT에서 JSP 사용하기 (0) | 2017.01.07 |