로그인 상태인 회원만 글을 작성하게 하려면 세션을 활용해볼 수 있다. 하지만 이 경우 로그인 시에 세션을 만들어 반환해주고, 기능에 따라 세션을 자체적으로 계속 체크해주어야한다. 물론 세션은 여러 문제가 있기 때문에 JWT를 이용할 것인데 스프링 시큐리티를 사용하면 편리하게 인증 및 인가를 구현할 수 있다고 한다. 사실 JWT만 이용하면 되지 않을까 생각을 했었는데, 스프링 시큐리티를 이용하면 패스워드 암호화나 페이지 권한 등 직접 개발해야하는 문제를 모두 도와준다고 한다. 아직은 어떠한 장점이 있는지 잘 모르기 때문에 천천히 공부하면서 알아가보려고 한다.
스프링 시큐리티(Spring Security)란?
- Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크
- 기본적으로 세션 & 쿠키 방식으로 인증한다.
- 인증과 권한에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
- [참고] 세션, 서블릿 필터, 스프링 인터셉터
- Filter는 DispatcherServlet으로 가기 전에 적용되어 가장 먼저 URL 요청을 받는다.
- HTTP 요청 ➙ WAS ➙ 필터 ➙ 서블릿(디스패처 서블릿) ➙ 스프링 인터셉터 ➙ 컨트롤러
- 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서 로직을 하나하나 작성하지 않아도 된다는 장점이 있다.
- 인증관리자(Authentication Manager)와 접근 관리자(Access Decision Manager)를 통해 사용자의 리소스 접근을 관리한다.
- 인증 관리자는 UserNamePasswordAuthenticationFilter, 접근 관리자는 FilterSecurityInterceptor가 수행한다.
인증(Authorizatoin)과 인가(Authentication)
인증(Authentication): 해당 사용자가 본인이 맞는지 확인하는 절차(로그인)
인가(Authorizatoin): 인증된 사용자가 요청한 자원에 접근 가능한지 결정하는 절차(권한부여, 허가)
접근 주체(Principal): 보호받는 자원에 접근하는 유저
비밀번호(Credential): 자원에 접근하는 유저의 비밀번호
- 스프링 시큐리티는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행한다.
- 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인한다.
- 스프링 시큐리티는 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.
스프링 시큐리티 구조(Spring Security Architecture)
- Client에서 로그인 요청
- UsernamePasswordAuthenticationFIlter에서 Id와 Password를 담은 인증객체(Authentication)를 생성
- 인증관리자(AuthenticationManager)에게 인증객체를 넘기며 인증처리를 위임
- 인증관리자(AuthenticationManager)는 적절한 Provider(AuthenticationProvider)에게 인증처리를 위임
- 해당 Provider는 전달받은 인증 객체를 가지고 실제 인증 처리 역할을 함
- UserDetailsService 인터페이스의 loadUserByUsername(username) 메서드를 호출해서 유저 객체 요청
- Repository에 findById() 메서드로 유저 객체 조회
- 유저가 존재한다면 UserDetails 타입으로 반환(못찾으면 예외 발생)
- UserDetailsService 인터페이스의 loadUserByUsername(username) 메서드를 호출해서 유저 객체 요청
- 인증관리자(AuthenticationManager)에서 Password 검증 시작
- 인증객체의 Password와 반환받은 UserDetails의 Password를 비교(실패 시 예외 발생)
- 성공한 인증객체(UserDetals와 authorities를 담은 인증 후 토큰 객체 Authentication)를 UsernamePasswordAuthenticationFilter에 전달
- SecurityContext에 저장
- 이후 전역적으로 SecurityContextHolder에서 인증 객체를 가져와 사용
SecurityConfig
@Configuration
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')")
//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/loginProc")
.defaultSuccessUrl("/");
http.addFilterBefore(new MyFilter1(), UsernamePasswordAuthenticationFilter.class); // 필터 등록
}
}
configure(HttpSecurity http)@EnableWebSecurity: 필터 체인 관리 시작 어노테이션으로 스프링 시큐리티 필터가 스프링 필터 체인에 등록됨@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true): 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화- prePostEnabled : @preAuthorize 와 @postAuthorize어노테이션 활성화
- @preAuthorize 는 컨트롤러의 특정 메서드가 실행되기 직전에 실행됨
- @preAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") 로 여러 개의 접근 권한 설정 가능
- securedEnabled : @secured 어노테이션 활성화
- 컨트롤러의 특정 메서드에서 @secured("ROLE_ADMIN") 로 접근 권한을 간단하게 걸 수 있음
- 이러한 속성으로 특정 어노테이션에만 권한을 걸 수 있고, configure 에서 .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")와 같이 글로벌하게 걸수도 있음
- prePostEnabled : @preAuthorize 와 @postAuthorize어노테이션 활성화
- loginProcessingUrl : 해당 url 주소가 호출되면 시큐리티가 낚아채서 대신 로그인을 진행해줌
- defaultSuccessUrl: 성공 시 해당 url로 이동
BCryptPasswordEncoder- 비밀번호 암호화
UserDetails 구현체
// Authentication 객체에 저장할 수 있는 유일한 타입
@Data
public class PrincipalDetails implements UserDetails{
private User user;
public PrincipalDetails(User user) { // 생성자
super();
this.user = user;
}
@Override
public String getPassword() { return user.getPassword(); }
@Override
public String getUsername() { return user.getUsername(); }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() {
// 1년동안 회원이 로그인을 안하면/계정 탈퇴를 하면 휴면 계정으로 하기로 함
// [현재 시간] - user.getLoginDate() => 1년을 초과하면 return false;
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // 해당 User의 권한을 리턴
Collection<GrantedAuthority> collet = new ArrayList<GrantedAuthority>();
collet.add(()->{ return user.getRole();});
return collet;
}
}
- 과정
- 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킴
- 로그인 진행이 완료가 되면 시큐리티 sessin을 만들어 Security ContextHolder에 저장
- 시큐리티에 들어갈 수 있는 오브젝트가 정해져있음(Authentication 타입 객체)
- Authentication 안에 User 정보가 있어야 됨
- User 오브젝트 타입도 정해져있음(UserDetails 타입 객체)
- Security Session ⇒ Authentication ⇒ UserDetails(PrincipalDetails) 순서로 접근
- Security Session 내부(Authentication 내부( UserDetails(PrincipalDetails)))
UserDetailsService 구현체
@Service
public class PrincipalDetailsService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user == null) {
return null;
}
return new PrincipalDetails(user);
}
}
.loginProcessingUrl("/loginProc")으로 login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어있는 loadUserByUsername 함수가 실행됨loadUserByUsername(): 해당 함수에서 UserRepository를 이용해 User가 있는지 확인
스프링 시큐리티 필터(Spring Security Filter)
Servlet Filter
HTTP 요청 ➙ WAS ➙ 필터 ➙서블릿(디스패처 서블릿) ➙ 스프링 인터셉터 ➙ 컨트롤러
- DispatcherServlet이 요청을 받기 전에 다양한 서블릿 필터들이 올 수 있다.
DispatcherServlet: Spring MVC의 프론트 컨트롤러로, Spring Boot는 DispatcherServlet을 서블릿으로 자동 등록하여 모든 경로에 대해 매핑한다.
- 필터는 클라이언트와 자원 사이에서 요청과 응답 정보를 이용해 다양한 처리를 하는데 목적이 있다.
- Filter들은 여러 개가 연결되어있어 Filter Chain이라고도 불린다.
Security의 Filter
- 스프링 시큐리티는 다양한 기능을 가진 필터들을 기본적으로 제공하고, 이렇게 제공되는 필터들을
Security Filter Chain이라고 한다. - 스프링 시큐리티는 서블릿 필터를 기반으로 동작한다.
- 사용자의 요청이 서블릿에 전달되기 전에 필터의 생명주기를 이용해서 인증과 권한 작업을 수행한다.
- 하지만 서블릿 컨테이너는 스프링 컨테이너에 등록된 빈을 인식할 수 없다.
- Spring Bean은 스프링 컨테이너에서 생성및 관리하는 컴포넌트들이고, ServletFilter는 서블릿 컨테이너에서 생성 및 관리하는 필터들이기 때문에 서로 실행되는 위치가 다르기 때문
- 이 때문에 스프링 시큐리티는
DelegatingFilterProxy라는 서블릿 필터의 구현체를 제공한다.
- DelegatingFilterProxy는 서블릿 매커니즘으로 서블릿의 필터도 등록될 수 있고, 스프링에 등록된 빈을 가져와 의존성 주입을 할 수 있다.
- 즉, 서블릿 필터를 구현한 스프링 빈에게 요청을 위임해주는 대리자 역할의 서블릿 필터
- 스프링 시큐리티는
DelegatingFilterProxy라는 필터를 만들어FilterChainProxy에 넣고, 그 아래에Security Filter Chain그룹을 등록한다.- FilterChainProxy는 DelegatingFilterProxy를 통해 받은 요청과 응답을 SecurityFilterChain에 전달하고 작업을 위임하는 역할을 한다.
- DelegatingFilterProxy에서 바로 SecurityFilterChain을 실행할 수 있지만 중간에 FilterChainProxy를 두는 이유
- 서블릿을 지원하는 시작점 역할을 하기 위해서
- 서블릿에 문제가 발생할 경우, FilterChainProxy의 문제라는 것을 알 수 있다.
- SecurityFilterChain은 인증을 처리하는 여러 개의 시큐리티 필터를 담는 필터 체인으로, 매칭되는 URL에 따라 적용되는 Filter Chain을 다르게 할 수 있다.
Security Filter 종류
SecurityContextPersistenceFilter: SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할LogoutFilter: 설정된 로그아웃 URL로 오는 요청을 감시하고 해당 유저를 로그아웃 처리UsernamePasswordAuthenticationFilter: (ID와 Password를 사용하는 실제 form 기반 인증을 처리) 설정된 로그인 URL로 오는 요청을 감시하고 유저 인증 처리ConcurrentSessionFilter: 동시 접속을 허용할지 체크RememberMeAuthenticationFilter: 세션이 사라지거나 만료되더라도, 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리AnonymousAuthenticationFilter: 사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환SessionManagementFilter: 로그인 후, 인증된 사용자와 관련된 세션 작업을 처리ExceptionTranslationFilter: 필터 체인 내에서 발생되는 인증, 인가 예외 처리FilterSecurityInterceptor: 권한부여와 관련한 결정을 AccessDecisionManager에게 위임함으로써 권한 부여 결정 및 접근 제어 처리
Filter 생성
public class MyFilter1 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter 1");
filterChain.doFilter(servletRequest, servletResponse); // 다음 필터로 넘어가라는 의미
}
}
- Servlet 아래에 있는 Filter 인터페이스 구현
- 해당 필터를 처리하고 다시 다음 필터로 넘겨주는
doFilter()호출을 해주어야 한다. - [참고] 클라이언트에서 JWT 토큰을 담아 요청을 보내면, JWT 필터를 생성해서 JWT 토큰 검증을 제일 먼저 할 수 있다.
Filter 등록
1. SecurityConfig에 추가
@Configuration
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new MyFilter1(), UsernamePasswordAuthenticationFilter.class); // 필터 등록
}
}
addFilter(Filter filter)- 에러 발생 → SecurityFilter가 아니기 때문
- SpringSecurityFilterChain에 등록되지 않았기 때문에 등록하고 싶으면 addFilterBefore나 addFilterAfter를 사용해야 한다.
- SpringSecurityFilter보다 이전, 이후에 실행
addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)
2. FilterConfig 생성
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<MyFilter2> filter2() {
FilterRegistrationBean<MyFilter2> bean = new FilterRegistrationBean<>(new MyFilter2());
bean.addUrlPatterns("/*"); // 모든 요청에 대해서 필터 적용
bean.setOrder(0); // 낮은 숫자일수록 우선순위가 높음
return bean;
}
}
- SecurityConfg에서 추가하지 않고, 따로 FilterConfig를 생성하여 등록하는 방법
- FilterRegistrationBean을 생성하여 MyFilter1을 빈으로 등록시켜준다.
필터 실행
- MyFilter1: SecurityConfg에 등록한 필터
http.addFilterBefore(new MyFilter1(), UsernamePasswordAuthenticationFilter.class);
- MyFilter2: FilterRegistrationBean에 등록한 필터
public FilterRegistrationBean<MyFilter2> filter2() { }
- FilterRegistrationBean에 등록한 필터보다 SpringSecurityFilter에 등록한 필터가 가장 최우선 순위에 있다.
- 결국 SpringSecurityFilter가 직접 만든 MyFilter2보다 먼저 실행된다.
참고
- https://docs.spring.io/spring-security/reference/servlet/architecture.html
- [이론편] 스프링 시큐리티란 ?
- [Spring Security] Filter란?
- Spring Security, 제대로 이해하기 - FilterChain
'Web > Spring, JPA' 카테고리의 다른 글
[Spring Websocket] 채팅 서버 구축 (2) - STOMP (0) | 2023.11.23 |
---|---|
[Spring Websocket] 채팅 서버 구축 (1) - Websocket (0) | 2023.11.22 |
[JPA] Entity 생성 방법 (0) | 2023.08.29 |
[아키텍쳐] 스프링 패키지 구조(계층형과 도메인형) (0) | 2023.08.22 |
[Spring Boot] 내장 톰캣 vs 외장 톰캣 (1) | 2022.09.02 |