Spring Security에서의 Authentication(인증)과 관련된 클래스와 내부 처리에 대해 알아보겠습니다.
Spring Security에 대해 큰 흐름은 알지만, 처음부터 적용하는게 아니면 어떤 권한을 주고 권한 체크하는 로직만 추가하거나 수정하며 생각없이 쓰게 되는데, 어떤 흐름으로 되는지 전보다 좀 더 살펴볼 기회가 있었고 Spring Security의 각 클래스들이 어떤 일을 하는지에 대해 두서없이 소소하게 정리해봅니다.
이 글에서 Spring Security Version은 3.2.5 버전이 기준입니다.
1. 인증과 인가란 무엇인가?
인증 절차를 거친후 인가 절차를 진행!
1) 인증(Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차.
2) 인가(Authorization) : 인증된 사용자가 요청된 자원에 접근가능한지를 결정하는 절차.
2. 인증 방식
1) credential 기반 인증 : 사용자명과 비밀번호를 이용한 방식
2) 이중 인증(twofactor 인증) : 사용자가 입력한 개인 정보를 인증 후, 다른 인증 체계(예: 물리적인 카드)를 이용하여 두가지의 조합으로 인증하는 방식
3) 하드웨어 인증 : 자동차 키와 같은 방식
이중 Spring Security는 credential 기반의 인증을 취합니다.
- principal : 아이디
- credential : 비밀번호
3. Spring Security 주요 모듈은 아래 그림과 같이 이루어져있습니다.
1) SecurityContextHolder, SecurityContext, Authentication
세가지 클래스는 Spring Security에서 주요 컴포넌트로, 각 컴포넌트의 관계를 간단히 표현하자면 다음과 같습니다.
우리가 흔히 하는 아이디/패스워드 사용자 정보를 넣고 실제 가입된 사용자인지 체크한후 인증에 성공하면
사용자의 principal과 credential 정보를 Authentication에 담습니다.
Spring Security에서 방금 담은 Authentication을 SecurityContext에 보관합니다.
이 SecurityContext를 SecurityContextHolder에 담아 보관하게 됩니다.
2) Spring Security에서 인증 처리를 코드로 풀어보면
- 1. username과 password를 조합해서 UsernamePasswordAuthenticationToken 인스턴스를 만듭니다.
- 2. 이 토큰(UsernamePasswordAuthenticationToken)은 검증을 위해 AuthenticationManager의 인스턴스로 전달됩니다.
- 3. AuthenticationManager는 인증에 성공하면 Authentication 인스턴스를 리턴합니다.
- 4. 이 authentication을 SecurityContextHolder.getContext().setAuthentication(...)를 set 해줍니다.
import org.springframework.security.authentication.*; import org.springframework.security.core.*; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; public class AuthenticationExample { private static AuthenticationManager am = new SampleAuthenticationManager(); public static void main(String[] args) throws Exception { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while(true) { System.out.println("Please enter your username:"); String name = in.readLine(); System.out.println("Please enter your password:"); String password = in.readLine(); try { Authentication request = new UsernamePasswordAuthenticationToken(name, password); Authentication result = am.authenticate(request); SecurityContextHolder.getContext().setAuthentication(result); break; } catch(AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication()); } } class SampleAuthenticationManager implements AuthenticationManager { static final List AUTHORITIES = new ArrayList(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) { return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } }
3) 로그인한 사용자의 정보 얻기
이렇게, 순차적으로 사용자의 정보를 담아놓고, 필요한 곳에서 다시 현재 로그인한 사용자의 정보를 얻기 위해서는
코드상에서 이렇게 접근하면 됩니다.
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
예전엔 이해없이,
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
이 구문만 보고 누가, 이걸 어떻게 외워서 쓸 수 있을까...생각했었는데
이 포함관계를 이해하고 나서는 바로 쓸수있을것 같습니다. ^^; 역시 암기가 아니라, 이해라는...
4) Spring Security Filter Chain
DelegatingFilterProxy
우리가 Spring Security를 적용하기 위해 거의 처음 넣는게 web.xml설정에 SpringSecurityFilterChain이란 이름으로 filter를 등록하는데,
이 필터는 간단히는 모든 요청(예: /* )을 가로채서 보안 적용을 위한 서블릿 필터입니다. 사실상 보안 처리와 관련된 일을 하는게 아니고, 보안 적용을 위해 Spring Security에게 권한 부여 등을 체크하기 위해 넘겨주는 역할을 합니다.
web.xml 설정
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Spring Security Filter는 A Filter의 일을 수행하고, 다음 B Filter의 일을 수행하는 A -> B-> C Filter...의 Chain형식으로 이루어져 있습니다.
아래 그림과 같이 한 요청에 대해 다음과 같은 filter들이 순차적으로(위에서 아래 방향으로) 일을 수행합니다.
이 필터들이 순차적으로 요청되면서 doFilter 메소드가 수행됩니다.
Spring Security의 제공하는 FormLogin을 기본적으로 사용한다면, FROM_LOGIN_FILTER이름의 UsernamePasswordAuthenticationFilter 클래스에서 인증 절차가 이루어 집니다.
UsernamePasswordAuthenticationFilter클래스는 추상클래스인 AbstractAuthenticationProcessingFilter를 상속하고 있고,
이 클래스의 doFilter 메소드는 다음과 같이 구현되어있습니다.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch(InternalAuthenticationServiceException failed) { logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); }
만약, 이 필터들 사이나 대체하고 싶은 사용자 정의 필터를 넣고 싶다면? Custom filter element 적용
* Custom filter element
커스텀 필터 엘리먼트는 기본적으로 Spring Security에서 제공하는 필터외에 사용자가 만든걸로 적용할 수가 있는데,
적용할 포인트 after, position, before가 있는데, position은 기존에 있던 필터 대신 지정한 filter를 동작하게 하는것이고,
after나 before는 특정 필터를 수행하기 전, 후로 동작하게 됩니다.
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter">
</custom-filter></http>
<beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter">
</beans:bean>
다음 코드는 Spring Security에서 기본적으로 수행하게 되는 "FORM_LOGIN_FILTER" 이름의 UsernamePasswordAuthenticationFilter 클래스 대신에(position으로 지정되었기 때문에), 사용자가 지정한 MySpecialAuthenticationFilter 클래스가 수행됩니다.
5) UserDetails
인증(Authentication) 에 성공을 하면 UserDetails는 Authentication을 SecurityContextHolder에 저장하기 위한 객체로 만들기 위해 사용됩니다.
UserDetails interface를 살펴보면, username, password, 그리고 authorities등 정보를 가져오는 메소드를 가지고 있습니다.
6) UserDetailsService
이 인터페이스는 username으로 UserDetails 객체를 리턴하는 단 하나의 메소드를 가지고 있습니다.
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
UserDetailsService의 구현체로는 in-memory map형태의 InMemoryDaoImpl, JDBC를 사용하는 JdbcDaoImpl등이 있습니다.
7) GrantedAuthority
GrantAuthority는 현재 사용자(principal)이 가지고 있는 permission(허가)
GrantedAuthority는 principal에 주어진 권한입니다. ROLE_ADMINISTRATOR나 ROLE_HR_SUPERVISOR와 같이 보통 "roles" 이라고 하고, GrantedAuthority 객체는 UserDetailsService에 의해 보통 로드됩니다.
사용자가 가지고 있는 permission인 Authorities들을 가져와서 특정 자원(resource)에 권한이 있는지를 체크하여 접근을 허용할지 말지를 체크하는데 쓰입니다.
참고 URL
http://www.slideshare.net/fredondo/javacro2014-springsecurity?qid=e880d0df-7911-4282-8e13-4f2a21438413&v=default&b=&from_search=11
http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#technical-overview
'backend > Spring' 카테고리의 다른 글
[SpringData JPA] query method predicate keywords - null이 아닌 빈값을 제외하고 싶을때 (0) | 2021.10.14 |
---|---|
[Junit5] SpringBoot 2에 JUnit5 적용 (0) | 2019.08.22 |
[Junit5] SpringBoot2+Junit5 에서 TestEngine with ID 'junit-jupiter' failed to discover tests 오류 해결방법 (3) | 2019.07.26 |
[Spring] Lazy Initialization in Spring Boot 2.2 - 번역 (0) | 2019.07.16 |
[Spring] @Autowired의 Before/After (2) | 2009.04.29 |
[Spring] 스프링에서 VelocityTools 환경설정 (2) | 2008.03.20 |
[Spring] 스프링 MVC를 이용한 웹 요청 처리 (4) | 2008.03.13 |
[Spring] Bean과 BeanFactory의 후처리 (8) | 2008.02.12 |