하위 태스크 1
Spring Security 의존성 추가
build.gradle에 Security 의존성 확인
build.gradle에서 Spring Security 의존성이 추가된 것을 확인한다.
dependencies {
// ...
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'
}하위 태스크 2 ~ 3
SecurityConfig 클래스 생성
@EnableWebSecurity와 SecurityFilterChain 설정
기본 보안 설정 구성
인증이 필요한 경로와 허용 경로 설정
config/SecurityConfig.java에 SecurityConfig 클래스를 작성한다. /와 /public/** 패턴과 일치하는 위치에 대한 접근은 허용하고, 그 외의 위치는 모두 인증이 선행되어야 한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return http.build();
}
}필터 체인에 의해 /something과 같이 허가되지 않은 URL에 접근하면 로그인 페이지 /login으로 이동한다.

하위 태스크 4
User 엔티티 생성
사용자 정보를 저장할 엔티티 생성
사용자에 대응하는 User 엔티티 클래스를 생성한다.
model/User.java:
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String email;
}하위 태스크 5
UserRepository 생성
사용자 조회를 위한 Repository 생성
UserRepository 클래스를 생성한다.
repository/UserRepository.java:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}하위 태스크 6
CustomUserDetailsService 구현
UserDetailsService 인터페이스 구현
UserDetailsService 인터페이스를 구현한 CustomUserDetailsService 클래스를 생성한다.
service/CustomUserDetail.java:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build();
}
}하위 태스크 7
PasswordEncoder Bean 등록
BCryptPasswordEncoder 등록
passwordEncoder 메서드를 Bean으로 등록한다.
public class SecurityConfig {
// ...
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
}Warning
model/User.java의User클래스 구조와 일치하도록resources/schema.sql과resources/data.sql을 수정한다.
resources/schema.sql:-- ... create table users ( id bigint auto_increment primary key, username varchar(50) not null unique, password varchar(500) not null, email varchar(255) ); -- ...
로그인 후 http://localhost:8080/product/list에 접근하면 정상적으로 내용을 확인 할 수 있다.

하위 태스크 8
UserService 생성
회원가입 로직 구현
service/UserService.java에 UserService 클래스를 구현한다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public User registerUser(String username, String password, String email) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEmail(email);
return userRepository.save(user);
}
}하위 태스크 9
회원가입 API 구현
POST /register 엔드포인트 구현
dto/UserRegistrationDto.java에 UserRegistrationDto 클래스를 구현한다.
@Data
public class UserRegistrationDto {
private String username;
private String password;
private String email;
}controller/UserController.java에 UserController 클래스를 구현한다.
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody UserRegistrationDto dto) {
userService.registerUser(dto.getUsername(), dto.getPassword(), dto.getEmail());
return ResponseEntity.ok("User registered successfully");
}
}하위 태스크 10
로그인 페이지 생성
커스텀 로그인 페이지 HTML 생성
resources/templates/login-custom.html 파일을 작성한다.
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<style>
body {
width: 600px;
padding: 20px;
margin-left: auto;
margin-right: auto;
border-color: lightgray;
border-style: solid;
border-width: 1px;
}
input {
border-color: gray;
border-radius: 4px;
border-style: solid;
border-width: 1px;
background-color: aliceblue;
}
</style>
</head>
<body>
<h1>로그인</h1>
<div th:if="${param.error}">아이디와 패스워드가 잘못 되었습니다</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="아이디">
</div>
<div>
<input type="password" name="password" placeholder="패스워드">
</div>
<div>
자동 로그인 <input type="checkbox" name="remember-me" />
</div>
<button type="submit">로그인</button>
</form>
</body>
</html>하위 태스크 11
로그인 성공/실패 핸들러 설정
SecurityConfig에서 핸들러 설정
config/SecurityConfig.java에서 로그인 성공 및 실패 핸들러를 설정한다.
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**", "/login-custom").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
- .loginPage("/login")
+ .loginPage("/login-custom")
+ .successHandler((request, response, authentication) -> {
+ System.out.println("로그인 성공: " + authentication.getName());
+ response.sendRedirect("/");
+ })
+ .failureHandler((request, response, exception) -> {
+ System.out.println("로그인 실패: " + exception.getMessage());
+ response.sendRedirect("/login-custom?error");
+ })
.permitAll()
);
return http.build();
}
// ...
}controller/AuthenticationController.java에 /login-custom 경로로 매핑하기 위한 getLoginCustom 메서드를 추가한다.
public class AuthenticationController {
// ...
+ @GetMapping("/login-custom")
+ public String getLoginCustom() {
+ return "login-custom";
+ }
// ...
}웹브라우저에서 http://localhost:8080/login-custom에 접속한 결과는 다음과 같다.

하위 태스크 12
세션 관리 설정
세션 타임아웃 및 관리 설정
application.properties를 수정해 세션 타임아웃 시간을 설정할 수 있다. 다음은 예시는 세션 타임아웃을 30분으로 설정한다.
# ...
server.servlet.session.timeout=30mconfig/SecurityConfig.java에서 최대 허용 세션 수 등의 세션 관련 설정을 할 수 있다. 다음 예시는 최대 허용 세션을 1개로 제한하며, 중복 로그인 시 기존 세션을 만료시킨다.
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
+ .sessionManagement(session -> session
+ .maximumSessions(1)
+ .maxSessionsPreventsLogin(false)
+ );
return http.build();
}
// ...
}