[인프런]스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security
스프링시큐리티
스프링 시큐리티 기본 API 및 Filter이해
Form Login
로그인 페이지를 이용한 인증 방식
관련 설정 종류
- loginPage("/loginPage") : 내가 사용할 로그인 페이지, 만약 설정 안하면 스프링시큐리티 기본 로그인 페이지 이용
- defaultSuccessUrl("/") : 로그인 성공 후에 이동할 경로, 스프링시큐리티는 기본적으로 로그인을 성공하게 되면 제일 먼저 로그인을 성공하기 직전에 거쳐왓던 url 정보를 기억하고 있다가,
성공하게 되면 그 URL로 리다이렉트함, SavedRequest, requestCache등에 이동할 경로가 없거나, 이외에도 우선순위에 따른 targetUrl을 계속 구하다가 아무값도 없을때 defaultSuccessUrl로 이동한다 - failureUrl("/login") : 실패시 이동할 경로, defaultSuccessUrl과 비슷하다.
- usernameParameter("userId") : form으로부터 넘어오는 user 필드 이름값
- passwordParameter : form으로 부터 넘어오는 password 필드 이름값
- successHandler() : 로그인 성공시 이후 동작을 설정할 수 있다.
- failureHandler : 로그인 실패시 이후 동작을 설정할 수 있다.
스프링 시큐리티 주요 아키텍쳐
FilterChainProxy
- springSecurityFilterChain의 이름으로 생성되는 필터 빈
- DelegatingFilterProxy으로 부터 요청을 위임 받고 실제 보안 처리
- 스프링 시큐리티 초기화 시 생성되는 필터들을 관리하고 제어
- 스프링 시큐리티가 기본적으로 생성하는 필터 + 설정 클래스에서 API 추가시 생성되는 필터
- 사용자의 요청을 필터 순서대로 호출하여 전달
- 첫번째 필터가 처리되면 FilterChainProxy로 돌아가고, 거기서 두번째 필터를 호출, 처리되면 세번째...
- 사용자 정의 필터를 생성해서 기존의 필터 전,후로 추가 가능하다.
- 하나라도 인증 및 인가 예외가 발생하지 않으면 보안 통과
SecurityContext
- SecurityContextHolder에 Authentication를 Authentication에 User를 가지고 있음
- ThreadLocal에 저장되어 아무 곳에서나 참조가 가능하도록 설계함
- 인증이 완료되면 HttpSession에 저장되어 어플리케이션 전반에 걸쳐 전역적인 참조가 가능하다.
SecurityContextHolder
- SecurityContext 객체 저장 방식
- MODE_THREADLOCAL : 스레드당 SecurityCOntext 객체를 할당, 기본값
- MODE_INHERITABLETHREADLOCAL: 메인 스레드와 자식 스레드에 관하여 동일한 SecurityContext를 유지
- MODE_GLOBAL: 응용 프로그램에서 단 하나의 SecurityContext를 저장한다.
인증이 끝나고 SecurityContext에 저장이 되면, SecurityContext.getContext().getAuthentication()을 사용해 접근할 수 있음
또 HttpSession에도 저장하는데,
SecurityContext attribute = (SecurityContext) httpSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
이런식으로 사용할 수 있다.

인증 성공시 아래 위치에서 인증값을 저장한다.

SecurityContextPersistenceFilter
- SecurityContext 객체의 생성, 저장 조회
- 익명 사용자
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장
- AnonymousAuthenticationFilter에서 AnonymouseAuthenticationToken 객체를 SecurityContext에 저장
- 인증시
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장
- UsernamePasswordAuthenticationFilter에서 인증 성공 후 SecurityContext에 UsernamePasswordAuthentication 객체를 SecurityContext에 저장
- 인증이 최종 완료되면 Session에 SecurityContext를 저장
- 인증 후
- Session에서 SecurityContext꺼내어 SecurityContextHolder에서 저장
- SecurityContext안에 Authentication 객체가 존재하면 계속 인증을 유지한다.
- 최종 응답시 공통
- SecurityContextHolder.clearContext()

인증을 받은 사용자건 아닌 사용자건 SecurityContextPersistenceFilter를 거쳐감
인증전
- 일단 새로운 컨텍스트를 생성한다. SecurityContext는 null상태
- 인증객체를 SecurityContextHolder의 SecurityContext에 저장
인증후
- Session에서 SecurityContext를 확인함
- SecurityContextHolder의 SecurityContext에 저장
Authentication Flow
UsernamePasswordAuthenticationFilter → AuthenticationManager → AuthenticationProvider → UserDetailsServier → Repository
AuthenticationProvider → 폼인증을 처리(DaoAuthentication)

- 사용자의 요청을 받아서 Authentication인증객체를 생성
(id+ password 정보를 담음) - Filter는 AuthenticationManager에게 인증 요청을 보냄
- AuthenticationManager는 id, password검증에 참여하지 않음AuthenticationProvider에게 인증 처리를 위임함
- AuthenticationProvider는 id, password를 검증
UserDetailsService에 username을 주면서 id 조회 검증
5. (UserDetailsService)만약 있다면 UserDetails타입으로 반환
패스워드 체크 진행, 틀리면 Bad Creidential Exception 발생
6. (AuthenticationProvider)id , password 검증이 끝나면
Authentication 인증 객체를 생성함(UserDetails + authorities)
그후 AuthenticationManager에게 전달
7. (AuthenticationManager) 전달받은 객체를 Filter에 다시 전달
AuthenticationManager

- 사용자로부터 인증이 들어온다.
- ProviderManager는 인증 요청을 받아 해당 인증을 처리할 Provider를 찾는다.
- 인증을 처리하게되면 인증객체를 다시 필터로 넘겨준다.
*만약 없으면 부모 ProviderManager에서 탐색을 시작해 해당 인증을 처리할Provider를 찾아 처리한다.
인증처리를 위임한다. 인증처리를하지는 않는다. 인증처리 요건에 맞는 AuthenticationProvier를 찾아 인증처리를 위임한다.
- Form인증 → DaoAuthentocationProvier
- RememberMe 인증 → RememberAuthenticationProvider
- Oauth인증 → ProvierManager에는 없음, parent속성의 ProvierManager가 가지고 있는 OauthAuthenticationProvider를 찾아 위임
아래와 같이 parentAuthenticationManager를 초기 생성할때 같이 넣어준다.


이외에 Provier에서 오류가 발생하면 해당 오류를 받아 Filter에게 다시 전달해주는 역활도 한다.
AuthenticationProvier

인증 역활을 하는 가장 핵심 (AuthenticationProvier)
- authenticate
- ID검증 → UserDetailsService
(없을시)UserNotFoundException
(있으면) UserDetails 반환 - password검증 → (틀리다면)BadCredentialException
(성공하면)createSuccessAuthentication에서
UsernamePasswordAuthenticationToken을 만든다 . - 추가 검증
- (1,2,3 전부 이상 없으면 )Authentication(user, authorities) 생성 후 → (자신을 호출한)AuthenticationManager로 전달
- ID검증 → UserDetailsService
- supports
(참조)UsernamePasswordAuthenticationToken 생성 코드

Authorization
- 당신에게 무엇이 허가 되었는지 증명하는 것
FilterSecurityInterceptor
- 마지막에 위치한 필터로써 인증된 사용자에 대하여 특정 요청의 승인/거부 여부를 최종적으로 결정
- 인증 객체 없이 보호자원에 접근을 시도할 경우 AuthenticationException을 발생
- 인증 후 자원에 접근 가능한 권한이 존재하지 않을 경우 AccessDeniedException을 발생
- AccessDecisionManager에 의해 인가 여부를 판단함

- 인증 객체가 없으면 AuthenticationExcetion 에러를 발생시킴
있으면 SecurityMetadataSource로 부터 사용자가 설정한 인가정보를 가져옴 - 권한 정보가 없다면 자원 접근을 허용, 있다면 해당 인증객체가 권한을 가지고 있는지 확인한다.
- 권한이 없다면 AccessDeniedException을 발생시킴, 있으면 해당 자원 접근 허용
AccessDecisionManager
- 인증 정보 요청정보, 권한 정보를 이용해서 사용자의 자원접근을 허용할 것인지 거부할 것인지를 최종 결정하는 주체
- 여러 개의 Voter 들을 가질 수 있으며 Voter 들로부터 접근 허용 , 거부, 보류에 해당하는 각각의 값을 리턴받고 판단 및 결정
- 최종 접근 거부 시 예외 발생
-접근 결정의 세가지 유형
- AffirmativeBased:
- 여러개의 Voter 클래스 중 하나라도 접근 허가로 결론을 내면 접근 허가로 판단한다.
- ConsensusBased:
- 다수표에 의해 최종 결정을 판단한다.
- 동수일경우 기본은 접근허가이나 allowIfEqualGrantedDeniedDecisions을 False로 설정할 경우 접근 거부로 결정된다.
- UnanimousBased:
- 모든 보터가 만장일치로 접근을 승인해야 하며 그렇지 않은 경우 접근을 거부한다.

AccessDecisionVoter
- 판단을 심사하는것
- Voter가 권한 부여 과정에서 판단하는 자료
- Authentication - 인증정보
- FilterInvocation - 요청정보(antMatcher("/user")
- ConfigAttributes - 권한 정보 (hasRole("USER"))
- 결정방식
- ACCESS_GRANTED: 접근허용(1)
- ACCESS_DENIED: 접근 거부(-1)
- ACCESS_ABSTAIN : 접근 보류(0)

최종 정리 한장

HttpSessionRequestCache,DefaultRedirectStrategy와 같이 아키텍쳐 관점에서 전체적인 흐름 한번만 설명해주실수 있나요?
스프링 시큐리티의 인가처리
- http.antMatchers("/user").access("hasRole('USER')")
- 사용자가 → 인증 정보
/user 자원에 접근하기 위해서는 → 요청정보
ROLE_USER권한이 필요하다 → 권한 정보
Authentication (S.c)
- user
FilterInvocation
- request(/user)
List
- "hasRole('USER')"
http.antMatchers("/user").access("hasRole('USER')")
Map → { "/user" : "hasRole('USER')" }
- 권한 정보를 파싱해서 Map형식으로 만들고 해당 데이터를 List 형식으로 만들어 SecrurityInterceptor로 보내주고
- SecrurityInterceptordptj vote()를 호출하면서 인증 요청 권한 정보를 넘겨준다
!


URL 패턴으로 인가 설정하기
- FilterInvocationSecurityMetadataSource을 구현하면됨
public class UrlFilterInvocationSecurityMetaDataSource implements FilterInvocationSecurityMetadataSource {
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap = new LinkedHashMap<>();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
**requestMap.put(new AntPathRequestMatcher("/mypage"), Arrays.asList(new SecurityConfig("ROLE_USER")));**
HttpServletRequest request = ((FilterInvocation) object).getRequest();
if (requestMap != null) {
for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()) {
RequestMatcher matcher = entry.getKey();
if (matcher.matches(request)) {
return entry.getValue();
}
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet();
Iterator var2 = this.requestMap.entrySet().iterator();
while (var2.hasNext()) {
Map.Entry<RequestMatcher, List<ConfigAttribute>> entry = (Map.Entry) var2.next();
allAttributes.addAll((Collection) entry.getValue());
}
return allAttributes;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
requestMap.put(new AntPathRequestMatcher("/mypage"), Arrays.asList(new SecurityConfig("ROLE_USER")));
→ DB에서 조회해와서 매칭하는 방식으로 교체 가능
디버깅 순서
FilterChainProxy : doFilter() → FilterSecurityInterceptor:doFilter() → UrlFilterInvocationSecurityMetaDataSource: getAttributes()
필터에 내가 만든 필터 확인
Urlq방식 - Map 기반 DB 연동
UrlResourcesMapFactoryBean
- DB로 부터 얻은 권한 /자원 정보를 ResourceMap을 빈으로 생성해서 UrlFilterInvocationSecurityMetaDataSource에 전달
Url 방식- 계층 권한 적용하기 Hierarchy
RoleHierarchy
- 상위 계층 Role은 하위 계층 Role의 자원에 접근 가능
- ROLE_ADMIN > ROLE_MANAGER > ROLE_USER일 경우 ROLE_ADMIN만 있으면 하위 모든 권한을 포함
RoleHierarchyVoter
- RoleHierarchy를 생성자로 받으며 이 클래스에서 설정한 규칙이 적용되어 심사함