[Spring Security] 인증
- 4. 인증
- 4-1. 개요
- 4-2. 인증 프로세스
- 4-3. SecurityContext, SecurityContextHolder
- 4-4. Authentication
- 4-5. AuthenticationManager
- 4-6. AuthenticationProvider
- 4-7. UserDetails, UserDetailsService
- 4-8. UserDetailsManager
4. 인증
4-1. 개요
인증 (Authentication)
- 식별(Identification)에 대한 주장이 사실인지 검증하는 과정
- 신분을 주장하는 주체가 리소스에 접근하는 주체가 본인인지 확인받기 위해 사용하는 모든 수단과 방법을 포함한다.
- 웹 애플리케이션에서 인증은 주로 주체가 알고 있는 정보를 기반으로 수행된다.
Servlet 기반 웹 애플리케이션에서의 인증
- Username/Password 방식
- OpenID Connect 기반의 OAuth 2.0 로그인
- 중앙 인증 서버를 통한 인증
- …
Username and Password 기반 인증 방식
- 스프링 시큐리티는 HttpServletRequest 객체로부터 username과 password를 취득하여 인증을 수행할 수 있는 내장 기능을 제공한다.
- 개발자는 스프링 시큐리티의 인증 프로세스를 사용하여 인증을 간편하게 구현할 수 있다.
- 세부 처리 절차
- HTTP Basic 인증
- 클라이언트가 서버에 요청할 때 Authorization 헤더에 username과 password를 Base64로 인코딩하여 전달하는 방식
- Form 기반 인증
- 로그인 폼을 사용하여 username과 password를 제출하고, 서버는 이를 검증하여 인증을 수행하는 방식입니다.
- Digest 인증
- 클라이언트가 서버에 요청할 때, 암호화된 방식으로 username과 password를 전달한다.
- 보안 상의 이유로 모던 웹 애플리케이션 개발에서는 사용하지 않는 것이 권장된다.
- HTTP Basic 인증
4-2. 인증 프로세스
- 인증을 수행하는 메서드 호출
- AbstractAuthenticationProcessingFilter.java
doFilter
에서 인증을 수행하는 메소드를 호출하게 되는데, 이 때 사용되는 클래스가 UsernamePasswordAuthenticationFilter
void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // Attempt an authentication process Authentication authenticationResult = attemptAuthentication(request, response); // ... }
- UsernamePasswordAuthenticationFilter.java
- 아직 인증되지 않은 인증 처리 용도의 임시 토큰을 생성한다.
- 인증 매니저에게 생성한 토큰을 전달하면서 실제 인증 처리를 요청한다.
- username, password라는 parameter를 request에서 받아 온 후, 새로 만든 Authentication 객체에 두 개의 값을 담아 AuthenticationManager로 넘긴다.
Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { // retrieve Username, password value from HttpServletRequest.. // Create a token that is not yet authenticated UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // call AuthenticationManager.authenticate(authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
- AuthenticationManager
- AuthenticationManager는 인터페이스이며, ProviderManager 구현체를 사용한다.
- 실제 인증 처리를 담당할 Provider를 list로 가지고 있다.
- 인증 처리 요청을 받은 인증 매니저는 각 인증 방식에 적합한 실제 인증을 수행할 Provider를 찾고, 해당 Provider에게 인증 처리 작업을 위임한다.
Authentication authenticate(Authentication authentication) { for (AuthenticationProvider provider: getProviders( )) { ... Authentication result = provider.authenticate(authentication); // from AbstractUserDetailsAuthenticationProvider.java ... } }
- AuthenticationProvider
- 실제 인증 처리 작업을 담당한다.
- retrieveUser(username, authentication) : username으로 사용자를 조회해 UserDetails 객체를 받아온다.
- additionalAuthenticationChecks : password와 조회한 UserDetails의 password의 일치여부를 체크한다.
Authentication authenticate(Authentication authentication) { ... UserDetails user = retrieveUser(username, authentication); additionalAuthenticationChecks(user, authentication); }
- retrieveUser(username, authentication)
- loadUserByUsername(username)을 통해 DB에서 username에 해당하는 User 정보를 조회한다.
UserDetails retrieveUser(username, authentication) { ... UserDetails loadedUser = UserDetailsService.loadUserByUsername(username); return loadedUser; }
- DaoAuthenticationProvider.java
- additionalAuthenticationChecks(user, authentication)를 통해 DB에서 조회된 User의 비밀번호(Hashed value)가 인증 토큰에 들어있는 비밀번호와 일치하는 지 검증한다.
- PasswordEncoder를 사용하여 이전 단계에서 반환된 UserDetails의 암호를 확인한다.
void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) { ... String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(...); } }
- additionalAuthenticationChecks(user, authentication)를 통해 DB에서 조회된 User의 비밀번호(Hashed value)가 인증 토큰에 들어있는 비밀번호와 일치하는 지 검증한다.
- BCryptPasswordEncoder.java
- 제출된 인코딩 되지 않은 패스워드와 인코딩 된 패스워드의 일치 여부를 확인한다.
boolean matches(CharSequence rawPassword,String encodedPassword) { // Verify that the raw password matches the encoded password return BCrypt.checkpw(rawPassword.toString(), encodedPassword); }
- AbstractUserDetailsAuthenticationProvider.java
- 비밀번호 검증까지 통과했을 경우, createSuccessAuthentication 메서드를 통해 인증이 성공한 토큰을 생성하는 작업을 수행한다.
- 매개변수로 Authentication 객체와 UserDetails 객체를 담아서 호출한다.
Authentication authenticate(Authentication authentication) { ... UserDetails user = retrieveUser(username, authentication); ... additionalAuthenticationChecks(user, authentication); // create Authenticated Token with Authentication type return createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user); }
- ProviderManager.java
- 인증 처리가 된 Authentication 객체가 AuthenticationProvider의 최종 반환값이 된다. (인증 완료된 토큰)
- eraseCredentials() : 토큰에서 비밀 정보를 지운다.
Authentication authenticate(Authentication authentication) { for (AuthenticationProvider provider: getProviders( )) { Authentication result = provider.authenticate(authentication); } // erase Credentials info eraseCredentials( ); return result; }
- UsernamePasswordAuthenticationFilter.java
- 인증 처리가 완료된 인증 토큰을 응답받아서, AbstractAuthenticationProcessingFilter에 반환한다.
Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { ... UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); return this.getAuthenticationManager().authenticate(authRequest); }
- AbstractAuthenticationProcessingFilter.java
- 인증 수행 메서드 호출
void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // Attempt an authentication process Authentication authenticationResult = attemptAuthentication(request, response); ... }
- successfulAuthentication() 메서드 호출
- attemptAuthentication(..) 메서드로 부터 응답받은 Authentication 토큰을 SecurityContext에 저장한다.
void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // retrieve Username, password value from HttpServletRequest.. Authentication authenticationResult = attemptAuthentication(request, response); ... // Saving the authentication token to the SecurityContext successfulAuthentication(request, response, chain, authenticationResult); }
- AbstractAuthenticationProcessingFilter.java
- 인증에 성공한 토큰을 SecurityContext에 담는다.
void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) { SecurityContext context = this.securityContextHolderStrategy.createEmptyContext( ); context.setAuthentication(authResult); this.securityContextHolderStrategy.setContext(context); this.securityContextRepository.saveContext(context, request, response); ... this.successHandler.onAuthenticationSuccess(request, response, authResult); }
- onAuthenticationSuccess(..)
- SecurityContext에 인증 토큰이 저장되면, 다음 Security Filter 작업을 수행하기 위해 FilterChain의 doFilter 메서드를 호출한다.
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { onAuthenticationSuccess(request, response, authentication); chain.doFilter(request, response); } void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication);
4-3. SecurityContext, SecurityContextHolder
SecurityContextHolder
- SecurityContext를 관리하는 객체
- 현재 실행 중인 스레드와 연동하여 인증된 사용자 정보를 저장한다.
SecurityContext
- 인증된 사용자의 상세정보를 보관하는 객체
- Authentication 객체로 표현되며, 사용자가 로그인한 후에 생성된다.
- 인증 수행 전에는 비어있는 상태의 SecurityContext가 SecurityContextHolder에 저장된다.
- 인증 완료 후에는 인증된 사용자의 Authentication 정보가 SecurityContext에 저장된다.
4-4. Authentication
Authentication 인터페이스
- 현재 인증된 사용자의 자격 증명 정보를 저장한다.
- 인증이 성공하면 SecurityContext에 저장된다.
구성요소
- Principal (접근 주체)
- 서비스나 리소스에 접근하는 사용자
- Username/Password 기반 인증에서는 주로 UserDetails 객체가 사용된다.
- Credentials (비밀 정보)
- 주로 비밀번호를 나타낸다.
- 인증이 완료된 후에는 보안상의 이유로 비밀번호 정보는 가려지거나 지워진다.
- Authorities (권한)
- 해당 사용자가 가진 권한 정보
- 사용자가 시스템에서 어떤 작업을 수행할 수 있는 지를 나타낸다.
- Collection 타입으로 여러 권한을 가질 수 있다.
- 권한 정보는
GrantedAuthority
인터페이스로 관리된다.
GrantedAuthority
- 사용자의 개별 권한 정보를 담고 있는 인터페이스
사용자가 특정 리소스에 접근할 수 있는 지의 여부를 확인한다.
public interface GrantedAuthority extends Serializable { String getAuthority(); // 권한 이름 }
4-5. AuthenticationManager
AuthenticationManager 인터페이스
- 필터를 통해 들어오는 인증 요청을 처리하고, 인증이 성공 또는 실패했는 지를 결정한다.
authenticate()
메서드- 전달받은 Authentication 객체를 기반으로 인증을 시도한다.
- 성공 시, GrantedAuthority를 포함한 인증된 Authentication 객체를 반환한다.
- 반환된 Authentication 객체는 SecurityContextHolder에 저장되어, 이후 인증된 사용자 정보를 참조할 수 있다.
- 실패 시, AuthenticationException을 던진다.
ProviderManager
- AuthenticationManager의 구현체
- 인증을 처리하는 여러 AuthenticationProvider를 관리한다.
- 여러 AuthenticationProvider 인스턴스를 List 형태로 위임받아, 각각의 AuthenticationProvider가 인증 요청을 처리할 수 있도록 한다.
- 인증과정
- 여러 AuthenticationProvider 중 하나가 인증을 시도한다.
- 인증 성공 시, 인증된 Authentication 객체가 반환된다.
- 인증을 수행할 수 있는 AuthenticationProvider가 존재하지 않으면, 인증이 실패하고 ProviderNotFoundException이 발생한다.
4-6. AuthenticationProvider
AuthenticationProvider 인터페이스
- 인증에 필요한 로직을 정의한다.
- 인증 방식에 맞는 실제 인증 처리를 수행한다.
- 사용자 정보를 데이터베이스에서 조회하기 위해 UserDetailsService를 호출하여 인증을 처리한다.
DaoAuthenticationProvider
- AuthenticationProvider의 구현체 중 하나로, Username/Password 기반 인증을 처리한다.
UserDetailsService를 통해 데이터베이스에서 사용자 정보를 조회한 후, 해당 정보로 인증을 수행한다.
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private UserDetailsService userDetailsService; @Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null"); } return loadedUser; } }
4-7. UserDetails, UserDetailsService
UserDetails
사용자 정보를 담고 있는 인터페이스
public interface UserDetails extends Serializable { String getUsername(); String getPassword(); Collection<? extends GrantedAuthority> getAuthorities(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
User
- UserDetails 인터페이스의 구현체
- 스프링 시큐리티에서 제공하는 기본적인 사용자 객체
사용자의 이름, 비밀번호, 권한 정보, 계정 만료 여부 등을 보관하며 UserDetails 인터페이스의 메서드를 구현한다.
public class User implements UserDetails { private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; //... }
UserDetailsService
username을 기반으로 데이터베이스에서 사용자 정보를 조회하고, 이 정보를 UserDetails 객체에 담아 반환한다.
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
4-8. UserDetailsManager
UserDetailsManager 인터페이스
- 사용자 정보를 관리하는 기능을 제공한다.
사용자 정보를 생성, 수정, 삭제하고 비밀번호를 변경하는 등의 작업을 수행한다.
public interface UserDetailsManager extends UserDetailsService { void createUser(UserDetails user); void updateUser(UserDetails user); void deleteUser(String username); void changePassword(String oldPassword, String newPassword); boolean userExists(String username); }
UserDetailsManager의 주요 구현체
- InMemoryUserDetailsManager
- 사용자 정보를 메모리 상에서 저장하고 관리한다.
- 간단한 인증 처리를 위해 사용되며, 주로 테스트 또는 작은 애플리케이션에서 활용된다.
- 애플리케이션이 종료되면 데이터가 사라진다.
- JdbcUserDetailsManager
- 데이터베이스에 저장된 사용자 정보를 기반으로 사용자 관리 작업을 수행한다.
- 실제 DB와 연동하여 사용자 정보를 저장하고 관리하며, 비즈니스 애플리케이션에 적합하다.
- 데이터를 영구적으로 저장하며, SQL 기반의 사용자 관리가 가능하다.
Spring Security 인증 실습
- HelloController 생성
- RestController
- 인증이 되어야 접근할 수 있는 하나의 리소스
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello"; } }
- localhost:8080/hello 요청 시 login 창이 뜬다
- hello에 대한 요청이 날아감 → 로그인이 안됨
- location이 login 페이지로 설정되어 있다. 인증이 안되있으면 로그인 페이지로 이동한다.
- localhost:8080/login으로 GET요청을 보낸다.
- 로그인에 성공하면
localhost:8080/hello
로 POST 요청이 날아간다. - 쿠키에 JSession이 생성된다.
JSESSIONID=06F4B7E5FDC90F28D73EA975B1F84B67;
우리 FISA 수업을 참고하였습니다.