SpringBoot에서 Spring Security를 사용하는 방법을 알아보자
📌 서론
Spring Boot 3 프로젝트에서 Thymeleaf와 함께 JWT (JSON Web Token) 인증 방식을 적용했다. 일반적으로, JWT는 클라이언트 사이드 렌더링(CSR) 환경인 React, Vue, Android, iOS 앱에서 주로 사용된다. 이 방식에서 클라이언트는 인증에 필요한 정보를 담은 토큰을 사용하여 서버와 인증을 수행한다. 반면, 서버 사이드 렌더링(SSR) 환경에서는 주로 세션을 통한 인증 방식이 일반적이다. 이번 프로젝트에서는 SSR 환경에도 JWT를 적용해 본 결과, 서버가 페이지를 렌더링 하면서도 JWT를 통한 인증이 가능함을 확인했다. 지금부터 그 내용을 공유한다.
Spring security6의 FilterChain방식👇🏻👇🏻
Spring Security를 적용한 프로젝트의 깃허브 링크
시큐리티 관련 패키지 경로: java/com/jinan/profile/config 이 경로에 security패키지가 존재합니다.
주의사항!!
이 프로젝트에서 인터셉터와 필터를 모두 사용하는데 Spring Security에서는 Filter를 주로 사용하니 인터셉터 관련 코드는 작성하지 않고 진행하셔도 문제없습니다. (죄송합니다 ㅠㅠ)
1. 프로젝트 구성
1-1. 프로젝트 구성은 다음과 같다.
SpringBoot | 3.1.1 |
Gradle | 8.1.1 |
MVC | spring-boot-starter-web |
JPA | spring-boot-starter-data-jpa |
DB | MySQL , 내장 H2 |
Security | Spring Security6 |
jwt | 0.11.2 |
validation | spring-boot-starter-validation |
lombok | org.projectlombok:lombok |
thymeleaf + security | spring-boot-starter-thymeleaf thymeleaf-extras-springsecurity6 |
JAXB | javax.xml.bind:jaxb-api:2.3.1 com.sun.xml.bind:jaxb-impl:2.3.1 com.sun.xml.bind:jaxb-core:2.3.0.1 |
시큐리티 버전의 변천사를 공식 문서에서 가져왔다.
2. 로그인에 필요한 Entity와 dto 생성하기
2-1. User 엔티티 생성
- User 엔티티를 생성한다. 기본적으로 유저정보에 필요한 내용들을 담았다.
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "USERS")
@Getter
@Entity
public class User extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id; // 유저pk
@Column(name = "login_id", nullable = false)
private String loginId; // 로그인 ID
@Column(name = "password", nullable = false)
private String password; // 로그인 비밀번호
@Column(name = "username", nullable = false)
private String username; // 유저실명
@Column(name = "email", nullable = false)
private String email; // 이메일
@Column(name = "role", nullable = false)
@Enumerated(EnumType.STRING)
private RoleType roleType; // 계정 타입
@Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING)
private UserStatus userStatus;
// id, 생성일자, 수정일자는 자동으로 등록된다.
@Builder
private User(Long id, String loginId, String password, String username, String email, RoleType roleType, UserStatus userStatus) {
this.id = id;
this.loginId = loginId;
this.password = password;
this.username = username;
this.email = email;
this.roleType = roleType;
this.userStatus = userStatus;
}
}
2-2. RoleTyper과 UserStatus선언
- RoleTyper과 UserStatus는 Enum 타입으로 다음과 같이 선언했다.
package com.jinan.profile.domain.user.constant;
@Getter
@AllArgsConstructor
public enum RoleType {
ADMIN, USER
}
package com.jinan.profile.domain.user.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum UserStatus {
D,
Y
}
2-3. User 엔티티 전용 Dto 객체 선언
- 다음으로 UserDto를 생성한다. 불변 객체인 record타입을 사용해서 dto를 생성했다. 또한 factory method 패턴을 사용하여 생성자를 만들었다.
factory method 패턴이 무엇인지 궁금하다면 아래의 글을 확인해보자👇🏻👇🏻
public record UserDto(
Long userId,
String loginId,
String username,
String password,
UserStatus status,
String email,
RoleType roleType,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
// factory method of 선언
public static UserDto of(Long userId, String loginId, String username, String password, UserStatus status, String email, RoleType roleType, LocalDateTime createdAt, LocalDateTime updatedAt) {
return new UserDto(userId, loginId, username, password, status, email, roleType, createdAt, updatedAt);
}
// security에서 사용할 팩토리 메서드
public static UserDto of(String loginId) {
return new UserDto(
null, loginId, null, null, null, null, null, null, null
);
}
}
3. Spring Security 설정하기
프로젝트에 config 패키지를 생성하고 내부에 SecurityConfig 클래스를 생성한다.
스프링 시큐리티 config를 작성하기 전 알고 넘어가야하는 것이 있다. Spring Security 5.7 이후부터 @Bean으로 SecurityFilterChain을 구현해서 시큐리티를 적용시키는 방법을 권장하기 때문에 필터체인 구성을 extends로 하는 이전방식을 사용하지 않고 빈등록 방식으로 코드를 작성했다.
Spring Security 5.7이후부터 @Bean으로 등록하는 이유는 아래의 포스트를 확인하자!
3-1. WebSecurityCustomizer 코드 작성
- 정적 자원(static)에 대해서는 보안을 적용하지 않도록 한다.
@Slf4j
@Configuration
public class SecurityConfig {
/**
* 이 메서드는 정적 자원에 대해 보안을 적용하지 않도록 설정한다.
* 정적 자원은 보통 HTML, CSS, JavaScript, 이미지 파일 등을 의미하며, 이들에 대해 보안을 적용하지 않음으로써 성능을 향상시킬 수 있다.
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
CustomAuthenticationFilter customAuthenticationFilter,
JwtAuthorizationFilter jwtAuthorizationFilter
) throws Exception {
log.debug("[+] WebSecurityConfig Start !!! ");
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/resources/**").permitAll()
.requestMatchers("/main/rootPage").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthorizationFilter, BasicAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(login -> login
.loginPage("/login")
.successHandler(new SimpleUrlAuthenticationSuccessHandler("/main/rootPage"))
.permitAll()
)
.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
3-2. csrf 설정코드 작성
- CSRF(Cross-Site Request Forgery)는 웹 애플리케이션의 취약점 중 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 하도록 만드는 공격이다. 이 설정은 CSRF 보호 기능을 비활성화한다. 이렇게 설정하면, CSRF 토큰 없이도 요청을 처리할 수 있게 된다.
.csrf(AbstractHttpConfigurer::disable)
3-3. cors 설정코드 작성
- CORS(Cross-Origin Resource Sharing)는 다른 도메인의 리소스에 웹 페이지가 접근할 수 있도록 브라우저에게 권한을 부여하는 메커니즘이다. 아래의 설정은 특정 CORS 구성 소스(corsConfigurationSource())를 사용하여 CORS 설정을 적용한다.
Cors에 대한 이해는 아래의 글을 읽어보자👇🏻👇🏻
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
- 위에서 사용하는 cors 코드는 아래와 같이 작성했다. 이렇게 따로 빼서 관리하면 언제든지 편하게 수정할 수 있다는 장점이 있다.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Content-Type", "Authorization", "X-XSRF-token"));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
3-4. authorizeHttpRequests 설정코드 작성
- 이 설정은 HTTP 요청에 대한 인증 및 권한을 정의한다.
- "/resources/**"에 있는 자원에 대한 접근이나 "/main/rootPage" 경로로 들어오는 요청은 모든 사용자에게 허용된도록 설정했다. 그 외의 모든 요청은 인증된 사용자만 접근할 수 있도록 설정했다.
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/resources/**").permitAll()
.requestMatchers("/main/rootPage").permitAll()
.anyRequest().authenticated()
)
3-5. addFIlterBefore 설정코드 작성
- JwtAuthorizationFilter를 BasicAuthenticationFilter전에 실행되도록 작성했다. 이 필터는 요청 헤더에서 JWT 토큰을 추출하고 해당 토큰의 유효성을 검증하는 역할을 한다.
// 1. 먼저, JwtAuthorizationFilter가 실행되어 요청 헤더에서 JWT 토큰을 추출하고 이 토큰을 검증한다.
.addFilterBefore(jwtAuthorizationFilter, BasicAuthenticationFilter.class)
- jwt인증필터(JwtAuthorizationFilter) 또한 SecurityConfig클래스 안에 @Bean으로 등록시켜 줬다.
/**
* "JWT 토큰을 통하여서 사용자를 인증한다." -> 이 메서드는 JWT 인증 필터를 생성한다.
* JWT 인증 필터는 요청 헤더의 JWT 토큰을 검증하고, 토큰이 유효하면 토큰에서 사용자의 정보와 권한을 추출하여 SecurityContext에 저장한다.
*/
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter(CustomUserDetailsService userDetailsService) {
return new JwtAuthorizationFilter(userDetailsService);
}
3-6. sessionManagement 설정코드 작성
- 이 설정은 세션 관리 전략을 정의한다. SessionCreationPolicy.STATELESS는 스프링 시큐리티가 세션을 생성하거나 사용하지 않도록 설정한다. 이는 주로 JWT와 같은 토큰 기반 인증에서 사용된다.
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
3-7. formLogin 설정코드 작성
- 이 설정은 form 기반으로 진행하는 로그인에 관한 설정을 정의한다.
- 로그인 페이지는 /login으로 설정된다.
- 로그인 성공 시, /main/rootPage로 리다이렉트 된다.
- 모든 사용자가 로그인 페이지에 접근할 수 있도록 허용된다.
.formLogin(login -> login
.loginPage("/login")
.successHandler(new SimpleUrlAuthenticationSuccessHandler("/main/rootPage"))
.permitAll()
)
3-8. addFilterBefore 설정코드 작성
- CustomAuthenticationFilter는 UsernamePasswordAuthenticationFilter 전에 실행된다. 이 사용자 정의 필터는 폼 기반 인증을 처리하며, HTTP 요청에서 사용자 이름과 비밀번호를 추출하여 인증을 시도한다.
// 2. CustomAuthenticationFilter가 실행되어 사용자 이름과 비밀번호를 사용하여 인증을 수행한다. 이 필터는 주로 로그인 요청을 처리하는 데 사용된다.
.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
- 이 커스텀 필터 또한 SecurityConfig에서 @Bean으로 등록해 준다. -> 클래스가 따로 있지만 초기설정값이 있어서 여기서 빈으로 등록한다.
/**
* 1. 커스텀을 수행한 '인증' 필터로 접근 URL, 데이터 전달방식(form) 등 인증 과정 및 인증 후 처리에 대한 설정을 구성하는 메서드다.
* 이 메서드는 사용자 정의 인증 필터를 생성한다. 이 필터는 로그인 요청을 처리하고, 인증 성공/실패 핸들러를 설정한다.
*
* @return CustomAuthenticationFilter
*/
@Bean
public CustomAuthenticationFilter customAuthenticationFilter(
AuthenticationManager authenticationManager,
CustomAuthSuccessHandler customAuthSuccessHandler,
CustomAuthFailureHandler customAuthFailureHandler
) {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager);
// "/user/login" 엔드포인트로 들어오는 요청을 CustomAuthenticationFilter에서 처리하도록 지정한다.
customAuthenticationFilter.setFilterProcessesUrl("/user/login");
customAuthenticationFilter.setAuthenticationSuccessHandler(customAuthSuccessHandler); // '인증' 성공 시 해당 핸들러로 처리를 전가한다.
customAuthenticationFilter.setAuthenticationFailureHandler(customAuthFailureHandler); // '인증' 실패 시 해당 핸들러로 처리를 전가한다.
customAuthenticationFilter.afterPropertiesSet();
return customAuthenticationFilter;
}
3-9. 남은 설정코드 작성 @Bean으로 등록시키기
- 이 코드는 SecurityConfig의 전체 코드이다. 이대로 넣고 사용하려고 하면 동작하지 않을 것이다. 왜냐하면 아직은 필요한 다른 클래스를 모두 생성해주지 않았기 때문이다. 이후의 내용은 다음 포스트에서 이어가도록 한다.
@Slf4j
@Configuration
public class SecurityConfig {
/**
* 이 메서드는 정적 자원에 대해 보안을 적용하지 않도록 설정한다.
* 정적 자원은 보통 HTML, CSS, JavaScript, 이미지 파일 등을 의미하며, 이들에 대해 보안을 적용하지 않음으로써 성능을 향상시킬 수 있다.
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Content-Type", "Authorization", "X-XSRF-token"));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* Spring Security 설정을 정의하는 메서드.
* <ul>
* <li>CSRF 방지 기능 비활성화</li>
* <li>CORS 설정 적용</li>
* <li>`/resources/**`, `/main/rootPage` 경로 접근 허용</li>
* <li>나머지 요청은 인증된 사용자만 접근 허용</li>
* <li>헤더의 JWT 토큰을 추출하고 검증하는 `JwtAuthorizationFilter` 적용</li>
* <li>세션을 상태 없이 관리</li>
* <li>로그인 페이지 설정 및 로그인 성공 시 리다이렉트 경로 설정</li>
* <li>사용자 이름과 비밀번호 인증을 위한 `CustomAuthenticationFilter` 적용</li>
* </ul>
*
* @param http Spring Security의 HttpSecurity 객체
* @param customAuthenticationFilter 사용자 정의 인증 필터
* @param jwtAuthorizationFilter JWT 인증 필터
* @return 구성된 SecurityFilterChain 객체
* @throws Exception 설정 중 발생할 수 있는 예외
*/
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
CustomAuthenticationFilter customAuthenticationFilter,
JwtAuthorizationFilter jwtAuthorizationFilter
) throws Exception {
log.debug("[+] WebSecurityConfig Start !!! ");
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/resources/**", "/static/**").permitAll()
.requestMatchers("/css/**").permitAll()
.requestMatchers("/main/rootPage").permitAll()
.requestMatchers("/error.html").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthorizationFilter, BasicAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(login -> login
.loginPage("/login")
.successHandler(new SimpleUrlAuthenticationSuccessHandler("/main/rootPage"))
.permitAll()
)
.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
/**
* 1. 커스텀을 수행한 '인증' 필터로 접근 URL, 데이터 전달방식(form) 등 인증 과정 및 인증 후 처리에 대한 설정을 구성하는 메서드다.
* 이 메서드는 사용자 정의 인증 필터를 생성한다. 이 필터는 로그인 요청을 처리하고, 인증 성공/실패 핸들러를 설정한다.
*
* @return CustomAuthenticationFilter
*/
@Bean
public CustomAuthenticationFilter customAuthenticationFilter(
AuthenticationManager authenticationManager,
CustomAuthSuccessHandler customAuthSuccessHandler,
CustomAuthFailureHandler customAuthFailureHandler
) {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager);
// "/user/login" 엔드포인트로 들어오는 요청을 CustomAuthenticationFilter에서 처리하도록 지정한다.
customAuthenticationFilter.setFilterProcessesUrl("/user/login");
customAuthenticationFilter.setAuthenticationSuccessHandler(customAuthSuccessHandler); // '인증' 성공 시 해당 핸들러로 처리를 전가한다.
customAuthenticationFilter.setAuthenticationFailureHandler(customAuthFailureHandler); // '인증' 실패 시 해당 핸들러로 처리를 전가한다.
customAuthenticationFilter.afterPropertiesSet();
return customAuthenticationFilter;
}
/**
* 2. authenticate 의 인증 메서드를 제공하는 매니져로'Provider'의 인터페이스를 의미한다.
* 이 메서드는 인증 매니저를 생성한다. 인증 매니저는 인증 과정을 처리하는 역할을 한다.
* 과정: CustomAuthenticationFilter → AuthenticationManager(interface) → CustomAuthenticationProvider(implements)
*/
@Bean
public AuthenticationManager authenticationManager(CustomAuthenticationProvider customAuthenticationProvider) {
return new ProviderManager(Collections.singletonList(customAuthenticationProvider));
}
/**
* 3. '인증' 제공자로 사용자의 이름과 비밀번호가 요구된다.
* 이 메서드는 사용자 정의 인증 제공자를 생성한다. 인증 제공자는 사용자 이름과 비밀번호를 사용하여 인증을 수행한다.
* 과정: CustomAuthenticationFilter → AuthenticationManager(interface) → CustomAuthenticationProvider(implements)
*/
@Bean
public CustomAuthenticationProvider customAuthenticationProvider(UserDetailsService userDetailsService) {
return new CustomAuthenticationProvider(
userDetailsService
);
}
/**
* 4. Spring Security 기반의 사용자의 정보가 맞을 경우 수행이 되며 결과값을 리턴해주는 Handler
* customLoginSuccessHandler: 이 메서드는 인증 성공 핸들러를 생성한다. 인증 성공 핸들러는 인증 성공시 수행할 작업을 정의한다.
*/
@Bean
public CustomAuthSuccessHandler customLoginSuccessHandler() {
return new CustomAuthSuccessHandler();
}
/**
* 5. Spring Security 기반의 사용자의 정보가 맞지 않을 경우 수행이 되며 결과값을 리턴해주는 Handler
* customLoginFailureHandler: 이 메서드는 인증 실패 핸들러를 생성한다. 인증 실패 핸들러는 인증 실패시 수행할 작업을 정의한다.
*/
@Bean
public CustomAuthFailureHandler customLoginFailureHandler() {
return new CustomAuthFailureHandler();
}
/**
* "JWT 토큰을 통하여서 사용자를 인증한다." -> 이 메서드는 JWT 인증 필터를 생성한다.
* JWT 인증 필터는 요청 헤더의 JWT 토큰을 검증하고, 토큰이 유효하면 토큰에서 사용자의 정보와 권한을 추출하여 SecurityContext에 저장한다.
*/
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter(CustomUserDetailsService userDetailsService) {
return new JwtAuthorizationFilter(userDetailsService);
}
/**
* isAdmin 메소드는 Supplier<Authentication>와 RequestAuthorizationContext를 인자로 받아서 "ADMIN" 역할을 가진 사용자인지 확인한다.
* 만약 사용자가 "ADMIN" 역할을 가지고 있다면, AuthorizationDecision 객체는 true를 반환하고, 그렇지 않다면 false를 반환한다.
*/
private AuthorizationDecision isAdmin(
Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext requestAuthorizationContext
) {
return new AuthorizationDecision(
authenticationSupplier.get()
.getAuthorities()
.contains(new SimpleGrantedAuthority("ADMIN"))
);
}
}
다음 포스트에서 WebConfig 설정을 진행하자👇🏻👇🏻
'Spring > Spring Security' 카테고리의 다른 글
Spring Boot 3 & Security 6 시리즈: JWT 검증 인터셉터 작성하기 (6편) (0) | 2023.08.07 |
---|---|
Spring Boot 3 & Security 6 시리즈: AuthenticationProvider, 인증 핸들러 구현하기 (5편) (0) | 2023.08.07 |
Spring Boot 3 & Security 6 시리즈: JWT 인증 필터 JwtAuthorizationFilter 작성(4편) (0) | 2023.08.07 |
Spring Boot 3 & Security 6 시리즈: WebConfig 클래스 작성 (3편) (0) | 2023.08.07 |
Spring Boot 3 & Security 6 시리즈: JWT 로그인 폼 구현 (1편) (0) | 2023.08.07 |