1. 스프링 시큐리티란?
- 스프링의 보안 관련 기능 (인증과 권한) 을 담당하는 스프링 하위 프레임워크
- 필터 기반으로 동작
- 기본적으로 세션 기반 인증 제공
2. 스프링 시큐리티 인증 처리 과정
1. 사용자 인증 요청
보통 로그인 폼을 통해 자격 증명 정보를 제출
2. UsernamePasswordAuthenticationFilter 동작
제출 정보를 토대로 AuthenticationManager 인증 수행
3. AuthenticationProvider
커스텀 AuthenticationProvider 를 통해 인증 로직을 구현하고 인증 결과 (성공 or 실패) 반환
4. SecurityContextHolder 에 인증정보 저장
인증 성공시 SecurityContextHolder 에 SecurityContext 을 생성하고 Authentication 객체를 저장
성공적으로 인증된 사용자는 리소스에 접근할 수 있음 (필요에 따라 추가 권한 검사 수행)
3. 필터 호출 과정
스프링 시큐리티는 다양한 필터로 구성되며 각 필터에선 인증/인가 관련 작업을 처리한다.
주요 필터명과 역할은 다음과 같다.
필터명 | 역할 |
SecurityContextPersistenceFilter | SecurityContextRepository 에서 SecurityContext (접근 주체와 인증 정보) 를 가져옴 |
LogoutFilter | 사용자 로그아웃 처리 및 세션 무효화. 로그아웃 URL 설정 |
UsernamePasswordAuthenticationFilter | 폼 기반 로그인 처리 및 사용자 인증 인증 성공시 AuthenticationSuccessHandler, 인증 실패 시 AuthenticationFailureHandler 실행 |
DefaultLoginPageGeneratingFilter | 로그인 페이지가 지정되어 있지 않았을 때 default 로그인 폼 제공 |
RequestCacheAwareFilter | 로그인 후 요청 캐시 처리 |
SecurityContextHolderAwareRequestFilter | 보완 관련 정보 제공 HttpServletRequest 를 변형해 SecurityContextHolder 와 연계 |
AnonymousAuthenticationFilter | 로그인을 하지 않았을 때, AnonoymousAuthentication 객체를 생성해 익명 사용자에 대한 인증 제공 |
SessionManagementFilter | 세션 관리 및 관련 작업 수행 |
ExceptionTranslationFilter | 요청 처리 관련 예외 전달 |
FilterSecurityInterceptor | HTTP 리소스에 대한 접근 제어를 위한 보안 처리 및 접근 권한 확인 |
4. 스프링 시큐리티 설정
1) build.gradle: 스프링 시큐리티 설치
// build.gradle
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
...
}
2) SecurityConfig.java: 스프링 시큐리티 필터 설정
// SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 모든 인증되지 않은 요청을 허가
// authorizeHttpRequests: 스프링 시큐리티에서 HTTP 규칙을 정의 메서드. 특정 URL 패턴의 접근 제어에 사용
// AntPathRequestMatcher("/**")).permitAll(): /로 시작하는 URL (모든 URL) 에 대한 접근 허가
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
// 2. 스프링 시큐리티가 CSRF 처리시 H2 콘솔은 예외로 처리
// 스프링 시큐리티 환경에선 H2 콘솔 로그인시 CSRF 로 인한 403 예외 발생
// ignoringRequestMatchers: 특정 요청을 무시하도록 지시하는 메서드
// new AntPathRequestMatcher("/h2-console/**") : h2 콘솔 접속시 필터체인 거치지 않도록
.csrf((csrf) -> csrf
.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
// 3. URL 요청시 X-Frame-Options 헤더값을 sameorigin으로 설정
// 스프링 시큐리티는 사이트 콘텐츠가 다른 사이트에 포함되지 않기 위해 X-Frame-Options 사용
// frame에 포함된 페이지가 제공하는 사이트와 동일한 경우 사용 가능하도록 설정
.headers((headers) -> headers
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
// 4. formLogin 메서드: 로그인 설정 담당.
// loginPage(String loginFormUrl): 로그인 폼 URL
// defaultSuccessUrl(loginSuccessUrl): 로그인 성공 후 이동할 URL
// 로그인 URL: "/user/login", 로그인 성공시: "/" 로 이동
.formLogin((formLogin) -> formLogin
.loginPage("/user/login")
.defaultSuccessUrl("/"))
// 5. logout 메서드: 로그아웃 설정 담당
// logoutSuccessUrl(String logoutSuccessUrl): 로그아웃 성공 후 이동할 URL
// invalidateHttpSession(boolean invalidateHttpSession): HTTP 세션 무효화 결정
// deleteCookies(String cookieNames): 삭제할 쿠키 이름 지정
.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true))
;
return http.build();
}
// 회원가입 메서드에서 사용할 암호화 방식: PasswordEncoder 객체 빈 주입
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// AuthenticationManager 객체 빈 주입
// AuthenticationManager 는 스프링 시큐리티의 인증을 담당
// 사용자 인증시 UserSecurityService와 PasswordEncoder 사용
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
어노테이션
- @Configuration: 스프링 환경 설정 파일임을 선언
- @EnableWebSecurity: 요청 URL이 스프링 시큐리티 제어를 받도록 설정. SpringSecurityFilterChain 기반 URL 필터 적용.
- @Bean: 빈을 통한 세부 설정
5. 회원가입과 로그인 구현
1. SiteUser: 회원 정보 엔티티
//SiteUser.java
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "siteUser")
public class SiteUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@Column(unique = true)
private String email;
}
2. UserRepository: SiteUser 엔티티의 CRUD 작업을 지원하는 JpaRepository 확장 인터페이스
// UserRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<SiteUser, Long> {
Optional<SiteUser> findByUsername(String username);
}
3. UserService: 회원가입 메서드 구현
// UserService.java
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
/**
* User 데이터 생성
*/
public SiteUser create(String username, String email, String password){
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
// 빈으로 등록 된 PasswordEncoder 로 비밀번호 암호화
user.setPassword(passwordEncoder.encode(password));
this.userRepository.save(user);
return user;
}
}
4. UserCreateForm: 유효성 검사를 담당하는 회원가입 폼
// UserCreateForm.java
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserCreateForm {
@Size(min = 3, max = 25)
@NotEmpty(message = "사용자ID는 필수항목입니다.")
private String username;
@NotEmpty(message = "비밀번호는 필수항목입니다.")
private String password1;
@NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
private String password2;
@NotEmpty(message = "이메일은 필수항목입니다.")
@Email // 해당 속성값이 이메일 형식인지 검증
private String email;
}
5. UserController: 회원가입/로그인 관련 비즈니스 로직과 URL 연결
// UserController.java
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/user")
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 회원가입 폼
*/
@GetMapping("/signup")
public String signUp(UserCreateForm userCreateForm){return "signup_form";}
/**
* 회원가입 완료
*/
@PostMapping("/signup")
public String signUp(@Valid UserCreateForm userCreateForm, BindingResult bindingResult){
// 1. 에러가 있을 경우 다시 폼 작성
if (bindingResult.hasErrors()) {
return "signup_form";
}
//2. 두 개의 비밀번호가 동일한지 검증
if (!(userCreateForm.getPassword1().equals(userCreateForm.getPassword2()))){
// bindingResult.rejectValue(필드명, 오류코드, 에러메시지)
bindingResult.rejectValue("password2", "passwordInCorrect","2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
try {
this.userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(), userCreateForm.getPassword1());
}catch (DataIntegrityViolationException e){
//3. 사용자 ID 또는 이메일 주소가 동일할 경우 -> DataIntegrityViolationException 예외발생
e.printStackTrace();
bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
return "signup_form";
}catch (Exception e){
// 4. 기타 예외 발생 경우
e.printStackTrace();
bindingResult.reject("signupFailed", e.getMessage());
return "signup_form";
}
// 5. 회원가입 성공시, 리다이렉트
return "redirect:/";
}
/**
* 로그인 폼
*/
@GetMapping("/login")
public String login(){
return "login_form";
}
}
6. UserRole: 인증 후 사용자에게 부여할 권한 정의
// UserRole.java
import lombok.Getter;
@Getter
public enum UserRole {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER");
UserRole(String value) {
this.value = value;
}
private String value;
}
7. UserSecurityService: 스프링 시큐리티 로그인 핵심 부분
// UserSecurityService.java
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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 java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
private final UserRepository userRepository;
/**
* loadUserByUsername: 사용자명으로 비밀번호 조회 후 리턴
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 유저네임 조회를 통한 사용자 객체 선언
Optional<SiteUser> _siteUser = this.userRepository.findByUsername(username);
// 2. 비어있다면 UsernameNotFoundException 리턴
if(_siteUser.isEmpty()){
throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");
}
SiteUser siteUser = _siteUser.get();
List<GrantedAuthority> authorities = new ArrayList<>();
// 3. 사용자명이 admin 이면 ADMIN 권한 부여, 이외에는 USER 권한 부여
// SimpleGrantedAuthority: 인가정보를 나타내는 클래스. 사용자 권한을 정의하고 관리
if ("admin".equals(username)){
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
} else{
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
}
System.out.println("authorities = " + authorities);
System.out.println("siteUser = " + siteUser);
return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
}
}
- 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스 구현
- loadUserByUsername (사용자명으로 비밀번호 조회 후 리턴) 메서드 구현 강제
- 스프링시큐리티는 loadUserByUsername 의해 리턴된 User 객체의 비밀번호가 입력 받은 비밀번호와 일치하는지 검사하는 로직을 내부적으로 가짐
6. 타임리프 기반의 View
1. navbar.html: 로그인/로그아웃 버튼이 있는 헤더
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<html xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<nav th:fragment="navbar" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">Board</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
<a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/user/signup}">회원가입</a>
</li>
</ul>
</div>
</div>
</nav>
- sec:authorize 을 통해 인증이 되지 않은 사용자일 경우 로그인 버튼을 표시
- 인증된 사용자의 경우 로그아웃 버튼 표시
2. login_form.html: 로그인 폼
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="header :: header">
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<body>
<nav th:replace="~{navbar :: navbar}"></nav>
<div th:fragment="content" class="container my-3">
<form th:action="@{/user/login}" method="post">
<div th:if="${param.error}">
<div class="alert alert-danger">
사용자ID 또는 비밀번호를 확인해 주세요.
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">사용자ID</label>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
</body>
</html>
3. signup_form.html: 회원가입 폼
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="header :: header">
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<body>
<nav th:replace="~{navbar :: navbar}"></nav>
<div th:fragment="content" class="container my-3">
<div class="my-3 border-bottom">
<div>
<h4>회원가입</h4>
</div>
</div>
<form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post">
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<div class="mb-3">
<label th:for="username" class="form-label">사용자ID</label>
<input type="text" th:field="*{username}" class="form-control">
</div>
<div class="mb-3">
<label th:for="password1" class="form-label">비밀번호</label>
<input type="password" th:field="*{password1}" class="form-control">
</div>
<div class="mb-3">
<label th:for="password2" class="form-label">비밀번호 확인</label>
<input type="password" th:field="*{password2}" class="form-control">
</div>
<div class="mb-3">
<label th:for="email" class="form-label">이메일</label>
<input type="email" th:field="*{email}" class="form-control">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</body>
</html>
7. 화면 예시
구현한 코드를 통해 세션 기반의 회원가입, 로그인, 로그아웃 기능을 사용할 수 있다.인증된 사용자라면 로그인 버튼을 인증되지 않은 사용자라면 로그아웃 버튼을 확인할 수 있다.
8. 참조 링크
3-05 스프링 시큐리티 - 점프 투 스프링부트 (wikidocs.net)
3-05 스프링 시큐리티
* `[완성 소스]` : [https://github.com/pahkey/sbb3/tree/3-05](https://github.com/pahkey/sbb3/tree/3-05) …
wikidocs.net
https://devhan.tistory.com/310#ViewController%20%EC%83%9D%EC%84%B1-1
스프링부트 3.X 스프링 시큐리티 사용해서 회원가입, 로그인, 로그아웃 구현하기
스프링 시큐리티? 스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크이다. 인증(Authentication)? 인증은 사용자의 신원을 입증하는 과정이
devhan.tistory.com
'백앤드 개발 > Java & Spring' 카테고리의 다른 글
[Spring boot] 자주 쓰는 어노테이션 (1) | 2024.02.01 |
---|---|
[Spring boot] JPA 칼럼명과 언더바 (0) | 2023.12.12 |
[Spring boot] 타임리프 (0) | 2023.11.22 |
[Spring boot] 스프링 MVC 구조 이해 (0) | 2023.11.14 |
[Spring boot] 서블릿, JSP을 활용한 MVC 패턴 (0) | 2023.11.14 |