의존성 주입(DI) -> Loosely Coupled 설계를 할 수 있고 모듈화를 완성할 수 있음
AOP -> 코드를 스파게티로 엮지 말고, 관심사에 따라 코드를 분리하는 개념
Aspect
: 공통의 관심사(Pointcut + Advice)
ex) 권한 처리, 로그, 트랜잭션 관리, 세션 관리, 기타...
-> 이것들을 비즈니스 로직과 서로 분리해서 작업
-> 어떤 포인트컷 메서드에 대해 어떤 어드바이스 메서드를 실행할 지 결정
Weaving
: 빈과 빈을 Proxy로 감싸서 연결해주는 작업
-> 빈과 빈 호출 사이에 Pointcut을 적용해서 JoinPoint를 판별한 다음 PointCut 을 요청한 Advice를 JoinPoint에 적용
-> 쉽게 말해 포인트컷으로 지정한 핵심 관심 메서드가 호출될때, Advice에 해당하는 횡단 관심 메서드를 삽입하는 과정을 의미
JoinPoint
: 클라이언트가 호출하는 모든 비즈니스 메서드(일종의 포인트컷 후보)
Pointcut
: 특정 조건에 의해 필터링된 조인포인트
-> 특정 메서드에서만 횡단 공통기능을 수행하기 위해 사용
Advice
: 횡단 관심에 해당하는 공통 기능의 코드
(@Before, @After-Returning ....)
Spring Security에서 인증은 Securiy Config라는 필터 체인 상에 위치하게 됨
Authentication Manager
(인증기관)이 Authentication Provider
(인증 제공 주체 : 사람)들을 가지고 있고 Authentication
이라는 통행증을 발급
권한의 경우 인증이 완료되어야 체크 가능
-> 어떤 리소스에 접근할려고 할 때 권한이 있는지
필터 위에 상주하는 Interceptor 를 FilterSecurityInterceptor
라 하고 Method 위에 annotation의 형태로 상주하는 Interceptor 를 MethodSecurityInterceptor
라고 한다
-> @EnableGlobalMethodSecurity
를 설정해줘야 MethodSecurityInterceptor 가 동작합니다.
Filter Security Intercepter(필터 권한 위원회)는 Filter 단에서 설정
Access decision manager
가 Filter Security Intercepter에 매핑이 되기 때문에 각 SecurityInterceptor당 한개의 AccessDecisionManager를 둘수 있다
스프링 애플리케이션에서는 어느 필터를 따라 들어올지 모르기 때문에 메소드 시큐리티에 대한 Access Decision Manager는 딱 한개가 존재
체크해야 할 내용(invocation)
에 있는 ConfigAttribute에 권한 관련 내용이 들어가 있음
Invocation을 직접 체크하는 클래스 -> VOTER
Voter 각각이 ConfigAttribute에 대해 평가를 했을때 Check or Deny 인지 결정
Access Decision Manager는 모든 평가 내용을 취합하여 결정을 내린다
GrantedAuthority
AccessDecisionManager : 권한 위원회
인증은 Authentication Filter에서 Authentication을 인풋으로 Authentication Manager 에게 전달
이후 Authentication Manager는 Provider들을 소집해서 Authentication을 발급할 수 있는 Provider에게 인증을 맏김
supprt() 메서드
는 매니저가 일을 맏길 적절한 Provider를 판단하는 기준
권한은 Security Intercepter
에서 권한을 체크
Security Intercepter는 Invocation
이라는 호출 시점의 환경을 Access Decision Manager
에게 넘겨 줌
-> Manager가 Decide(평가)를 통해 통과시킬지 말지 결정
여기서 support()는 해당 ConfigAttribute를 너가 처리해줄수 있니?라고 물어봄
Security Intercepter라는 것은 Aop개념에서 Advice를 삽입하는 것
Authentication Manager는 재검증이 필요할 경우 사용
권한판정을 위한 Config Attribute를 모아 놓은 Map이 SecurityMetaDataSource
-> 두 인터셉터는 서로 각자 다른 SecurityMetaDataSource를 가지고 있음
Security Config
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser(
User.withDefaultPasswordEncoder()
.username("user1")
.password("1111")
.roles("USER")
);
}
FilterSecurityInterceptor filterSecurityInterceptor;
AccessDecisionManager filterAccessDecisionManager(){
return new AccessDecisionManager() {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// throw new AccessDeniedException("접근 금지"); 이 경우 403
// 그냥 return 만 할시 다 통과
return;
}
@Override
public boolean supports(ConfigAttribute attribute) {
// 어떤것이 오든 다 트루
return true;
}
@Override
public boolean supports(Class<?> clazz) {
// filter invocation에서 이 보터가 쓰일 것
return FilterInvocation.class.isAssignableFrom(clazz);
}
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.httpBasic().and()
.authorizeRequests(
authority->authority
// test1() Filter Security Intercepter에서 Access Denied(User 권한으로 get url 보낼 시)
.mvcMatchers("/greeting").hasRole("USER")
.anyRequest().authenticated()
// .accessDecisionManager(filterAccessDecisionManager()) // 위에 구현한 매니저 사용, 이 경우 다 통과(위에 코드 두줄의 의미가 없겠지?)
);
;
}
}
Controller
@RestController
public class HomeController {
MethodSecurityInterceptor methodSecurityInterceptor;
// 이를 동작시킬려면 Global Method 권한 위원회를 소집해야 함(@EnableGlobalMethodSecurity)
// 직접 만들수 도
@PreAuthorize("hasRole('ADMIN')") // Filter Security Intercepter 후 2차 방어선
@GetMapping("/greeting")
public String greeting(){
return "hello";
}
}
Custom Voter
public class CustomVoter implements AccessDecisionVoter<MethodInvocation> {
// 이 Voter 혼자서 통과시켜주는 방식으로 동작
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return MethodInvocation.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) {
return ACCESS_GRANTED;
}
}
Method Security Configuration
// 이는 반드시 Configuration에 선언해야함
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(new CustomVoter()); // 이 권한 위원회는 컨트롤러가 아닌 서비스 단에서도 적용 됨
return new AffirmativeBased(decisionVoters); // 한명이라도 동의할 시 통과(긍정 위원회)
// return new UnanimousBased(decisionVoters); // 이 경우는 만장일치
// return new ConsensusBased(decisionVoters); // 다수결 경우 위경우에는 찬성:1 반대:1 -> 이 경우 allowIfEqualGrantedDeniedDecision 옵션 설정 해야 함
}
}
테스트 코드
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebIntegrationTest {
@LocalServerPort
int port;
public URI uri(String path) {
try{
return new URI(format("http://localhost:%d%s", port, path));
} catch(Exception ex) {
throw new IllegalArgumentException();
}
}
}
public class AuthorityBasicTest extends WebIntegrationTest {
TestRestTemplate client;
@DisplayName("greeting 메세지 불러오기")
@Test
void test_1(){
client = new TestRestTemplate("user1", "1111");
ResponseEntity<String> response = client.getForEntity(uri("/greeting"), String.class);
System.out.println(response.getBody());
}
}