1. 개요
인증과 인가
인증(authentication)
- 보호된 리소스에 접근하는 것을 허용하기 이전에, 등록된 사용자의 신원을 입증하는 과정
- ex) 로그인
인가(authorization)
- 특정 부분에 접근할 수 있는 권한이 있는지 확인하는 과정
- ex) 관리자 페이지
접근 주체(principal)
- 인증된 사용자의 신원을 나타내는 객체
- 사용자 로그인 후, 스프링 시큐리티는 인증된 사용자의 정보를 세션에 저장하고 이를 통해 애플리케이션 내에서 인증된 사용자의 신원을 확인할 수 있다.
스프링 시큐리티
https://docs.spring.io/spring-security/reference/index.html
- 스프링 시큐리티(Spring Security)는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크
- 애노테이션을 통한 쉬운 설정
- CSRF 공격* 및 세션 고정(session fixation) 공격* 방어, 요청 헤더 보안 처리 등 보안 관련 옵션 다수 제공
- 개발자가 보안 관련 개발해야 하는 부담을 크게 줄여줌!
- 스프링 시큐리티는 필터 기반으로 동작
- 기본적으로 세션 & 쿠키 방식으로 인증을 처리
* CSRF 공격
사용자의 권한을 가지고 특정 동작을 수행하도록 유도하는 공격
* 세션 고정 공격
사용자의 인증 정보를 탈취하거나 변조하는 공격

- 위 그림의 필터체인(FilterChain)은 서블릿 컨테이너에서 관리하는 ApplicationFilterChain을 의미
- 클라이언트에서 애플리케이션으로 요청을 보내면, 서블릿 컨테이너는 URI를 확인해서 필터와 서블릿을 매핑
- 스프링 시큐리티는 사용하고자 하는 필터체인을 서블릿 컨테이너의 필터 사이에서 동작시키기 위해 다음 그림과 같이 DelegatingFilterProxy를 사용

- DelegatingFilterProxy는 서블릿 컨테이너의 생명주기와 스프링 애플리케이션 컨텍스트(Application Context) 사이에서 다리 역할을 수행하는 필터 구현체
- 표준 서블릿 필터를 구현하고 있으며, 역할을 위임할 필터체인 프록시(FilterChain Proxy)를 내부에 가지고 있다.
- 필터체인 프록시는 스프링 부트의 자동 설정에 의해 자동 생성된다.
- 필터체인 프록시는 스프링 시큐리티에서 제공하는 필터로서 SecurityFilterChain을 통해 많은 Security Filter를 사용할 수 있다.
- 필터체인 프록시에서 사용할 수 있는 SecurityFilterChain은 List 형식으로 담을 수 있게 설정되어 있어 URI 패턴에 따라 특정 SecurityFilterChain을 사용하게 된다.
- SecurityFilterChain에서 사용하는 필터는 여러 종류가 있으며, 각 필터마다 실행되는 순서가 다르다.
- https://docs.spring.io/spring-security/reference/servlet/architecture.html

- 스프링 시큐리티는 다양한 필터로 나누어져 있으며, 각 필터에서 인증, 인가와 관련된 작업을 처리
- SecurityContextPersistenceFilter부터 시작해서 아래로 내려가며 FilterSecurityInterceptor까지 순서대로 필터를 거침
- 필터를 실행할 때는, 붉은 화살표로 연결된 오른쪽 박스의 클래스를 거치며 실행
- 특정 필터를 제거하거나, 필터 뒤에 커스텀 필터를 넣는 등에 설정도 가능
- UsernamePasswordAuthenticationFilter
- 아이디와 패스워드가 넘어오면 인증 요청을 위임하는 인증 관리자 역할
- FilterSecurityInterceptor
- 권한 부여 처리를 위임해 접근 제어 결정을 쉽게 하는 접근 결정 관리자 역할
| 핉터명 | 설명 |
| SecurityContextPersistenceFilter | SecurityContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 담고 있는 객체)를 가져오거나 저장하는 역할을 함 |
| LogoutFilter | 설정된 로그아웃 URL로 오는 요청을 확인해 해당 사용자를 로그아웃 처리함 |
| UsernamePasswordAuthenticationFilter | - 인증 관리자로, 폼 기반 로그인을 할 때 사용되는 필터 - 아이디, 패스워드 데이터를 파싱해 인증 요청을 위임 - 인증이 성공하면 AuthenticationSuccessHandler를 실행 - 인증이 실패하면 AuthenticationFailureHandler를 실행 |
| DefaultLoginPageGeneratingFilter | 사용자가 로그인 페이지를 따로 지정하지 않았을 때, 기본으로 설정하는 로그인 페이지 관련 필터 |
| BasicAuthenticationFilter | - 요청 헤더에 있는 아이디와 패스워드를 파싱해서 인증 요청을 위임 - 인증이 성공하면 AuthenticationSuccessHandler를 실행 - 인증이 실패하면 AuthenticationFailureHandler를 실행 |
| RequestCacheAwareFilter | - 로그인 성공 후, 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청을 처리 - 예를 들어, 로그인하지 않은 상태로 방문했던 페이지를 기억해두었다가 로그인 이후에 그 페이지로 이동 시켜줌 |
| SecurityContextHolderAwareRequestFilter | - HttpServletRequest 정보를 감쌈 - 필터 체인 상의 다음 필터들에게 부가 정보를 제공하기 위해 사용 |
| AnonymousAuthenticationFilter | 필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 객체인 AnonymousAuthentication을 만들어 SecurityContext에 넣어줌 |
| SessionManagementFilter | - 인증된 사용자와 관련된 세션 관련 작업을 진행 - 세션 변조 방지 전략을 설정 - 유효하지 않은 세션에 대한 처리 - 세션 생성 전략을 세우는 등의 작업을 처리 |
| ExceptionTranslationFilter | 요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달 |
| FilterSecurityInterceptor | - 접근 결정 관리자 - AccessDecisionManager로 권한 부여 처리를 위임함으로써, 접근 제어 결정을 쉽게 해줌. 이 과정에서는 이미 사용자가 인증되어 있으므로 유효한 사용자인지도 알 수 있음. 즉, 인가 관련 설정이 가능 |
가장 많이 사용하는 아이디와 패스워드 기반 폼 로그인을 시도하면, 스프링 스큐리티에서는 어떤 절차로 인증 처리를 하는지 알아보자!

➊ 사용자가 폼에 아이디와 패스워드를 입력하면, HTTPServletRequest에 아이디와 비밀번호 정보가 전달됨. 이때 AuthenticationFilter가 넘어온 아이디와 비밀번호의 유효성 검사를 진행.
➋ 유효성 검사가 끝나면, UsernamePasswordAuthenticationToken를 생성
➌ AuthenticationManager는 인증용 객체인 UsernamePasswordAuthenticationToken을 전달받음.
➍ AuthenticationManager의 구현체인 AuthenticationProvider에서 실제 인증 처리 진행
➎ PasswrodEncoder를 통한 패스워드 암호화
➏ ➐ UserDetailsService에게 사용자 ID를 넘겨주고, DB에서 사용자 정보(ID, 암호화된 PW, 권한 등)를 가져옴. UserDetailsService*는 사용자 정보를 UserDetails* 객체로 만들어 AuthenticationProvider에게 전달.
➑ AuthenticationProvider는 UserDetails 객체를 전달받아, 실제 사용자의 입력정보와 UserDetails에 담긴 정보를 비교해 실제 인증 처리를 진행.
➒ ➓ ⓫ 인증이 완료되면 Authentication 객체를 SecurityContextHolder에 저장. 인증 성공 여부에 따라 성공하면 AuthenticationSuccessHandler, 실패하면 AuthenticationFailureHandler 핸들러를 실행
참고) 인증 성공 시 사용자에게 세션 ID(JSESSIONID)와 함께 응답하고, 이후 클라이언트에서 세션 ID와 함께 요청 시 이를 검증 후 인증 처리
* UserDetailsService
- 스프링 시큐리티에서 사용자의 정보를 가져오는 데 사용
- 이 클래스를 상속받은 뒤 loadUserByUsername()을 오버라이드하면 시프링 시큐리티에서 사용자의 정보를 가져올 때 오버라이드 된 메서드를 사용함
* UserDetails
- 스프링 시큐리티에서 사용자의 인증, 인가 정보를 UserDetails 객체에 담음
- 이 클래스를 상속받은 뒤 메서드를 오버라이드해 사용하면 됨
* principal은 주로 다음과 같은 방법으로 접근할 수 있다. (위의 그림을 떠올리며 코드를 살펴보자!)
1. SecurityContextHolder
- SecurityContextHolder는 현재 인증된 사용자의 SecurityContext를 제공하고, 여기에서 Authentication 객체를 통해 Principal 정보를 가져올 수 있다. 보통 Principal은 사용자 이름, ID, 또는 사용자 객체 등을 포함할 수 있다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = (String) authentication.getPrincipal();
2. Controller에서 직접 접근
- 스프링 MVC 컨트롤러에서 Pricipal 객체를 메서드 매개변수로 직접 받아올 수 있다.
@GetMapping("/user")
public String user(Model model, Principal principal) {
model.addAttribute("username", principal.getName());
return "user";
}
3. @AuthenticationPrincipal 애노테이션
- 스프링 시큐리티는 컨트롤러 메서드에서 인증된 사용자의 정보를 직접 주입받기 위해 @AuthenticationPrincipal 애노테이션을 제공한다.
@GetMapping("/user")
public String user(@AuthenticationPrincipal UserDetails userDetails) {
String username = userDetails.getUsername();
return "user";
}
이러한 방식으로 Principal 객체를 얻어 인증된 사용자의 정보를 사용할 수 있다.
2. 회원 도메인 만들기
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security' // 스프링 시큐리티를 사용하기 위한 스타터 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' // 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성 추가
testImplementation 'org.springframework.security:spring-security-test' // 스프링 시큐리티를 테스트하기 위한 의존성 추가
엔티티 만들기
- User 클래스가 상속한 UserDetails 클래스는 스프링 시큐리티에서 사용자의 인증 정보를 담아두는 인터페이스
- 스프링 시큐리티에서 해당 객체를 통해 인증 정보를 가져오려면, 필수 오버라이드 메서드들을 여러 개 사용해야 한다.
| 메서드 | 반환 타입 | 설명 |
| getAuthorities() | Collection<? extends GrantedAuthority> | 사용자가 가지고 있는 권한의 목록을 반환 |
| getUsername() | String | 사용자를 식별할 수 있는 사용자 이름을 반환 이때 사용되는 사용자 이름은 반드시 고유해야함 |
| getPassword() | String | 사용자 비밀번호를 반환 비밀번호는 암호화한 상태로 저장 필요 |
| isAccountNonExpired() | boolean | 계정이 만료되었는 지 확인 true: 만료되지 않았음, false: 만료됨 |
| isAccountNonLocked() | boolean | 계정이 잠금되었는지 확인 true: 잠금되지 않았음, false: 잠금됨 |
| isCredentialsNonExpired() | boolean | 비밀번호가 만료되었는지 확인 true: 만료되지 않았음, false: 만료됨 |
| isEnabled() | boolean | 계정이 사용 가능한지 확인 true: 사용 가능, false: 사용 불가능 |
User
package me.progfrog.flog.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User extends TimeBaseEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column
private String password;
public User(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("user"));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserRepository
package me.progfrog.flog.repository;
import me.progfrog.flog.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
UserDetailService
- 스프링 시큐리티에서 사용자의 정보를 가져오는 UserDetailsService 인터페이스를 구현
- 필수로 구현해야 하는 loadUserByUsername() 메서드를 오버라이딩해서 사용자 정보를 가져오는 로직을 작성
- 이 예제에서는 이메일로 사용자를 식별할 수 있으므로, username은 이메일로 지정
package me.progfrog.flog.service;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.domain.User;
import me.progfrog.flog.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public User loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException(email));
}
}
3. WebSecurityConfig
- 실제 인증 처리를 하는 시큐리티 설정 파일을 작성
package me.progfrog.flog.config;
import me.progfrog.flog.service.UserDetailService;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class WebSecurityConfig {
/**
* 스프링 시큐리티 기능 비활성화
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/static/**");
}
/**
* 특정 HTTP 요청에 대한 웹 기반 보안 구성
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry
.requestMatchers("/login", "/signup", "/user").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/articles", true))
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessUrl("/login"));
return http.build();
}
/**
* 인증 관리자 관련 설정
*/
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity http,
BCryptPasswordEncoder bCryptPasswordEncoder,
UserDetailService userDetailService) throws Exception {
AuthenticationManagerBuilder sharedObject = http.getSharedObject(AuthenticationManagerBuilder.class);
sharedObject.userDetailsService(userDetailService)
.passwordEncoder(bCryptPasswordEncoder);
AuthenticationManager authenticationManager = sharedObject.build();
http.authenticationManager(authenticationManager);
return authenticationManager;
}
/**
* 패스워드 인코더로 사용할 빈 등록
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
webSecurityCustomizer()
/**
* 스프링 시큐리티 기능 비활성화
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/static/**");
}
- 스프링 시큐리티의 모든 기능을 사용하지 않게 설정하는 코드
- 즉, 인증/인가 서비스를 모든 곳에 적용하지 않는다. 일반적으로 정적 리소스(이미지, HTML 파일)를 설정
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers("/static/**");
}
- 만약, H2 데이터베이스 콘솔을 사용한다면 H2 데이터베이스 콘솔 URL 경로를 매칭해서 H2 콘솔에 접근할 때 스프링 시큐리티의 인증 절차를 거치지 않도록 설정해야 한다. (MySQL은 내장형 콘솔이 없기 때문에 이와 같은 설정이 필요하지 않다.)
filterChain()
/**
* 특정 HTTP 요청에 대한 웹 기반 보안 구성
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry
.requestMatchers("/login", "/signup", "/user").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/articles", true))
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessUrl("/login"));
return http.build();
}
- 특정 HTTP 요청에 대해 웹 기반 보안을 구성
- 이 메서드에서 인증/인가 및 로그인, 로그아웃 관련 설정을 할 수 있다.
- .authorizeHttpRequests
- requestMatchers(): 특정 요청에 일치하는 url에 대한 액세스를 설정
- permitAll(): 누구나 접근이 가능하게 설정, "/login", "/signup", "/user"로 요청이 오면 인증/인가 없이도 접근 가능
- anyRequest(): 위에서 설정한 url 이외의 요청에 대해서 설정
- authenticated(): 별도의 인가는 필요하지 않지만, 인증이 성공된 상태여야 접근할 수 있음
- .formLogin
- loginPage(): 로그인 페이지 경로를 설정
- defaultSuccessUrl(): 로그인이 완료되었을 때 이동할 경로를 설정
- .logout
- logoutUrl(): 로그아웃 경로를 설정
- logoutSuccessUrl(): 로그인이 완료되었을 때 이동할 경로를 설정
- invalidateHttpSession(): 로그아웃 이후에 세션을 전체 삭제할지 여부를 설정
- .csrf(AbstractHttpConfigurer::disable)
- CSRF 설정을 비활성화
- CSRF 공격을 방지하기 위해서는 활성화하는 것이 좋으나, 편리한 실습을 위해 비활성화
- 이 부분은 최근 스프링 부트 버전에서 달라진 부분이 있어서, 아래 문서를 많이 참고했다!
authenticationManager()
/**
* 인증 관리자 관련 설정
*/
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity http,
BCryptPasswordEncoder bCryptPasswordEncoder,
UserDetailService userDetailService) throws Exception {
AuthenticationManagerBuilder sharedObject = http.getSharedObject(AuthenticationManagerBuilder.class);
sharedObject.userDetailsService(userDetailService)
.passwordEncoder(bCryptPasswordEncoder);
AuthenticationManager authenticationManager = sharedObject.build();
http.authenticationManager(authenticationManager);
return authenticationManager;
}
- 인증 관리자 관련 설정
- 사용자 정보를 가져올 서비스를 재정의하거나, 인증 방법(LDAP, JDBC 기반 인증)등을 설정할 때 사용
- 여기서는 사용자 서비스를 설정
- userDetailsService(): 사용자 정보를 가져올 서비스를 설정, 이때 설정하는 서비스 클래스는 반드시 UserDetailsService()를 상속받은 클래스여야 한다.
- passwordEncoder(): 비밀번호를 암호화하기 위한 인코더를 설정
bCryptPasswordEncoder()
/**
* 패스워드 인코더로 사용할 빈 등록
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
- 패스워드 인코더를 빈으로 등록
4. 회원 가입 구현하기
DTO 추가
package me.progfrog.flog.dto.user;
public record UserReqAddDto(
String email,
String password
) {
}
UserService
- 패스워드를 저장할 때, 시큐리티를 설정하며 패스워드 인코딩용으로 등록한 빈을 사용해서 암호화한 후에 저장
package me.progfrog.flog.service;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.domain.User;
import me.progfrog.flog.dto.user.UserReqAddDto;
import me.progfrog.flog.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public void save(UserReqAddDto reqAddDto) {
userRepository.save(new User(
reqAddDto.email(),
bCryptPasswordEncoder.encode(reqAddDto.password()))
);
}
}
UserApiController
- 회원 가입 메서드를 호출하고, 회원 가입이 완료된 이후에는 로그인 페이지로 리다이렉트
- /logout GET 요청을 하면 로그아웃을 담당하는 핸들러인 SecurityContextLogoutHandler의 logout() 메서드를 호출해서 로그아웃 진행한다. 로그아웃이 완료된 이후에는 로그인 페이지로 리다이렉트
package me.progfrog.flog.api;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.dto.user.UserReqAddDto;
import me.progfrog.flog.service.UserService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
public class UserApiController {
private final UserService userService;
@PostMapping("/user")
public String signup(UserReqAddDto reqAddDto) {
userService.save(reqAddDto);
return "redirect:/login";
}
@PostMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/login";
}
}
뷰 작성하기
resources/templates/users
- resources/templates 하위에 users 파일 생성
- user과 관련된 뷰를 구현
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
<style>
.gradient-custom {
background: linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1))
}
</style>
</head>
<body class="gradient-custom">
<section class="d-flex vh-100">
<div class="container-fluid row justify-content-center align-content-center">
<div class="card bg-dark" style="border-radius: 1rem;">
<div class="card-body p-5 text-center">
<h2 class="text-white">LOGIN</h2>
<div class = "mb-2">
<form action="/login" method="POST">
<!-- 토큰을 추가하여 CSRF 공격 방지 -->
<input type="hidden" th:name="${_csrf?.parameterName}" th:value="${_csrf?.token}" />
<div class="mb-3">
<label class="form-label text-white">이메일</label>
<input type="email" class="form-control" name="username">
</div>
<div class="mb-3">
<label class="form-label text-white">비밀번호</label>
<input type="password" class="form-control" name="password">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
<button type="button" class="btn btn-secondary mt-3" onclick="location.href='/signup'">회원가입</button>
</div>
</div>
</div>
</div>
</section>
</body>
</html>
signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 가입</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
<style>
.gradient-custom {
background: linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1))
}
</style>
</head>
<body class="gradient-custom">
<section class="d-flex vh-100">
<div class="container-fluid row justify-content-center align-content-center">
<div class="card bg-dark" style="border-radius: 1rem;">
<div class="card-body p-5 text-center">
<h2 class="text-white">SIGN UP</h2>
<div class = "mb-2">
<form th:action="@{/user}" method="POST">
<!-- 토큰을 추가하여 CSRF 공격 방지 -->
<input type="hidden" th:name="${_csrf?.parameterName}" th:value="${_csrf?.token}" />
<div class="mb-3">
<label class="form-label text-white">이메일</label>
<input type="email" class="form-control" name="email">
</div>
<div class="mb-3">
<label class="form-label text-white">비밀번호</label>
<input type="password" class="form-control" name="password">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</div>
</div>
</div>
</section>
</body>
</html>
bodyHeader.html
- 로그아웃 버튼을 추가해 준다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="header" th:fragment="bodyHeader">
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">개굴개굴 🐸</h1>
<h4 class="mb-3">프로그의 블로그에 오신 것을 환영합니다.</h4>
<button type="button" class="btn btn-danger" onclick="location.href='/logout'">로그아웃</button>
</div>
</div>
UserViewController
- 로그인, 회원 가입 경로로 접근하면 뷰 파일을 연결하는 컨트롤러를 생성
package me.progfrog.flog.web;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class UserViewController {
@GetMapping("/login")
public String getLoginForm() {
return "users/login";
}
@GetMapping("/signup")
public String getSignupForm() {
return "users/signup";
}
}
5. 마무리
flog-mysql 컨테이너가 잘 실행되고 있는지 확인하고...

http://localhost:8080/articles 접속 시 /login 페이지로 리다이렉트 되는 것을 확인한다.
회원 가입 진행 후, 로그인 및 로그아웃 테스트!


users 테이블에 사용자 정보가 저장된 부분도 확인할 수 있었다.

툴은 이걸 사용하였다.
[DB] 맥북용 Database Tool
회사에서 업무용으로 사용할 때는 MySQL Workbench와 HeidiSQL을 사용한다. 각각 장단점이 있어서 같이 사용하기에 좋은!!! 근데!!! 집에서 개인 프로젝트를 할 때는 맥북을 사용하는지라 HeidiSQL은 맥용
progfrog.tistory.com
정리하면,
- 인증은 보호된 리소스에 접근하는 것을 허용하기 이전에 등록된 사용자의 신원을 입증하는 과정
- 인가는 특정 부분에 접근할 수 있는지 확인하는 과정
- 스프링 시큐리티는 스프링 기반의 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크로 필터 기반으로 동작
- 각 필터에서 인증, 인가와 관련된 작업을 처리
- 기본적으로 세션 & 쿠키 방식으로 인증을 처리
- 시프링 시큐리티에서 사용자의 인증, 인가 정보를 UserDetails 객체에 담는다. 이 클래스를 상속받은 뒤 메서드를 오버라이드해 사용하면 된다.
- 스프링 시큐리티에서 사용자의 정보를 가져오는 데 사용하는 UserDetailsService를 사용한다. 이 클래스를 상속받은 뒤 loadUserByUsername()을 오버라이드하면 스프링 시큐리티에서 사용자의 정보를 가져올 때, 오버라이드된 메서드를 사용한다.
다음 시간에는 JWT에 대해 알아보고, 이를 활용해서 OAuth2를 곁들여 회원가입 및 로그인/로그아웃을 구현해 보도록 하겠다!