Spring Security | 인증 | UserDetailsService:사용자 정보 검색

사용자의 정보를 검색하는 역할은 UserDetailsService에서 담당한다. Spring Security에는 UserDetailsService를 구현한 클래스가 일부 포함되어 있다.

메모리 사용

사용자 정보를 메모리에 저장할 구현한다. 구체적인 클래스는 InMemoryUserDetailsManager 이다.

XML Configuration

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       ...>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="hoge" password="HOGE" authorities="ROLE_USER" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
  • <user-service><user> 태그를 사용하여 선언한다.

Java Configuration

MySpringSecurityConfig.java

package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ...
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("hoge").password("HOGE").roles("USER");
    }
}
  • 설정 클래스에 AuthenticationManagerBuilder을 받는 메소드를 선언하고, @Autowired 어노테이션이 부여한다.
  • inMemoryAuthentication() 메소드로 정의 정보를 설정한다.

JDBC

데이터베이스에서 사용자 정보를 얻을 구현한다. 실제 클래스는 JdbcUserDetailsManager이다.

build.gradle (추가 종속성)

    compile 'org.springframework:spring-jdbc:4.3.6.RELEASE'
    compile 'com.h2database:h2:1.4.193'
  • 확인을 위해 DB에는 H2를 추가하여 사용한다.
  • 또한 DataSource를 만들기 위해 spring-jdbc도 종속성에 추가하고 있다.

src/main/resources/sql/create_database.sql

CREATE TABLE USERS (
    USERNAME VARCHAR_IGNORECASE(50) NOT NULL PRIMARY KEY,
    PASSWORD VARCHAR_IGNORECASE(50) NOT NULL,
    ENABLED  BOOLEAN NOT NULL
);

CREATE TABLE AUTHORITIES (
    USERNAME  VARCHAR_IGNORECASE(50) NOT NULL,
    AUTHORITY VARCHAR_IGNORECASE(50) NOT NULL,
    CONSTRAINT FK_AUTHORITIES_USERS FOREIGN KEY (USERNAME) REFERENCES USERS(USERNAME),
    CONSTRAINT UK_AUTHORITIES UNIQUE (USERNAME, AUTHORITY)
);

INSERT INTO USERS VALUES ('fuga', 'FUGA', true);
INSERT INTO AUTHORITIES VALUES ('fuga', 'USER');
  • 기본적으로 작성한 것과 같이 테이블 컬럼을 선언하면 자동으로 사용자 정보가 검색된다.
  • 설정에 따라 변경될 수도 있다.
  • 파일은 클래스 경로 아래에 배치되도록 한다(나중에 데이터 소스를 만들 때 초기화 스크립트로 사용).

XML Configuration

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="
         ...
         http://www.springframework.org/schema/jdbc
         http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

    ...

    <jdbc:embedded-database id="dataSource" type="H2">
        <jdbc:script location="classpath:/sql/create_database.sql" />
    </jdbc:embedded-database>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:jdbc-user-service data-source-ref="dataSource"  />
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
  • <jdbc-user-service> 태그를 사용한다.
  • 데이터 소스의 정의는 <jdbc:script> 태그를 이용하여 위에 쓴 초기화 스크립트를 실행한다.

Java Configuration

MySpringSecurityConfig.java

package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import javax.sql.DataSource;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ...
    }

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(EmbeddedDatabaseType.H2)
                .setScriptEncoding("UTF-8")
                .addScript("/sql/create_database.sql")
                .build();
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource);
    }
}
  • AuthenticationManagerBuilder의 jdbcAuthentication() 메소드를 사용한다.

테이블 이름과 컬럼 이름 변경

CREATE TABLE USERS (
    LOGIN_ID VARCHAR_IGNORECASE(50) NOT NULL PRIMARY KEY,
    PASSWORD VARCHAR_IGNORECASE(50) NOT NULL,
    ENABLED  BOOLEAN NOT NULL
);

CREATE TABLE AUTHORITIES (
    LOGIN_ID  VARCHAR_IGNORECASE(50) NOT NULL,
    ROLE VARCHAR_IGNORECASE(50) NOT NULL,
    CONSTRAINT FK_AUTHORITIES_USERS FOREIGN KEY (LOGIN_ID) REFERENCES USERS(LOGIN_ID),
    CONSTRAINT UK_AUTHORITIES UNIQUE (LOGIN_ID, ROLE)
);
  • USERNAMELOGIN_IDAUTHORITYROLE로 변경해 본다.

  • 테이블 이름과 컬럼 이름을 원하는 것으로 변경하려면 사용자 정보를 검색 할 때 SQL을 변경한다.

  • 검색시에 항목의 순서만 일치한다면, 컬럼 이름은 무엇이든 상관 없다.

XML Configuration

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       ...>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:jdbc-user-service
                data-source-ref="dataSource"
                users-by-username-query="SELECT LOGIN_ID, PASSWORD, ENABLED FROM USERS WHERE LOGIN_ID=?"
                authorities-by-username-query="SELECT LOGIN_ID, ROLE FROM AUTHORITIES WHERE LOGIN_ID=?" />
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
  • users-by-username-query 속성에서 USERNAME, PASSWORD, ENABLED를 검색하는 쿼리를 정의한다.
  • authorities-by-username-query 속성에서 USERNAME, AUTHORITY을 검색하는 쿼리를 정의한다.

Java Configuration

MySpringSecurityConfig.java

package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import javax.sql.DataSource;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ...
    }

    @Bean
    public DataSource dataSource() {
        ...
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
        .usersByUsernameQuery("SELECT LOGIN_ID, PASSWORD, ENABLED FROM USERS WHERE LOGIN_ID=?")
        .authoritiesByUsernameQuery("SELECT LOGIN_ID, ROLE FROM AUTHORITIES WHERE LOGIN_ID=?");
    }
}
  • usersByUsernameQuery(String) 메소드에서 USERNAME, PASSWORD, ENABLED를 검색하는 쿼리를 정의한다.
  • authoritiesByUsernameQuery(String) 메소드에서 USERNAME, AUTHORITY을 검색하는 쿼리를 정의한다.

UserDetailsService 만들기

MyUserDetailsService.java

package sample.spring.security;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if ("hoge".equals(username)) {
            return new User(username, "HOGE", AuthorityUtils.createAuthorityList("USER"));
        }

        throw new UsernameNotFoundException("not found : " + username);
    }
}
  • UserDetailsService를 구현한 클래스를 생성한다.
  • UserDetailsService에는 loadUserByUsername(String)라는 메소드 하나만 존재한다.
    • 인수에서 사용자를 식별하는 문자열(보통은 로그인 화면 등에서 입력 된 사용자 이름)이 들어 오기 때문에 그 식별 문자열에 대응하는 사용자 정보를 반환한다.
    • 해당하는 사용자 정보가 없는 경우는 UsernameNotFoundException을 thows를 한다.
  • 사용자 정보는 UserDetails 인터페이스를 구현한 클래스로 작성한다.
    • 표준으로 User라는 클래스가 준비되어 있다.
    • 어떤한 사정으로 User 클래스을 변경해야 하는 경우는 UserDetails 인터페이스를 구현한 클래스를 만들면 된다.