Spring Security를 처음 학습하며 프로젝트를 실제 구현해봤습니다.
이번 글에서는 Spring Boot에서 기본으로 적용되는 보안 설정이 어떤 식으로 동작하는지 살펴보고, 사용자 인증과 권한 처리 흐름을 어떻게 구성할 수 있는지 간단한 실습 예제를 통해 정리해보았습니다. 실제 프로젝트에 Spring Security를 도입하거나 커스터마이징할 때 참고용으로 활용하시면 좋을 것 같습니다.
회사에서 Mybatis를 사용하고, json으로 API를 자주 받기 때문에 Mybatis와 Json을 활용한 로그인 프로젝트를 만들었습니다.
🗂 프로젝트 설정(실행 환경은 자바 8입니다)
프로젝트명 | security |
Java 버전 | 17 |
Spring Boot | 3.5.4 |
MyBatis | 2.2.2 |
DB | MariaDB (mariadb-java-client: 3.1.2) |
인증 방식 | JSON 기반 로그인 /loginAuth |
패키지 루트 | org.example.security |
📁 프로젝트 구조 및 DB
src/main/java/org.example.security
├── config/
│ └── SecurityConfig.java
├── controller/
│ └── TestController.java
├── dto/
│ ├── LoginDto.java
│ └── UserDto.java
├── mapper/
│ └── UserMapper.java
├── service/
│ ├── CustomDetailService.java
│ ├── JsonUsernamePasswordAuthenticationFilter.java
│ ├── LoginFailureHandler.java
│ └── LoginSuccessHandler.java
└── SecurityApplication.java
src/main/resources
├── mapper/UserMapper.xml
└── application.properties
⚙️ 설정 파일
application.properties
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/login
spring.datasource.username=root
spring.datasource.password=1234
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=org.example.security.dto
🔐 SecurityConfig
1. customFilter.setFilterProcessesUrl("/loginAuth") 로그인 요청 경로를 설정합니다.
2. requestMatchers("/loginAuth").permitAll() -> 이 경로로는 모든 요청을 허용함
3. .requestMatchers("/admin/**").hasRole("ADMIN") // 관리자만 허용
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 사용자/관리자 허용
-> 요청 URL 에 따라서 ROLE 에 따라 허락한다.
4. addFilterAt(customFilter, UsernamePasswordAuthenticationFilter.class); // custom 한 필터 위치 지정.
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
JsonUsernamePasswordAuthenticationFilter customFilter = new JsonUsernamePasswordAuthenticationFilter();
customFilter.setAuthenticationManager(authenticationManager);
customFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
customFilter.setAuthenticationFailureHandler(new LoginFailureHandler());
customFilter.setFilterProcessesUrl("/loginAuth");
http.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/loginAuth").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.addFilterAt(customFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 테스트용
}
}
📥 LoginDto & UserDto
1. LoginDto는 json Request 받을때 편하기 위해서 만들었습니다.
2. UserDto는 Mybatis에서 DB값 가져오기 위해 만들었습니다.
@Data
public class LoginDto {
private String email;
private String password;
}
@Data
public class UserDto {
private Long id;
private String email;
private String password;
private String name;
private String createdAt;
private String role;
}
🗂 Mapper 설정
UserMapper.java
@Mapper
public interface UserMapper {
UserDto findByEmail(String email);
}
UserMapper.xml
<mapper namespace="org.example.security.mapper.UserMapper">
<select id="findByEmail" resultType="org.example.security.dto.UserDto">
SELECT id, email, password, name, created_at, role
FROM user
WHERE email = #{email}
</select>
</mapper>
🔄 로그인 필터 구현
1. password, email 값을 Dto로 받아와서 return this.getAuthenticationManager().authenticate(authRequest);로 뿌려준다.
2. DaoAuthenticationProvider는 UserDetailsService와 PasswordEncoder를 자동으로 사용합니다. 인증 과정 중 내부적으로 additionalAuthenticationChecks()가 호출되어
→ passwordEncoder.matches(요청 비밀번호, DB 비밀번호) 비교 수행 (현재는 평문으로 비교)
정리
AuthenticationManager.authenticate()
→ DaoAuthenticationProvider.authenticate()
→ UserDetailsService.loadUserByUsername() ← ⭐ 먼저 실행됨!
→ additionalAuthenticationChecks() (비밀번호 비교)
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
try{
ObjectMapper objectMapper = new ObjectMapper();
LoginDto loginRequest = objectMapper.readValue(request.getInputStream(), LoginDto.class);
String email = loginRequest.getEmail();
String password = loginRequest.getPassword();
if (email == null || password == null) {
throw new AuthenticationServiceException("Email or Password is missing");
}
System.out.println("email = " + email);
System.out.println("password = " + password);
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(email, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}catch (Exception e){
e.printStackTrace();
throw new AuthenticationServiceException("Failed to parse authentication request body", e);
}
}
}
✅ customDetailService
-> CustomDetailService는 Spring Security 내부에서 자동으로 적용되어 사용됩니다.
@Service
@RequiredArgsConstructor
public class CustomDetailService implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserDto user = userMapper.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(email);
}
System.out.println("ROLE: " + user.getRole());
return new User(
user.getEmail(), // 로그인 시 입력한 아이디
user.getPassword(), // DB에 저장된 암호화된 비밀번호
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())) // 기본 권한
);
}
}
✅ 트러블슈팅 요약
문제 해결방법 | |
Mapper 인식 오류 factoryBeanObjectType 오류 |
bad class file: (~~~~~~~~~) [ERROR] class file has wrong version 61.0, should be 52.0 [ERROR] Please remove or make sure it appears in the correct subdirectory of the classpath. [ERROR] [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException |
userMapper Bean 등록 오류 | @MapperScan("org.example.security.mapper") 추가 또는 @Mapper 어노테이션 사용 확인 |
한글 응답 "???" | response.setCharacterEncoding("UTF-8") 명시 |
AuthenticationManager null | SecurityConfig에서 Bean 주입 방식으로 setAuthenticationManager() 처리 |
JSON 파싱 실패 | ObjectMapper.readValue(...) 적용 및 DTO 정의 |
403 Forbidden 발생 | /loginAuth 경로 permitAll()로 허용하지 않음 |
"Failed to parse request" | request.getParameter 사용 대신 ObjectMapper 사용 필요 |
🔑 로그인 테스트
📮 요청 예시
- URL: POST <http://localhost:8080/loginAuth>
- Headers:
- Content-Type: application/json
- Body:
- { "email": "test@test.com", "password": "1234" }
다음엔 있었던 트러블 슈팅과, form 형식일때의 로그인에 대해서 좀더 자세히 적어보겠습니다.
'개발자 로그' 카테고리의 다른 글
이 작업을 수행하기 위해 이 파일과 연결된 앱이 없습니다. 앱을 설치하거나 이미 설치된 경우 기본 앱 설정 페이지 에서 연결을 만드세요. (1) | 2025.08.07 |
---|---|
Spring Security, JWT 사용방법 (2) | 2025.08.06 |
🔐 Spring Security - 커스텀 인증 로직 정의 및 구성 클래스 분리 (3) | 2025.07.29 |
🔐 Spring Security - 엔드포인트 수준에서 인가 적용하기 (1) | 2025.07.29 |
🌐 Spring Security 클래스 설계의 큰 그림 (0) | 2025.07.29 |