Spring Security는 Java 애플리케이션을 위한 보안 프레임워크로, 인증(authentication)과 인가(authorization)를 유연하고 확장 가능하게 지원합니다. Spring Boot에서는 spring-boot-starter-security
스타터를 통해 관련 의존성을 한 번에 관리하고 자동 구성을 제공합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
spring-boot-starter-security
는 Spring Security 관련 의존성을 묶어 제공하며, 기본적인 보안 필터 체인을 자동 구성합니다./css/**
, /js/**
, /images/**
등 정적 리소스 경로는 인증 없이 접근할 수 있도록 기본 제외됩니다.application.properties
에서 기본 사용자 계정과 역할을 변경할 수 있습니다:
spring.security.user.name=alice
spring.security.user.password=alicepw
spring.security.user.roles=USER,ADMIN
템플릿을 렌더링하기 위한 컨트롤러 없이도 뷰 연결을 할 수 있습니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
@Bean
public SpringSecurityDialect securityDialect() {
return new SpringSecurityDialect();
}
}
필요에 따라 명시적 컨트롤러를 정의할 수도 있습니다:
@Controller
public class HomeController {
@GetMapping("/")
public ModelAndView home() {
return new ModelAndView("home");
}
}
thymeleaf-extras-springsecurity6
의존성을 추가하고, 뷰에서 sec:authorize
등의 속성을 사용할 수 있습니다.
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/extras/spring-security">
<head>
<title>Spring Security 튜토리얼</title>
</head>
<body>
<h2>Welcome</h2>
<div sec:authorize="hasRole('USER')">User에게만 보이는 텍스트</div>
<div sec:authorize="hasRole('ADMIN')">Admin에게만 보이는 텍스트</div>
<div sec:authorize="isAuthenticated()">인증된 사용자만 보이는 텍스트</div>
<p>인증된 사용자: <span sec:authentication="name"></span></p>
</body>
</html>
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@ManyToMany
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "USER_ID"),
inverseJoinColumns = @JoinColumn(name = "ROLE_ID")
)
private List<Role> roles;
}
@Entity
@Table(name = "roles")
public class Role {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, unique = true)
private String rolename;
@ManyToMany(mappedBy = "roles")
private List<User> users;
}
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmail(String email);
}
public interface RoleRepository extends JpaRepository<Role, Integer> {
Optional<Role> findByRolename(String rolename);
}
UserDetailsService
를 구현하여 DB에서 사용자 정보를 로드합니다.
@Service
@Transactional
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(
"Email not found: " + username));
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), getAuthorities(user));
}
}
@Configuration
public class WebSecurityConfig {
@Autowired
private UserDetailsService customUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/", "/home", "/signup").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.failureUrl("/login?error")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.userDetailsService(customUserDetailsService)
.csrf(csrf -> csrf.disable());
return http.build();
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
BCrypt는 자동으로 랜덤 솔트를 생성하여 같은 평문이라도 매번 다른 해시를 생성합니다.
th:action
과 POST/PUT/DELETE 메서드를 사용하면 CSRF 토큰이 자동 삽입됩니다.http.csrf(csrf -> csrf.disable());
<form th:action="@{/signup}" th:object="${user}" method="post">
<div th:if="${emailExists}">
<span style="color:red">Email already exists</span>
</div>
<label>Email:</label>
<input type="text" th:field="*{email}" />
<label>Password:</label>
<input type="password" th:field="*{password}" />
<button type="submit">SignUp</button>
</form>