반응형

오늘은 Spring Security에서 제공하는 기본 JdbcUserDetailsManager 대신, 사용자 정의 UserDetailsService를 구현하여 유연한 인증 처리 방법을 학습합니다.


1. Custom UserDetailsService 개요

Spring Security는 사용자 정보를 UserDetails 인터페이스로 관리하며, 이를 제공하기 위해 UserDetailsService 인터페이스를 사용합니다. 기본 구현체를 사용하는 대신, 커스텀 구현을 통해 아래와 같은 작업을 수행할 수 있습니다:

  • 데이터베이스 구조를 커스터마이징.
  • 추가 필드 처리(예: 사용자 프로필 정보).
  • 외부 API 연동.

2. 데이터베이스 구조 수정

(1) 사용자 테이블

users 테이블에 추가 필드를 포함합니다.

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN NOT NULL,
    email VARCHAR(100) NOT NULL
);

(2) 권한 테이블

authorities 테이블은 그대로 유지합니다.

CREATE TABLE authorities (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    authority VARCHAR(50) NOT NULL,
    FOREIGN KEY (username) REFERENCES users(username)
);

3. 예제 데이터 삽입

테스트 데이터를 삽입합니다.

INSERT INTO users (username, password, enabled, email) VALUES
('admin', '$2a$10$e1NrKZy/l4Jhtd89KVGjOuwBr.jE24OvTHjxMvHk8XK.tFbVOO.aS', true, 'admin@example.com'),
('user', '$2a$10$e1NrKZy/l4Jhtd89KVGjOuwBr.jE24OvTHjxMvHk8XK.tFbVOO.aS', true, 'user@example.com');

INSERT INTO authorities (username, authority) VALUES
('admin', 'ROLE_ADMIN'),
('user', 'ROLE_USER');

4. UserDetailsService 커스터마이징

(1) User 엔티티 생성

Spring Security에서 사용할 사용자 정보를 담는 엔티티를 정의합니다.

package com.example.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class CustomUser implements UserDetails {

    private String username;
    private String password;
    private boolean enabled;
    private String email;
    private List<GrantedAuthority> authorities;

    public CustomUser(String username, String password, boolean enabled, String email, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.email = email;
        this.authorities = authorities;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

(2) CustomUserDetailsService 구현

데이터베이스에서 사용자 정보를 로드하는 UserDetailsService를 구현합니다.

package com.example.security;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final DataSource dataSource;

    public CustomUserDetailsService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try (Connection connection = dataSource.getConnection()) {
            // 사용자 정보 조회
            PreparedStatement userStatement = connection.prepareStatement(
                "SELECT username, password, enabled, email FROM users WHERE username = ?");
            userStatement.setString(1, username);

            ResultSet userResult = userStatement.executeQuery();

            if (!userResult.next()) {
                throw new UsernameNotFoundException("User not found: " + username);
            }

            String password = userResult.getString("password");
            boolean enabled = userResult.getBoolean("enabled");
            String email = userResult.getString("email");

            // 권한 정보 조회
            PreparedStatement authStatement = connection.prepareStatement(
                "SELECT authority FROM authorities WHERE username = ?");
            authStatement.setString(1, username);

            ResultSet authResult = authStatement.executeQuery();
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();

            while (authResult.next()) {
                authorities.add(new SimpleGrantedAuthority(authResult.getString("authority")));
            }

            return new CustomUser(username, password, enabled, email, authorities);

        } catch (Exception e) {
            throw new UsernameNotFoundException("Database error occurred", e);
        }
    }
}

5. SecurityConfig 설정

Spring Security 설정에서 커스텀 UserDetailsService를 등록합니다.

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder)
            throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                   .userDetailsService(userDetailsService)
                   .passwordEncoder(passwordEncoder)
                   .and()
                   .build();
    }

    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests()
                .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .httpBasic();
    }
}

6. 테스트

  1. 애플리케이션을 실행합니다.
  2. /login 페이지에서 사용자 계정으로 로그인합니다.
    • 사용자명: admin, 비밀번호: admin123
    • 사용자명: user, 비밀번호: user123
  3. 정상적으로 로그인되고 권한에 따라 페이지 접근이 가능하면 성공입니다.
반응형

+ Recent posts