
@Configuration / @EnableWebSecurity- 생략가능1.
@Configuration
- 역할:
- 이 애노테이션은 클래스를 스프링의 설정 파일로 표시합니다.
- 보통 자바 기반 설정을 사용하여 빈(Bean)을 정의할 때 사용됩니다.
- 스프링 시큐리티에서는 보안 설정 클래스를 정의할 때 사용됩니다.
- 생략 가능 여부:
- 최신 스프링 부트 프로젝트에서는 Spring Boot가 자동 구성(Autoconfiguration)을 지원하기 때문에 특정 설정이 명시적으로 필요하지 않다면 생략 가능합니다.
- 하지만 보안 관련 세부 사항을 직접 정의해야 할 경우 명시적으로 선언하는 것이 좋습니다.
2.
@EnableWebSecurity
- 역할:
- 스프링 시큐리티의 웹 보안 기능을 활성화합니다.
- 이 애노테이션이 붙은 클래스는 스프링 시큐리티의 설정 클래스임을 나타냅니다.
- 내부적으로
@Configuration과 관련 설정을 포함합니다.- 생략 가능 여부:
- 최신 스프링 부트에서는
spring-boot-starter-security를 의존성으로 포함하면 자동으로 보안 설정이 활성화됩니다.- 기본적인 보안 구성을 사용하는 경우 생략 가능합니다.
- 하지만, 커스터마이징된 보안 구성을 추가해야 한다면 명시적으로 선언하고 추가 설정을 구현해야 합니다.
![]() |
|---|
| 그림: 기본적이 Security 기본 로그인 화면 |
@Configuration
@EnableWebSecurity
public class SecurityConfig {
}
실제 프로젝트에서는 기본 보안 설정으로는 복잡한 보안 요구 사항을 충족하기 어려우므로, 다음과 같은 세부 항목들을 독자적으로 설정해야 합니다. 이를 위해 @Bean 메서드를 사용하여 스프링 필터 체인 객체(SecurityFilterChain)를 생성하고 커스터마이징합니다.
주요 보안 설정 항목
- 요청 인가 (Authorization)
- 각 요청에 대한 접근 권한을 설정합니다.
- 예: URL 패턴별 접근 권한, 특정 역할에 따른 허용/차단.
- 로그인 화면 (Login Page)
- 기본 로그인 화면 대신 사용자 정의 로그인 화면을 제공합니다.
- 커스터마이징된 로그인 페이지 URL을 설정하여 사용자 경험 개선.
- 인가 실패 화면 (Access Denied Page)
- 접근 권한이 없는 사용자가 특정 리소스에 접근하려 할 때 표시할 화면 설정.
- 사용자가 적절한 안내를 받을 수 있도록 오류 페이지를 제공.
- 인증 시 사용하는 데이터 저장 위치 (Authentication Data Store)
- 사용자 인증 정보를 저장하고 확인하는 위치를 설정합니다.
- 일반적으로 메모리(In-Memory), 데이터베이스(Database), 또는 외부 인증 시스템(예: LDAP, OAuth2)을 사용.
요청 인가는 스프링 시큐리티에서 사용자가 특정 요청에 접근할 권한이 있는지 확인하는 보안 설정으로, 애플리케이션의 보안을 강화하는 핵심 기능입니다. SecurityFilterChain을 통해 역할(Role)과 권한(Authority)에 따른 세부 정책을 설정하며, 명확하고 일관된 정책 순서를 통해 의도치 않은 접근 허용이나 차단을 방지하고 안전한 보안 체계를 구축할 수 있습니다.
요청 인가란?
요청 인가와 관련된 요소
ADMIN, USER). hasRole로 접근 제어.READ, WRITE). hasAuthority로 접근 제어.요청 인가의 처리 흐름
요청 인가는 SecurityFilterChain의 Bean 정의에서 설정합니다. 스프링 시큐리티는 URL 패턴과 HTTP 메서드를 기준으로 요청별로 권한을 정의할 수 있습니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.POST, "/admin/**").hasRole("ADMIN") // ADMIN 역할만 POST 요청 허용
.requestMatchers(HttpMethod.GET, "/user/**").hasAnyRole("USER", "ADMIN") // USER 또는 ADMIN 역할 허용
.requestMatchers("/public/**").permitAll() // 누구나 접근 가능
.requestMatchers("/private/**").authenticated() // 인증된 사용자만 접근 가능
.anyRequest().denyAll() // 나머지 모든 요청은 차단
)
.formLogin(); // 기본 로그인 설정
return http.build();
}
| 메서드명 | 설명 | 예시 사용법 |
|---|---|---|
hasRole | 지정한 역할(Role)을 가진 사용자만 접근 가능. | hasRole("ADMIN") |
hasAnyRole | 지정한 역할(Role) 중 하나를 가진 사용자만 접근 가능. | hasAnyRole("ADMIN", "USER") |
hasAuthority | 지정한 권한(Authority)을 가진 사용자만 접근 가능. | hasAuthority("READ") |
hasAnyAuthority | 지정한 권한(Authority) 중 하나를 가진 사용자만 접근 가능. | hasAnyAuthority("READ", "WRITE") |
permitAll | 모든 사용자에게 접근 허용. | permitAll() |
denyAll | 모든 사용자에게 접근 차단. | denyAll() |
isAuthenticated | 인증된 사용자(로그인한 사용자)만 접근 가능. | isAuthenticated() |
isAnonymous | 익명 사용자(로그인하지 않은 사용자)만 접근 가능. | isAnonymous() |
요청 인가는 가장 제한적인 정책부터 가장 포괄적인 정책으로 작성해야 합니다. 순서가 잘못되면 상위 조건이 하위 조건을 무시하거나, 의도치 않은 접근이 허용될 수 있습니다.
| 순서 | 메서드 | 설명 | 예시 코드 |
|---|---|---|---|
| 1 | denyAll | 특정 경로에 대해 무조건 접근 차단. | .requestMatchers("/admin/secret/**").denyAll() |
| 2 | permitAll | 누구나 접근 가능한 경로 설정 (로그인, 회원가입, 에러 페이지 등). | .requestMatchers("/login", "/register").permitAll() |
| 3 | isAnonymous | 익명 사용자(로그인하지 않은 사용자)만 접근 가능. | .requestMatchers("/guest/**").isAnonymous() |
| 4 | isAuthenticated | 인증된 사용자(로그인한 사용자)만 접근 가능. | .requestMatchers("/user/**").isAuthenticated() |
| 5 | hasRole / hasAnyRole | 특정 역할(Role)을 가진 사용자만 접근 가능. 여러 역할 중 하나를 허용하려면 hasAnyRole 사용. | .requestMatchers("/manager/**").hasRole("MANAGER") |
| 6 | hasAuthority / hasAnyAuthority | 세부 권한(Authority)을 기준으로 접근 권한 설정. | .requestMatchers("/edit/**").hasAuthority("EDIT") |
| 7 | anyRequest | 나머지 모든 요청에 대한 기본 정책 설정. | .anyRequest().authenticated() |
스프링 시큐리티에서는 기본적으로 제공되는 로그인 화면을 사용하거나, 애플리케이션 요구사항에 따라 커스텀 로그인 페이지를 설정할 수 있습니다. 로그인 화면은 사용자가 애플리케이션에 인증(로그인)할 수 있도록 하는 진입점입니다.
http
.formLogin(); // 기본 로그인 화면 사용
애플리케이션 요구사항에 맞는 로그인 화면을 설정하려면, 로그인 페이지를 직접 정의하고 스프링 시큐리티에 해당 페이지를 등록해야 합니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requests -> requests
.anyRequest().authenticated() // 모든 요청 인증 필요
)
.formLogin(login -> login
.loginPage("/login") // 커스텀 로그인 페이지 경로
.defaultSuccessUrl("/home") // 로그인 성공 시 이동할 경로
.failureUrl("/login?error") // 로그인 실패 시 이동할 경로
.permitAll() // 로그인 페이지는 누구나 접근 가능
)
.exceptionHandling(handling -> handling
.accessDeniedPage("/display-access-denied") // 인가 실패 시 표시할 페이지 경로
);
return http.build();
}
loginPage("/login"): 커스텀 로그인 페이지 경로를 설정합니다.defaultSuccessUrl("/home"): 로그인 성공 후 이동할 기본 페이지를 설정합니다.failureUrl("/login?error"): 로그인 실패 시 이동할 페이지를 설정합니다.permitAll(): 로그인 페이지는 인증되지 않은 사용자도 접근할 수 있도록 설정합니다. <!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form action="/login" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required><br>
<button type="submit">Login</button>
</form>
<p th:if="${param.error}">Invalid username or password</p>
<p th:if="${param.logout}">You have been logged out</p>
</body>
</html>
action="/login": 스프링 시큐리티가 제공하는 로그인 처리 엔드포인트.name="username" 및 name="password": 스프링 시큐리티에서 기본적으로 요구하는 입력 필드 이름.param.error: 로그인 실패 시 오류 메시지를 표시.param.logout: 로그아웃 성공 메시지를 표시.UserDetails)스프링 시큐리티에서 UserDetails는 사용자 인증과 관련된 정보를 캡슐화한 인터페이스로, 사용자 ID, 비밀번호, 권한 등의 정보를 제공하며, 인증 메커니즘의 핵심 요소로 사용됩니다.
| 그림 출처 : 그림으로 배우는 스프링6 |
|---|
![]() |
UserDetails의 역할username), 비밀번호(password), 권한(roles) 등 인증에 필요한 정보를 제공.UserDetailsService를 통해 데이터베이스나 메모리에서 사용자 정보를 로드하여 인증 과정에서 활용.AuthenticationManager가 인증을 처리할 때 UserDetails를 사용하여 사용자의 인증 여부를 확인.SecurityContextHolder에 사용자 정보를 저장하여 요청 처리 중 참조 가능.UserDetails 인터페이스 구조UserDetails는 인증과 인가에 필요한 사용자 정보를 정의한 인터페이스입니다.
public interface UserDetails {
String getUsername(); // 사용자 이름
String getPassword(); // 비밀번호
Collection<? extends GrantedAuthority> getAuthorities(); // 권한 목록
boolean isAccountNonExpired(); // 계정 만료 여부
boolean isAccountNonLocked(); // 계정 잠김 여부
boolean isCredentialsNonExpired(); // 비밀번호 만료 여부
boolean isEnabled(); // 계정 활성화 여부
}
UserDetailsService 인터페이스UserDetailsService는 사용자 정보를 로드하기 위해 사용하는 스프링 시큐리티의 핵심 인터페이스입니다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
username)을 기반으로 사용자 정보를 조회.UserDetails 반환: 조회된 사용자 정보를 UserDetails로 반환하여 인증 과정에 사용.스프링 시큐리티는 다양한 데이터 소스에서 사용자 정보를 가져오기 위해 몇 가지 기본 구현체를 제공합니다.
InMemoryUserDetailsManager기능:
코드 예제:
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
JdbcUserDetailsManager기능:
코드 예제:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 데이터베이스 또는 외부 API 호출
UserEntity userEntity = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new CustomUserDetails(
userEntity.getUsername(),
userEntity.getPassword(),
userEntity.isEnabled(),
mapRolesToAuthorities(userEntity.getRoles())
);
}
private Collection<? extends GrantedAuthority> mapRolesToAuthorities(List<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
| 클라이언트 요청부터 UserDetails 흐름도 |
|---|
![]() |
sec:authentication을 통해 표시 가능sec:authentication 속성을 활용하여 사용자 이름, 권한 등 인증 정보를 출력할 수 있습니다.<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Authenticated User Info</title>
</head>
<body>
<h1>Welcome, <span sec:authentication="name"></span>!</h1>
<p>Your roles: <span sec:authentication="principal.authorities"></span></p>
<p>Your username (from Principal): <span sec:authentication="principal.username"></span></p>
<!-- 로그아웃 버튼 -->
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</body>
</html>
sec:authentication 속성 설명| 속성 | 설명 |
|---|---|
sec:authentication="name" | 인증된 사용자의 이름을 출력 (기본적으로 UserDetails.getUsername() 값). |
sec:authentication="principal.username" | 인증된 사용자의 이름을 출력 (Principal 객체에서 직접 가져옴). |
sec:authentication="principal.authorities" | 인증된 사용자의 역할/권한 목록 출력. |
sec:authentication="credentials" | 인증 요청 시 사용된 자격 증명 (비밀번호와 같은 정보). 보안상 화면에 표시하지 않는 것이 좋음. |
ROLE_ADMIN이라면:Welcome, admin!
Your roles: [ROLE_ADMIN]
로그인 상태에 따라 메뉴를 표시하거나 숨기기:
<ul>
<li><a th:href="@{/home}">Home</a></li>
<li sec:authorize="hasRole('ADMIN')"><a th:href="@{/admin}">Admin Page</a></li>
<li sec:authorize="isAuthenticated()"><a th:href="@{/profile}">Profile</a></li>
<li sec:authorize="isAnonymous()"><a th:href="@{/login}">Login</a></li>
</ul>
헤더나 푸터에 로그인된 사용자의 이름 표시:
<div>
Logged in as: <span sec:authentication="name"></span>
</div>
스프링 시큐리티에서 로그아웃 기능을 Thymeleaf로 간단히 구현:
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
스프링 시큐리티에서 메서드 인가는 서비스 계층 또는 비즈니스 로직 수준에서 메서드 호출 권한을 검사하는 기능입니다. 이는 컨트롤러나 서비스 클래스의 메서드에 특정 역할(Role) 또는 권한(Authority)을 가진 사용자만 접근할 수 있도록 제한합니다.
스프링 시큐리티에서 메서드 인가를 사용하려면 @EnableGlobalMethodSecurity를 활성화해야 합니다.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Security Configuration
}
사용자 요청:
메서드 호출 전 인가 검사:
@PreAuthorize 또는 @Secured 등의 애노테이션에 따라 스프링 시큐리티가 인증 및 권한을 검사.인가 조건 만족 여부 확인:
AccessDeniedException 발생.반환 시 검사 (옵션):
@PostAuthorize가 설정된 경우 반환 값을 기반으로 추가 권한 검사를 수행.@Service
public class ProductService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteProduct(Long productId) {
// 관리자만 상품 삭제 가능
productRepository.deleteById(productId);
}
@PreAuthorize("#username == authentication.name")
public void updateUserProfile(String username, UserProfile userProfile) {
// 현재 로그인한 사용자만 자신의 프로필 수정 가능
userRepository.updateProfile(username, userProfile);
}
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public List<Product> getAllProducts() {
// USER와 ADMIN 역할을 가진 사용자만 접근 가능
return productRepository.findAll();
}
}