스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크이다.
Spring Security를 이용하면 CSRF 공격, Session 고정 공격을 방어해주고, 요청 헤더도 보안 처리를 해준다.
// bulid.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'
}
UsersEntity 클래스에 UserDetails 인터페이스를 구현(implements)하여 Spring Security의 인증 객체로 사용해야한다.@ToString
@Entity
@Table(name = "users") // 테이블 이름 지정
@Data // Lombok: Getter, Setter, toString, equals, hashCode 생성
public class UsersEntity implements UserDetails {
// 기본키
// 시퀀스=> 자동 증가값
/* 기본 값 지정 */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT 설정
private Long id;
/* 사용자 이름 */
@Column(nullable = false, length = 50) // NOT NULL, 길이 제한, 고유 값 설정
private String username;
/* 사용자 이메일 */
@Column(nullable = false, length = 100) // NOT NULL, 길이 제한
private String email;
/* 사용자 id */
@Column(nullable = false, length = 50)
private String userid;
/* 사용자 비밀번호 */
@Column(nullable = false, length = 255) // NOT NULL
private String password;
/* 사용자 권한 */
@Column(nullable = false, length = 10) // NOT NULL, 길이 제한
private String role;
/* 활성화 상태 */
@Column(nullable = false) // NOT NULL
private boolean enabled;
/* 생성일 */
@CreationTimestamp
@Column(nullable = false, updatable = false) // NOT NULL, 수정 불가
private LocalDateTime createdAt;
/* Entity 객체를 DTO 객체로 변환하여 반환하는 메소드 */
/* Select 명령 사용시 호출 */
public UsersDTO toUserDTO() {
UsersDTO usersDTO = new UsersDTO();
usersDTO.setId(id);
usersDTO.setUsername(username);
usersDTO.setEmail(email);
usersDTO.setUserid(userid);
usersDTO.setPassword(password);
usersDTO.setRole(role);
usersDTO.setEnabled(enabled);
usersDTO.setCreatedAt(createdAt);
return usersDTO;
}
/* 권한 반환 */
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
/* 최소한의 권한 정보가 필요 */
/* ROLE_USER, ROLE_ADMIN */
return List.of(new SimpleGrantedAuthority(role));
}
/* 계정 만료 여부 반환 */
@Override
public boolean isAccountNonExpired() {
/* 만료되었는지 확인하는 로직 */
return true; // true -> 만료되지 않았음
}
/* 계정 잠금 여부 반환 */
@Override
public boolean isAccountNonLocked() {
/* 계정 잠금되었는지 확이니하는 로직 */
return true; // -> true -> 잠금되지 않았음
}
/* 패스워드의 만료 여부 반환 */
@Override
public boolean isCredentialsNonExpired() {
/* 패스워드가 만료되었는지 확인하는 로직 */
return true; // -> 만료되지 않았음
}
}
@Data
@Slf4j
@ToString
public class UsersDTO {
private Long id;
private String username;
private String email;
private String userid;
private String password;
private String role;
private Boolean enabled;
private LocalDateTime createdAt;
/* DTO 객체를 Entity 객체로 변환하여 반환하는 메소드 */
/* Insert 명령 또는 Update 명령 사용시 호출 */
public UsersEntity toUsersEntity() {
UsersEntity usersEntity = new UsersEntity();
usersEntity.setId(id);
usersEntity.setUsername(username);
usersEntity.setEmail(email);
usersEntity.setUserid(userid);
usersEntity.setPassword(password);
usersEntity.setRole(role);
usersEntity.setEnabled(enabled);
usersEntity.setCreatedAt(createdAt);
return usersEntity;
}
}
public interface UsersRepository extends JpaRepository<UsersEntity, Long> {
Optional<UsersEntity> findByUserid(String userid);
}
UserDetailService 클래스에 UserDetailsService 인터페이스를 구현(implements)하여 Spring Security에서 사용자 정보를 가져온다.@Slf4j
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
/* Repository에서 사용자 정보를 가져와야됨 */
private final UsersRepository usersRepository;
/* 사용자 이름(userid)로 사용자의 정보를 가져오는 메서드 */
@Override
public UserDetails loadUserByUsername(String userid) throws UsernameNotFoundException {
return usersRepository.findByUserid(userid)
.orElseThrow(() -> new UsernameNotFoundException("유저 정보를 찾지 못했습니다.: " + userid));
}
}
config 패키지를 만들어서 해당 파일 생성.loginProcessingUrl("경로"): 이 메서드 때문에 3시간을 고생했다.
=> 커스텀 로그인 페이지를 설정해주고 해당 로그인 경로를 form에서 post 방식으로 요청하면 시큐리티가 낚아채서 대신 로그인을 진행한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
/* 패스워드 인코더로 사용할 빈 등록 */
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/* 특정 HTTP 요청에 대한 웹 기반 보안 구성 */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthenticationFailureHandler customAuthenticationFailureHandler) throws Exception {
return http
// authorizeHttpRequests: HTTP 요청에 대한 보안 규칙을 정의한다.
.authorizeHttpRequests((auth) -> auth
// requestMatchers().permitAll(): 해당 경로들에 대해 모든 사용자의 접근을 허용한다.(인증없이)
.requestMatchers("/", "/login", "/signup").permitAll()
// requestMatchers().hasRole(): 해당 역할을 가진 사람만 접근할 수 잇다.
.requestMatchers("/admin").hasRole("ADMIN")
// requestMatchers().hasAnyRole(): 해당 경로에는 "USER" 또는 "ADMIN"역할을 가진 사용자가 접근할 수 있다.
.requestMatchers("/user/**").permitAll() //hasAnyRole("USER", "ADMIN")
// anyRequest().authenticated(): 이외의 모든 접근에 대해서는 인증을 진행한다.
//.anyRequest().authenticated()
).formLogin(form -> form
.loginPage("/login") // 커스텀 로그인 페이지
.loginProcessingUrl("/login") // login 주소가 호출되면 시큐리티가 낚에채서 대신 로그인 진행
.defaultSuccessUrl("/") // 로그인 성공시 이동 페이지
.usernameParameter("userid") // 아이디 파라미터 이름 변경
.failureHandler(customAuthenticationFailureHandler) // ✅ 실패 핸들러 등록
.permitAll())
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true) // 로그아웃 성공시 세션 삭제 여부
.logoutSuccessUrl("/")).csrf(AbstractHttpConfigurer::disable) // 개발할 때는 비활성화 -> 배포시 활성화
.build();
// 기본 HTTP 인증을 사용
//http.httpBasic(Customizer.withDefaults());
// 세션 관리 설정
//http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// h2 DB를 사용하는 경우 프레임이 안보이는 문제를 해결하기 위한 코드
//http.headers().frameOptions().sameOrigin();
}
/* 인증 관리자 관련 설정 */
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
/*
DB 기반 인증
UserDetailService와 PasswordEncoder를 연결하여 인증처리
*/
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailService);
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
/* 여러 인증 제공자(Authentication Provider)를 관리하는 객체 */
return new ProviderManager(authProvider);
}
}
✅ 역할
Spring 설정 파일임을 명시
빈(Bean)을 등록하기 위해 사용한다.
내부에 정의된 메서드(@Bean)들이 스프링 컨테이너에 자동으로 등록된다.
🔎 쉽게 이해하기
Java 기반 설정 파일이라는 뜻!
XML 설정(applicationContext.xml)을 대체하는 자바 기반 설정이다.
✅ 역할
Spring Security를 활성화한다.
기존의 Spring Boot 기본 보안 설정을 커스텀할 수 있도록 해준다.
Spring Security의 설정을 적용하기 위해 필요한 필수 어노테이션!!
🔎 쉽게 이해하기
"Spring Security 기능을 켜줘!"라는 의미
SecurityFilterChain을 사용해 직접 보안 설정을 구성할 수 있도록 해준다.
✅ 내부 동작
Spring Security 필터를 등록.
보안을 위한 인터셉터 및 인증/인가 설정을 적용할 수 있게 한다.
📍 중요
@EnableWebSecurity가 있어야 SecurityFilterChain 설정이 적용된다.
만약 없다면, Spring Security의 기본 설정이 적용되고, 커스텀 보안 설정이 무시된다.
✅ AuthenticationManager의 역할
Spring Security의 인증(Authentication)을 총괄하는 인터페이스이다.
로그인 요청이 들어오면 아이디와 비밀번호가 맞는지 확인한다.
다양한 인증 방식(DB, 소셜 로그인 등)을 처리할 수 있도록 여러 인증 제공자(AuthenticationProvider)를 관리한다.
🔎 흐름
로그인 시, 사용자가 입력한 아이디와 비밀번호를 받아서
AuthenticationManager가 인증 요청을 처리힌디.
이 처리를 AuthenticationProvider에게 위임한다.
✅ DaoAuthenticationProvider의 역할
DB 기반 인증을 담당하는 기본 제공 인증 제공자이다.
사용자가 입력한 아이디와 비밀번호를 DB에 저장된 값과 비교해서 인증한다.
✅ 내부 처리 흐름
UserDetailsService를 통해 DB에서 사용자 정보를 조회
PasswordEncoder(예: BCrypt)로 입력한 비밀번호와 DB의 비밀번호를 비교한다.
둘 다 일치하면 인증 성공, 아니면 인증 실패 처리한다.
🔎 정리
아이디 조회 → 비밀번호 비교 → 인증 여부 결정
이 과정을 자동으로 처리해준다.
✅ ProviderManager의 역할
여러 인증 제공자(AuthenticationProvider)를 관리하는 객체이다.
AuthenticationManager의 기본 구현체.
여러 인증 방식(DB, 소셜 로그인, JWT 등)을 사용할 때,
각각의 인증 제공자를 순서대로 검사한다.
🔎 흐름
로그인 요청 → ProviderManager에게 전달
등록된 AuthenticationProvider(여러 개 가능)가 순차적으로 인증 시도
인증에 성공하면 인증 완료, 실패하면 다음 제공자가 인증 시도
🔥 한 문장으로 요약
AuthenticationManager는 로그인 요청이 오면 DaoAuthenticationProvider를 통해
DB에서 사용자 정보를 조회하고, 비밀번호를 검증해서 인증을 처리한다.
이 전체 과정을 ProviderManager가 통합적으로 관리!!
public interface UsersService {
/* 유저 추가 */
void addUser(UsersDTO usersDTO);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class UsersServiceImpl implements UsersService{
private final UsersRepository usersRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
/* 유저 추가 */
@Override
public void addUser(UsersDTO usersDTO) {
UsersEntity usersEntity = usersDTO.toUsersEntity();
/* BCryptPasswordEncoder로 비밀번호를 암호화하여 저장 */
usersEntity.setPassword(bCryptPasswordEncoder.encode(usersEntity.getPassword()));
UsersEntity savedUsers = usersRepository.save(usersEntity);
log.info(savedUsers.toString());
}
}
/login 로그인 쪽의 @RequestParam은 없어도 된다.@Slf4j
@Controller
@RequiredArgsConstructor
public class SecurityController {
private final UsersService usersService;
@GetMapping("/")
public String index(Model model) {
/*Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UsersEntity user = (UsersEntity) auth.getPrincipal();
model.addAttribute("user", user);
log.info("로그인 사용자 정보: "+user.toString());*/
return "index";
}
/* 로그인 */
@GetMapping("/login")
public String login(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "message", required = false)String message, Model model) {
model.addAttribute("error", error);
model.addAttribute("message", message);
return "pages/security/login";
}
/* 회원가입 */
@GetMapping("/signup")
public String signup(){
return "pages/security/signup";
}
@PostMapping("/signup")
public String sign(@ModelAttribute UsersDTO user) {
user.setRole("ROLE_USER");
user.setEnabled(true);
log.info(user.toString());
usersService.addUser(user);
return "redirect:/user/login";
}
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/user/login";
}
}
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{common/layouts/defaultLayout}"
>
<body>
<!-- ✅ 로그인한 사용자일 때 -->
<div sec:authorize="isAuthenticated()">
<p>🔒 로그인한 사용자 있음</p>
<p>사용자 아이디: <span sec:authentication="principal.username"></span></p>
<p>사용자 이메일: <span sec:authentication="principal.email"></span></p>
<a href="/logout">로그아웃</a>
</div>
<!-- ✅ 로그인하지 않은 사용자일 때 -->
<div sec:authorize="isAnonymous()">
<p>📝 메인페이지 (로그인하지 않은 사용자)</p>
<a href="/login">로그인</a>
<a href="/signup">회원가입</a>
</div>
<!-- ✅ ROLE_USER 권한일 때 -->
<div sec:authorize="hasRole('ROLE_USER')">
<h2>사용자 페이지</h2>
<p>일반 사용자 전용 메뉴입니다.</p>
</div>
<!-- ✅ ROLE_ADMIN 또는 ROLE_MANAGER 권한이 있는 경우 -->
<div sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_MANAGER')">
<h2>관리자 및 매니저 페이지</h2>
</div>
</body>
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
String errorMessage;
if (exception instanceof DisabledException) {
errorMessage = "계정이 비활성화되었습니다. 관리자에게 문의하세요.";
} else if (exception instanceof BadCredentialsException) {
errorMessage = "아이디 또는 비밀번호가 잘못되었습니다.";
} else {
errorMessage = "로그인에 실패했습니다. 다시 시도해주세요.";
}
// ✅ Redirect로 에러 메시지 전달
setDefaultFailureUrl("/login?error=true&message=" + URLEncoder.encode(errorMessage, "UTF-8"));
super.onAuthenticationFailure(request, response, exception);
}
}