본문 바로가기
개발자 로그

🔐 Spring Boot Security+ MyBatis +Json Request 로그인 구현하기

by Hello_World_! 2025. 8. 1.
반응형

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 형식일때의 로그인에 대해서 좀더 자세히 적어보겠습니다.