https://product.kyobobook.co.kr/detail/S000201766024
이번 방학 동안 이 책으로 스프링 부트 3을 공부해 보려고 합니다. 이 책은 스프링 부트 3을 사용해 웹페이지를 직접 구현해 봅니다. 그중 스프링 시큐리티로 로그인 및 회원가입을 구현하는 부분이 나옵니다.
그래서 이번 기회에 스프링 시큐리티를 이 책을 통해 공부하고 정리해 보려고 합니다.
스프링 시큐리티란?
스프링 시큐리티(Spring Security)는 Java 기반의 애플리케이션에서 보안을 구현하기 위해 사용되는 강력하고 포괄적인 보안 프레임워크입니다. 스프링 시큐리티는 인증(Authentication)과 권한 부여(Authorization)를 처리하여 애플리케이션의 보안 요구 사항을 충족시킵니다.
스프링 시큐리티가 제공하는 기능
- 인증(Authentication) : 사용자의 신원 확인을 위한 인증 기능을 제공합니다. 사용자가 제공한 인증 정보(예: 아이디, 비밀번호)를 검증하여 사용자가 신뢰할 수 있는지 확인합니다. 스프링 시큐리티는 다양한 인증 방식을 지원하며, 사용자 정의 인증 로직을 구현할 수도 있습니다.
- 권한 부여(Authorization) : 인증된 사용자에 대한 권한을 관리하고, 액세스 제어를 구현합니다. 애플리케이션의 리소스(예: URL, 메서드)에 대한 접근 권한을 설정하고, 사용자의 역할(Role) 또는 특정 조건에 따라 접근을 허용하거나 거부할 수 있습니다.
- 보안 구성(Configuration) : 스프링 시큐리티는 XML 또는 자바 기반의 구성을 사용하여 보안 규칙과 정책을 정의할 수 있습니다. 보안 구성은 인증 및 권한 부여를 위한 필터 체인, 암호화 방법, 세션 관리, 로그인 페이지 등과 관련된 설정을 포함합니다.
- 웹 보안(Web Security) : 스프링 시큐리티는 웹 애플리케이션에서 특히 사용되는 웹 보안 기능을 제공합니다. CSRF(Cross-Site Request Forgery) 공격 방어, 세션 관리, 보안 헤더 설정, 사용자 인증을 위한 필터 등의 기능을 지원합니다.
CSRF 공격과 세션 고정 공격이란?
CSRF 공격은 사용자의 권한을 가지고 특정 동작을 수행하도록 유도하는 공격을 말합니다.
세션 고정 공격은 사용자의 인증 정보를 탈취하거나 변조하는 공격을 말합니다.
필터 기반으로 동작하는 스프링 시큐리티
위 그림은 스프링 시큐리티 구성입니다. 스프링 시큐리티는 이렇게 다양한 필터들로 구성됩니다.
SecurityContextPersistenceFilter에서부터 시작해 FilterSecurityInterceptor까지 순서대로 필터를 거칩니다.
필터를 실행할 때는 오른쪽에 연결된 클래스를 거치며 실행합니다.
이 책에서 설명한 중요한 부분은 UsernamePassowrdAuthenticationFilter와 FilterSecurityInterceptor입니다.
- UsernamePassowrdAuthenticationFilter : 아이디와 패스워드가 넘어오면 인증 요청을 위임하는 인증 관리자 역할을 합니다.
- FilterSecurityInterceptor : 권한 부여 처리를 위임해 접근 제어 결정을 쉽게 하는 접근 결정 관리자 역할을 합니다.
필터 구성요소 설명
필터명 | 설명 |
SecurityContextPersistenceFilter | 요청 처리 중에 현재 사용자의 SecurityContext(보안 컨텍스트)를 유지하도록 도와줍니다. SecurityContext는 현재 인증된 사용자와 해당 사용자의 인증 정보를 포함합니다. 요청을 처리하는 동안 SecurityContext의 상태를 유지하기 위해 필요한 필터입니다. |
LogoutFilter | 로그아웃 처리를 담당하는 필터입니다. 로그아웃 요청이 들어오면 현재 인증된 사용자의 인증 정보를 제거하고 세션을 무효화합니다. 로그아웃 후에는 보통 로그인 페이지 또는 로그아웃 성공 페이지로 리다이렉트합니다. |
UsernamePasswordAuthenticationFilter | 사용자의 아이디와 비밀번호를 사용하여 인증을 처리하는 필터입니다. 로그인 폼에서 제출된 아이디와 비밀번호를 검증하여 인증을 시도합니다. 인증에 성공하면 해당 사용자의 인증 정보를 SecurityContext에 저장합니다. 인증에 성공하면 AuthenticationSecuritySuccessHandler를, 인증에 실패하면 AuthenticationFailureHandler를 실행합니다. |
DefaultLoginPageGeneratingFilter | 기본 로그인 페이지를 생성하는 필터입니다. 로그인 페이지가 없을 경우, 스프링 시큐리티가 제공하는 기본 로그인 페이지를 생성합니다. 커스터마이징된 로그인 페이지를 사용하려면 이 필터를 제거하거나 대체해야 할 수도 있습니다. |
BasicAuthenticationFilter | HTTP 기본 인증(Basic Authentication)을 처리하는 필터입니다. 클라이언트가 요청에 Basic Authentication 헤더를 포함시키면, 해당 헤더를 검증하여 인증을 시도합니다. |
RequestCacheAwareFilter | 요청 캐시를 처리하는 필터입니다. 인증이 필요한 요청에서 인증되지 않은 경우, 인증 후에 원래의 요청으로 리디렉트하기 위해 요청을 캐시에 저장합니다. 예를 들어 로그인하지 않은 상태로 방문했던 페이지를 기억해 두었다가 로그인 이후에 그 페이지로 이동 시켜줍니다. |
RememberMeAuthenticationFilter | RememberMeAuthenticationFilter는 "Remember Me" 기능을 처리하는 필터입니다. 사용자가 로그인 후에 "Remember Me" 옵션을 선택하면, RememberMeAuthenticationFilter는 사용자를 자동으로 인증합니다. 이 필터는 쿠키 또는 토큰 기반의 "Remember Me" 인증을 처리하고, 유효한 "Remember Me" 정보가 있는 경우 사용자를 자동으로 인증합니다. 사용자의 세션이 만료되거나 로그아웃되면 RememberMeAuthenticationFilter는 해당 정보를 무효화합니다 |
SecurityContextHolderAwareRequestFilter | SecurityContextHolderAwareRequestWrapper를 사용하여 요청을 래핑하는 필터입니다. 래핑된 요청은 현재 사용자의 SecurityContext에 액세스할 수 있는 메서드를 제공합니다. 필터 체인 상의 다음 필터들에게 부가 정보를 제공합니다. |
AnonymousAuthenticationFilter | 인증되지 않은 익명 사용자를 처리하는 필터입니다. 익명 사용자는 인증되지 않은 상태로 애플리케이션에 접근할 수 있도록 합니다. 인증된 사용자와 익명 사용자를 구분하여 처리할 수 있게 해줍니다. |
SessionManagementFilter | 세션 관리를 처리하는 필터입니다. 동시에 여러 사용자 세션을 관리하거나, 세션 고정 공격(Session Fixation Attack)을 방지하는 등의 기능을 수행합니다. 유효하지 않은 세션에 대한 처리를 하고, 세션 생성 전략을 세우는 등의 작업을 처리합니다. |
ExceptionTranslationFilter | 예외 처리를 담당하는 필터입니다. 인증이나 권한 부여와 관련된 예외를 처리하여 적절한 응답을 반환합니다. 인증이 필요한 리소스에 접근할 때 인증 예외가 발생하거나, 권한 부여에 실패할 경우 예외 처리를 담당합니다. |
FilterSecurityInterceptor | 가장 중요한 보안 필터로, URL 기반의 접근 제어를 담당합니다. 요청된 URL과 사용자의 권한 정보를 비교하여 접근을 허용하거나 거부합니다. 인증 및 권한 검사 이전에 다른 필터들이 처리한 정보를 기반으로 결정을 내립니다. 즉, 인가 관련 설정할 수 있습니다. |
아이디와 패스워드 기반 폼 로그인 과정
가장 많이 사용하는 아이디와 패스워드 기반 폼 로그인을 시도하면 스프링 시큐리티에서는 어떤 절차로 인증 처리를 하는지 알아보려고 합니다. 이 책에서 설명해 주는 내용입니다.
- 사용자가 폼에 아이디와 패스워드를 입력하면, HTTPServletRequest에 아이디와 비밀번호 정보가 전달됩니다. 이때 AuthenticationFilter가 넘어온 아이디와 비밀번호의 유효성 검사를 합니다.
- 유효성 검사가 끝나면 실제 구현채인 UsernamePasswordAuthenticationToken을 만들어 넘겨줍니다.
- 이 토큰을 AuthenticationManager에게 보냅니다.
- 이 토큰을 다시 AuthenticationProvider에게 보냅니다.
- 사용자 아이디를 UserDetailService에 보냅니다. UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 UserDetails 객체로 만들어 AuthenticationProivder에게 전달합니다.
- DB에 있는 사용자 정보를 가져옵니다.
- 입력 정보와 UserDetails의 정보를 비교해 실제 인증 처리를 합니다.
- 이제 SecurityContextHolder에 Authentication을 저장합니다.
- 인증 성공 여부에 따라 성공하면 AuthenticationSuccessHandler를 실패하면 AuthenticationFailureHandler 핸들러를 실행합니다.
직접 구현해보기
의존성 추가하기
// 스프링 시큐리티를 사용하기 위한 스타터 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
// 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
// 스프링 시큐리티를 테스트하기 위한 의존성 추가
testImplementation 'org.springframework.security:spring-security-test'
인증 객체 만들기
원래 UserEntity는 많이 만들어 봤었다. 스프링 시큐리티에서는 위에 방법에서 봤듯이 UserDetails를 통해 인증을 해야 하므로 UserDetails를 상속받아야 한다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member extends BaseTimeEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name="password")
private String password;
@Builder
public Member(String email, String password, String auth){
this.email = email;
this.password = password;
}
@Override // 권한 반환
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("user"));
}
@Override
public String getUsername() {
return email;
}
// 사용자의 패스워드를 반환
@Override
public String getPassword() {
return password;
}
//계정 만료 여부 반환
@Override
public boolean isAccountNonExpired() {
// 만료되었는지 확인하는 로직
return true; // true -> 만료되지 않았음
}
// 계정 잠금 여부 반환
@Override
public boolean isAccountNonLocked() {
// 계정 잠금되었는지 확인하는 로직
return true; // true -> 잠금되지 않았음
}
// 패스워드의 만료 여부 반환
@Override
public boolean isCredentialsNonExpired() {
// 패스워드가 만료되었는지 확인하는 로직
return true; // true -> 만료되지 않았음
}
// 계정 사용 가능 여부 반환
@Override
public boolean isEnabled() {
// 계정이 사용 가능하지 확인하는 로직
return true; // true -> 사용 가능
}
}
시큐리티 설정하기
예전에 프로젝트를 할 때 시큐리티를 설정했던 적이 있습니다. 하지만 스프링부트 3으로 넘어오면서 설정하는 형식이 바뀌었습니다. 원래는 WebSecurityConfigurerAdapte를 상속받아 설정했지만 이제는 @Bean을 통해 등록해주어야 합니다.
// 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure(){
return (web) -> web.ignoring()
.requestMatchers("/static/**");
}
스프링 시큐리티의 모든 기능을 사용하지 않게 설정하는 코드입니다. 이 책에서 보통 static처럼 정적 리소스에 설정한다고 합니다.
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
return http
.authorizeRequests()
.requestMatchers("/login", "/signup", "/user").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/articles")
.and()
.logout()
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
.and()
.csrf().disable()
.build();
}
특정 HTTP 요청에 대해 웹 기반 보안을 구성합니다.
- requestMatchers는 특정 요청과 일치하는 url에 대한 액세스를 설정합니다.
- permitAll()은 누구나 접근이 가능하게 합니다.
- anyRequest(). authenticated()는 별도의 인가는 필요하지 않지만 인증이 접근할 수 있습니다.
- formLogin()은 로그인 페이지 경로를 설정하고 로그인이 완료되었을 때 이동할 경로를 설정합니다.
- logout()은 로그아웃 할 때 어디로 가야 할지와 로그아웃 후에 Session에 전체 삭제할지 여부를 설정합니다.
// 인증 관리자 관련 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder,
UserDetailsService userDetailsService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(memberService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
// 패스워드 인코더로 사용할 빈 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
이렇게 설정하고 각각 Controller를 통해 API들을 만들어서 호출해 주면 됩니다. 나머지 내용은 책의 내용이라 블로그에 모두 올리면 안 될 것 같아 구현만 해봤습니다.
결과
이렇게 회원가입 페이지에 입력을 하면
이렇게 비밀번호가 암호화 해서 들어갑니다!!
이제 JWT를 사용해 인증,인가를 구현해볼 것입니다. 지금은 글을 쓰면 아무나 수정과 삭제가 가능하기 때문에 문제가 있습니다.
'토이프로젝트 > 나만의 프로젝트' 카테고리의 다른 글
스프링 부트로 채팅 서버 구현하기 (0) | 2023.07.15 |
---|---|
토큰 기반 인증으로 로그인 업그레이드 하기(JWT) (0) | 2023.06.28 |
테스트 코드 공부 (0) | 2023.06.21 |
Docker로 CI/CD 구축하기 (0) | 2023.06.20 |
데이터베이스 정규화 하기 (2) | 2023.04.29 |