SpringBoot환경에서 Spring Security기반에 JWT 인증 방식을 구현하도록 하겠다.
이번 포스팅에서는 Spring Security에 대한 내용이다.
Spring Security, JWT 개념과 JWT 구현에 대한 내용은 아래 포스팅을 참고하기 바란다.
Spring Security 개념 및 Architecture
Spring Security + JWT 인증 (1/2) - JWT 구현
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
permit:
url:
/favicon.ico,
/auth/**
Spring Security와 JWT인증 로직에서 제외되어야 하는 URL 패턴을 정의한다.
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.devlog.common.entity.User;
import com.devlog.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
/**
* DaoAuthenticationProvider에 의해 실행됨.
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("CustomUserDetailsService loadUserByUsername : " + username);
User user = userRepository.findByEmail(username);
if(user == null) {
throw new UsernameNotFoundException("Check username");
}
//UserDetails 생성
PrincipalDetails principalDetails = new PrincipalDetails(user);
BeanUtils.copyProperties(user, principalDetails);
return principalDetails;
}
}
UserDetailsService를 상속받아 구현한다.
UserDetailsService의 구현체인 CustomUserDetailsService은 로그인 인증처리에서 실행되어 사용자 정보를 조회하여 반환한다.
loadUserByUsername
Spring Security의 인증 과정에서 사용자의 세부 정보를 조회하는 중요한 역할을 한다.
username은 사용자 명을 뜻하는 것이 아닌 인증 전 객체 생성 시 사용된 접근 주체(principal)의 값이다.
즉, 사용자의 key 값이다.
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(requestBody.get("email"), requestBody.get("password"));
인증 전 객체를 생성할때 email이 접근주체(principal)로 사용되고 있다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.devlog.common.filter.JwtAuthenticationFilter;
import com.devlog.common.jwt.JwtAccessDeniedHandler;
import com.devlog.common.jwt.JwtAuthenticationEntryPoint;
import com.devlog.common.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfiguration {
@Value("${permit.url}")
private final String[] PERMIT_ALL_RESOURCES;
private final JwtProvider jwtProvider;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// JWT를 사용할 것이기 때문에 HTTP Basic 인증 disable
.httpBasic(httpBasic -> httpBasic.disable())
// security의 기본 로그인 화면을 비활성화
.formLogin(formLogin -> formLogin.disable())
// 모든요청에 JWT 인증을 하므로, CSRF 보호가 필요하지 않다.
.csrf(csrf -> csrf.disable())
// JWT 인증방식으로 Session은 필요 없음
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// request 사용권한 체크
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(this.getPermitAllResources()).permitAll()
.requestMatchers(new AntPathRequestMatcher("/admin/**")).hasAuthority("ADMIN")
.requestMatchers(new AntPathRequestMatcher("/api/v1/**")).hasAnyAuthority("USER", "ADMIN")
.anyRequest().authenticated())
// 인증, 인가 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정
.exceptionHandling(exception -> exception
.accessDeniedHandler(new JwtAccessDeniedHandler()) //인가(Authorization) 실패시
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())) //인증(Authentication) 실패시
// JWT 인증 필터를 id/password 인증 필터 이전에 추가
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider, PERMIT_ALL_RESOURCES), UsernamePasswordAuthenticationFilter.class);
;
return httpSecurity.build();
}
private AntPathRequestMatcher[] getPermitAllResources() {
AntPathRequestMatcher antPathRequestMatcherArr[] = new AntPathRequestMatcher[PERMIT_ALL_RESOURCES.length];
for(int nIdx = 0 ; nIdx < PERMIT_ALL_RESOURCES.length ; nIdx++) {
antPathRequestMatcherArr[nIdx] = new AntPathRequestMatcher(PERMIT_ALL_RESOURCES[nIdx]);
}
return antPathRequestMatcherArr;
}
}
@EnableWebSecurity
PasswordEncoder
HTTP Basic
HTTP Basic인증 이란 클라이언트가 서버에 요청을 보낼 때 Authorization 헤더에 Basic이라는 접두사와 함께 사용자 이름과 비밀번호를 Base64로 인코딩한 문자열을 포함시킨가. 서버는 이 인코딩 된 문자열을 디코딩하여 인증하는 방식이다.
ex) Authorization: Basic YWRtaW46cGFzc3dvcmQ=
OAuth2, JWT 등의 다른 인증 방식을 사용하고자 할 때 HTTP Basic 인증을 비활성화한다.
.httpBasic(httpBasic -> httpBasic.disable())
CSRF
CSRF(Cross-Site Request Forgery)는 웹 애플리케이션에서 악의적인 사용자가 다른 사용자를 가장해 요청을 보내는 공격 기법이다.
CSRF 보호는 웹 애플리케이션에서 중요한 보안 기능으로, 클라이언트가 서버로 의도치 않은 요청을 보내는 것을 방지한다. 대부분의 경우, CSRF 보호를 활성화해 두는 것이 안전하지만, RESTful API와 같이 필요하지 않은 경우에는 비활성화할 수 있다.
CSRF 공격의 Example
sessionManagement
사용자의 세션을 관리하는 방법을 설정하는데 사용된다.
authorizeHttpRequests
HTTP 요청에 대한 권한을 설정하고 관리하는 데 사용되는 구성 메서드로 URL 패턴과 권한을 매핑하는 역할을 한다.
특정 URL은 인증된 사용자만 접근할 수 있도록 하거나, 특정 역할을 가진 사용자만 접근할 수 있도록 설정할 수 있다.
requestMatchers
특정 URL 패턴에 대해 권한을 설정한다.
anyRequest().authenticated()
정의된 패턴을 제외한 모든 요청에 대해 인증을 요구
exceptionHandling
인증 및 권한 부여 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정하는 기능이다. 이를 통해 사용자에게 적절한 오류 메시지를 표시하고, 특정 오류 상황에 맞는 행동을 정의할 수 있다.
//인증, 인가 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정
.exceptionHandling(exception -> exception
.accessDeniedHandler(new JwtAccessDeniedHandler()) //인가(Authorization) 실패시
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())) //인증(Authentication) 실패시
JwtAccessDeniedHandler, JwtAuthenticationEntryPoint 객체는 JWT 구현시 작성하였다.
간단하게 로그만 찍었지만 상황에 맞게 특정 페이지로 리다이렉션 처리를 할 수 있다.
Spring Security + JWT 인증 (1/2) - JWT 구현
addFilterBefore
필터를 추가하는 데 사용되는 메서드로 특정 필터를 기존 필터 체인의 특정 위치에 삽입할 수 있다.
// JWT 인증 필터를 id/password 인증 필터 이전에 추가
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider, PERMIT_ALL_RESOURCES), UsernamePasswordAuthenticationFilter.class);
JWT 인증 필터를 UsernamePasswordAuthenticationFilter앞에 추가
1. 로그인(USER 권한) 후 Access Token, Refresh Token 확인
AccessToken Header에서 추출하여 확인
RefreshToken DB값과 Response값 확인
2. /api/v1/** 요청 - 성공
3. /admin/** 요청 - 실패
ADMIN 권한이 없기 때문에 인가(Authorization)가 실패하여 ExceptionHandling으로 등록한 JwtAccessDeniedHandler이 실행된 것을 확인할 수 있다.
1. 로그인(ADMIN권한) 후 Access Token, Refresh Token 확인
AccessToken Header에서 추출하여 확인
RefreshToken DB값과 Response값 확인
2. /api/v1/** 요청 - 성공
3. /admin/** 요청 - 성공
AuthroizationFilter를 이용한 인가(AuthorizationManager, RequestMatcherDelegatingAuthorizationManager) (0) | 2024.12.01 |
---|---|
Spring Security - FilterSecurityInterceptor 이해 및 Example (2) | 2024.11.11 |
Spring Security + JWT 인증 (1/2) - JWT 구현 (25) | 2024.08.20 |
Spring Security 개념 및 Architecture (28) | 2024.08.19 |
[Spring] JWT(Json Web Token) 정리 (35) | 2024.08.15 |
댓글 영역