Spring Security
Spring 기반 애플리케이션의 인증로그인, 인가 등을 담당하는 스프링 하위 프레임워크
필터 기반으로 동작 → 인증, 인가를 필터 흐름에 따라 처리
보안과 관련하여 다양한 옵션을 제공해주고 커스텀도 가능하기 때문에 개발자의 편의성을 높여줌
근데 처음 마주하는 순간에는 세상에서 제일 막막한 사람 될 수 있음
관련 용어
- Resources (리소스) : 접근 주체가 접근하고자 요청하는 자원 (글 작성 페이지, 관리자 페이지 등)
- Principal(접근 주체) : 리소스에 접근하려는 사용자
- Credential(비밀번호) : 리소스에 접근하는 대상의 비밀번호
- Authentication(인증) : 접근 주체가 해당 어플리케이션을 사용할 수 있는 사용자인지를 확인
쉽게 말하면 로그인 했을 때 통과 되는 사용자인지를 확인하는 것 - Authorization(인가) : 인증된 사용자가 리소스에 접근할 수 있는 권한을 가졌는지 확인
Spring Security 흐름
- 사용자(접근 주체)가 로그인을 시도 (Http Request)
- AuthenticationFilter에서 UsernamePasswordAuthenticationToken의 첫 번째 생성자에 id, pw 정보를 담아서
- AuthenticationManager ~ UserDetailsService 까지 전달
- UserDetailsService의 loadUserByUsername 메소드를 통해 DB에서 사용자의 정보 조회
- DB에 존재하는 사용자일 경우 UserDetails 객체를 만들고 AuthenticationProvider에 넘겨줌
- AuthenticationProvider에서 인증(비밀번호 매칭 등)에 성공하면 사용자 정보가 담긴 Authentication 객체를 AuthenticationFilter까지 전달
- AuthenticationFilter가 전달받은 UsernamePasswordAuthenticationToken을 LoginSuccessHandler로 보내면
- Authentication 객체를 SecurityContext에 저장
- 사용자에게 sessionID 부여
- 로그인 성공한 후 사용자가 요청할 때마다 쿠키에서 JSESSIONID를 확인하고 유효하면 Authentication 처리해줌
주요 모듈
* 로직은 [더보기]
SecurityContextHolder
보안 주체의 세부 정보 및 응용 프로그램의 보안 컨텍스트에 대한 세부정보를 저장
기본적으로 ThreadLocal을 사용하며, SecurityContext를 감싸고 있음
ThreadLocal
1. 한 쓰레드 내에서 공유하는 저장소. 해당 쓰레드의 SecurityContext에 담긴 정보는 애플리케이션 어디에서든 접근 가능
2. ThreadLocal을 사용한다는 것은 각 쓰레드들이 각자의 SecurityContextHolder를 가지고 있다는 뜻이기도 하며, 이로 인해 사용자 별로 각각의 인증 객체(Authentication)를 가질 수 있는 것
의문 : Thread 생성에는 한계가 있을 텐데 이건 어떻게 처리되는가?
SecurityContext
Authentication을 저장하고 있음
ThreadLocal에 저장됨 (SecurityContextHolder가 ThreadLocal역할)
Authentication
접근 주체의 정보(+권한)을 담는 인터페이스
Authentication객체는 SecurityContext를 통해 접근할 수 있으며
SecurityContext 는 SecurityContextHolder를 통해 접근할 수 있음
SecurityContextHolder.getContext().getAuthentication()
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials(); //보통 PW
Object getDetails(); //상세 정보
Object getPrincipal(); //보통 ID
boolean isAuthenticated(); //인증 성공 여부
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
UsernamePasswordAuthenticationToken
user ID가 Principal역할, user PW가 Credential 역할
첫 번째 생성자는 인증 전, 두 번째 생성자는 인증 후의 객체를 생성할 때 사용됨
principal - user ID / credential - user pw
package org.springframework.security.authentication;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 560L;
private final Object principal;
private Object credentials;
//인증 되기 전의 객체를 생성할 때 사용
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
//인증 완료 후의 객체를 생성할 때 사용
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
AuthenticationManager
인증에 대한 부분을 처리하는 부분
(근데 실질적으로는 AuthenticationManager가 호출하는 AuthenticationProvider에서 처리함)
AuthenticationManager의 authenticate()를 통해 인증이 되면 isAuthenticated(boolean)값이 true로 변경됨
AutheticationProvider에서 인증 성공 시 해당 접근 주체의 정보가 담인 authentication 객체가 SecurityContext에 담김
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationProvider
실질적으로 인증에 대한 부분을 처리
인증 되기 전의 Authentication 객체를 받았다가 인증이 완료 된 객체를 return
인증 완료 시 UsernamePasswordAuthenticationToken의 두 번째 생성자를 사용하여 인증 성공한 접근 주체의 정보를 담고
AuthenticationProvider를 호출한 AuthenticationManager에 넘겨주게 됨
인증 시 체크해야 하는 부분들(활동정지, 탈퇴회원 여부 검증 등)을 커스텀하고자 하면
AuthenticationProvider를 inplements하고 구현해주면 됨
커스텀 시 예외처리는 AutenticationException을 사용
AutenticationException의 종류
- BadCredentialsException : 비밀번호 불일치
- UsernameNotFoundException : 계정없음
- AccountExpiredException : 만료된 계정
- CredentialsExpiredException : 만료된 비밀번호
- DisabledException : 비활성화 된 계정
- LockedException : 잠긴 계정
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
//AuthenticationProvider을 구현한 class
...
@Log
public class CustomAutheticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPwd; //비밀번호 암호화
//Authentication authentication : AuthenticationManager로부터 전달받은 인증객체로 username, password가 담겨 있음
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
String password = (String) authentication.getCredentials(); //Credentials : 비밀번호
// 인코딩 된 password 일치 여부, 활동정지 여부, 탈퇴 여부 등을 체크
// 인증 실패 시 AuthenticationException 중에서 적절한 Exception 선택하여 Exception 발생시킴
...
//인증 완료 후 authenticationToken에 회원 정보(비밀번호 제외한 회원 정보)를 담아 AuthenticationProvider를 호출한 AuthenticationManager에 return
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberAccount, null, memberAccount.getAuthorities());
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
AuthenticationProvider 커스텀 후 SecurityConfig에 Bean 등록
...
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Bean
public AuthenticationProvider customAuthenticationProvider() {
return new CustomAutheticationProvider();
}
...
}
UserDetailsService
DB에서 조회한 사용자 정보를 담은 UserDetails 객체를 반환하는 메소드를 가짐(loadUserByUsername)
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
//UserDetailsService를 구현한 class
...
public class CustomMemberDetailsService implements UserDetailsService {
@Autowired
private MemberService memberService;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
//view에서 받은 email(ID)을 사용해 사용자 정보가 있는지 조회하는 memberService 메소드
MemberVO memberVo = memberService.retrieveMemberByEmail(email);
//정보 없으면 예외 처리
if(memberVo == null) {
throw new UsernameNotFoundException("UsernameNotFoundException, memberVo == null");
}
...
// 권한 추가하는 로직
...
return new MemberAccount(memberVo, roles); //MemberAccount : User를 상속받은 class
}
}
UserDetails
인증에 성공하면 생성되는 UserDetails 객체
Authentication객체를 구현한 UsernamePasswordAuthenticationToken을 생성할 때 사용
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
참고
https://mangkyu.tistory.com/76
[SpringBoot] Spring Security란?
대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능
mangkyu.tistory.com
https://sjh836.tistory.com/165
spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal,
참조문서 https://docs.spring.io/spring-security/site/docs/4.2.7.RELEASE/reference/htmlsingle/#getting-started http://springsource.tistory.com/80 https://okky.kr/article/382738 1. 스프링 시큐리티란?..
sjh836.tistory.com
https://www.inflearn.com/questions/209694
그럼 SecurityContext가 저장되는 곳은 총 3곳인건가요? - 인프런 | 질문 & 답변
처음에 Authentication객체를 SecurityContext에 담아서 보관한다는것 까지능 이해했습니다. 그럼 SecurityContext가 저장되는 곳이 1. ThreadLocal 2. HttpSession 3. SecurityContextHolder 총 3개의 공...
www.inflearn.com
'Spring' 카테고리의 다른 글
[Spring] Spring Security - SecurityContextHolder와 ThreadLocal에 관한 의문 (0) | 2022.06.15 |
---|---|
[Spring] Spring Security - 필터 (0) | 2022.06.14 |
파일과 텍스트 함께 전송하기2 - 지양해야 하는 이유 (0) | 2022.06.09 |
파일과 텍스트 함께 전송하기1 - RequestPart (0) | 2022.06.09 |