본문 바로가기

프로그래밍/Spring Security

Spring Security의 DB를 사용하는 인증과 인증에 따른 권한 설정

지난 글에서는 Spring Security의 계정 클래스와 권한 클래스 설계에 대해 알아보았다. 이번 글에서는 이렇게 설계한 클래스를 DB에서 어떻게 조회하고 이를 Spring Security에서 어떤 식으로 설정하는지를 알아보도록 하자.

 

Spring Security에서는 사용자의 이름(계정, 로그인 아이디)과 비밀번호를 입력받아 DB에서 이를 조회하여 그 결과를 리턴해주는 기능과 해당 사용자의 권한을 DB에서 조회하는 기능을 제공하는 클래스가 이미 제공되고 있다. org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 클래스가 그것이다. 이 클래스의 소스는 여러분들이 따로 보시고, 일단 이 클래스가 상속받은 클래스와 구현해야 하는 인터페이스만 알려주도록 하겠다

 

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService

 

JdbcDaoImpl 클래스는 Spring에서 제공하는 JdbcTemplate을 통해 DB를 조회하기 위하여 org.springframework.jdbc.core.support.JdbcDaoSupport 클래스를 상속받고 있으며 사용자 정보를 조회하기 위한 목적으로 org.springframework.security.core.userdetails.UserDetailsService 인터페이스를 사용하고 있다.

UserDetailsService 인터페이스 소스를 보면 다음의 메소드가 있다.

 

 

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

 

사용자의 이름을 파라미터로 넘겨 UserDetails 객체를 return 하도록 되어 있다. UserDetails..어디서 들어본 이름 아닌가? 이전 글에서 사용자 계정 클래스 설계시에 언급했던 그 UserDetails 인터페이스를 말하는 것이다. 이전 글에서 우리가 사용자 계정 클래스인 MemberInfo 클래스를 설계할때 UserDetails 인터페이스를 구연했었다. 즉 우리는 loadUserByUsername 메소드에 MemberInfo 클래스 객체를 return 해주면 된다. 이미 JdbcDaoImpl 클래스가 UserDetailsService 클래스를 구현하고 있기 때문에 우리는 JdbcDaoImpl 클래스를 상속받아 쓰면 되겠다. 상속받아 사용해야 하는 이유는 이전 글에서 우리가 만든 MemberInfo 클래스 객체를 사용해야 하기 때문에 JdbcDaoImpl 클래스에서 MemberInfo 클래스 객체를 사용할려면 JdbcDaoImpl 클래스를 상속받아 새로운 클래스를 만든 뒤 이 클래스에서 MemberInfo 클래스 객체를 사용해 주면 된다.

 

우리가 만들어서 사용하게 될 소스코드는 다음과 같다

 

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;

import com.terry.springsecurity.vo.MemberInfo;

public class CustomJdbcDaoImpl extends JdbcDaoImpl {

	@Override
	public UserDetails loadUserByUsername(String username)	throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		List<UserDetails> users = loadUsersByUsername(username);

        if (users.size() == 0) {
            logger.debug("Query returned no results for user '" + username + "'");
            
            UsernameNotFoundException ue = new UsernameNotFoundException(messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
            throw ue;
        }

        MemberInfo user = (MemberInfo)users.get(0); // contains no GrantedAuthority[]

        Set<GrantedAuthority> dbAuthsSet = new HashSet<GrantedAuthority>();

        if (getEnableAuthorities()) {
            dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
        }

        if (getEnableGroups()) {
            dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
        }

        List<GrantedAuthority> dbAuths = new ArrayList<GrantedAuthority>(dbAuthsSet);
        user.setAuthorities(dbAuths);

        if (dbAuths.size() == 0) {
            logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");

            UsernameNotFoundException ue = new UsernameNotFoundException(messages.getMessage("JdbcDaoImpl.noAuthority", new Object[] {username}, "User {0} has no GrantedAuthority"));
            throw ue;
        }

        return user;
	}

	@Override
	protected List<UserDetails> loadUsersByUsername(String username) {
		// TODO Auto-generated method stub
		return getJdbcTemplate().query(getUsersByUsernameQuery(), new String[] {username}, new RowMapper<UserDetails>() {
            public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
                String username = rs.getString(1);
                String password = rs.getString(2);
                String name = rs.getString(3);
                return new MemberInfo(username, password, name, AuthorityUtils.NO_AUTHORITIES);
            }

        });
	}

	@Override
	protected List<GrantedAuthority> loadUserAuthorities(String username) {
		// TODO Auto-generated method stub
		return getJdbcTemplate().query(getAuthoritiesByUsernameQuery(), new String[] {username}, new RowMapper<GrantedAuthority>() {
            public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
                String roleName = getRolePrefix() + rs.getString(1);

                return new SimpleGrantedAuthority(roleName);
            }
        });
	}

	@Override
	protected List<GrantedAuthority> loadGroupAuthorities(String username) {
		// TODO Auto-generated method stub
		return super.loadGroupAuthorities(username);
	}
}

 

지금 보여주는 이 코드는 JdbcDaoImpl 클래스 소스것을 그대로 사용하되 우리가 작업할 내용에 맞춰서 고친 부분이 있다. 핵심이 되는 메소드는 loadUsersByUsername 와 UserDetailService 인터페이스에 정의된 loadUserByUsername 메소드이다. 이 메소드에서 사용자 계정 정보와 권한 설정을 사용자 계정 클래스에 설정하여 사용자 계정 정보를 리턴해주고 있다.

 

먼저 사용되는 메소드는 loadUsersByUsername(String username)이다.(UserDetailService 인터페이스에 정의된 loadUserByUsername 메소드가 아니다. loadUsersByUsername, s가 붙어있다. 이 메소드는 JdbcDaoImpl 클래스에 있는 메소드이다.) 이 메소드에서 JdbcTemplate을 이용해서 DB를 조회한 뒤 UserDetails 인터페이스 객체들이 들어있는 List 인터페이스 객체를 리턴하고 있다. 이 메소드는 파라미터 username에 사용자가 입력한 로그인 아이디가 들어가서 DB를 조회하게 된다. 여기서 생각해야 할 점이 있다. 우리가 흔히 DB를 조회하는 로그인을 구현할때 로그인 아이디와 비밀번호를 쿼리문의 조건으로 같이 사용해서 조회하고 있지만 Spring Security에서는 쿼리문의 비밀번호를 쿼리문의 조건으로 사용하지 않는다. 파라미터로 들어가는 로그인 아이디만 쿼리문의 조건으로 사용해서 비밀번호를 조회하는 개념이다. 이 부분은 UserDetails 인터페이스를 생각해보면 이해가 된다. 이전 글에서 언급했던 UserDetails 인터페이스를 다시 보도록 하자. UserDetails 인터페이스가 정의한 함수들이 return 하는 것들로 무엇이 있었는가? 계정 이름(아이디), 비밀번호, 계정 만료가 아닌지 여부, 계정 잠김이 아닌지 여부, 계정 패스워드 만료가 아닌지 여부, 사용 가능 계정 인지의 여부, 계정이 갖는 권한 목록이다. UserDetails 인터페이스에서 비밀번호를 리턴하기 때문에 Spring Security에서는 계정에 맞는 비밀번호인지 아닌지 확인하는 작업을 DB 레벨에서 하는게 아니라 DB에서 검색된 비밀번호를 자바 레벨에서 하는 것임을 알수가 있다

 

loadUsersByUsername 메소드가 DB에서 계정 정보를 조회한다고 이전 단락에서 얘길 했는데 이 부분에 대해 설명하도록 하겠다. 이 메소드를 본격적으로 설명하기 전에 CustomJdbcDaoImpl 클래스의 부모 클래스인 JdbcDaoImpl 클래스에서 이 메소드가 어떤 식으로 정의되어 있는지 봐야 할 필요가 있다. JdbcDaoImpl 클래스에서 이 부분을 어떻게 구현했고 우리는 이 부분을 어떻게 고쳐야 우리가 원하는 기능을 수행할 수 있는지 가닥을 잡을 수 있기 때문이다. 다음의 코드는 JdbcDaoImpl 클래스에 정의된 loadUSersByUsername 메소드와 여기서 사용하는 쿼리가 정의된 멤버 변수를 보여주고 있다.

 

public static final String DEF_USERS_BY_USERNAME_QUERY =
	"select username,password,enabled " +
	"from users " +
	"where username = ?";

protected List<UserDetails> loadUsersByUsername(String username) {
	return getJdbcTemplate().query(usersByUsernameQuery, new String[] {username}, new RowMapper<UserDetails>() {
		public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
			String username = rs.getString(1);
			String password = rs.getString(2);
			boolean enabled = rs.getBoolean(3);
			return new User(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
		}
	});
}

JdbcDaoImpl 클래스는 DEF_USERS_BY_USERNAME 변수를 직접 사용하는게 아니라 JdbcDaoImpl 클래스에 정의된 usersByUsernameQuery 멤버 변수를 사용한다. JdbcDaoImpl 클래스는 usersByUsernameQuery 변수에 DEF_USERS_BY_USERNAME_QUERY를 사용하도록 JdbcDaoImpl 클래스 생성자에서 설정한다. 그러나 usersByUSernameQuery 변수는 setter 메소드가 존재하기 때문에 우리가 사용하길 원하는 쿼리로 바꿔서 설정할 수가 있다. 일단 현재는 usersByUsernameQuery 변수에 다른 쿼리를 설정하고 있지 않기 때문에 DEF_USERS_BY_USERNAME_QUERY를 사용할 것이다. 

 

DEF_USERS_BY_USERNAME_QUERY에 사용된 쿼리문을 살펴보자. 계정 이름(로그인 아이디)를 조건으로 주어 계정 이름(username), 비밀번호(password), 계정 사용 가능 여부(enabled)를 조회하고 있다. DB를 통해 이 3가지를 조회한다고 보고 loadUsersByUsername 메소드를 보자. getJdbcTemplate() 함수를 이용하여 JdbcTemplate을 얻은 뒤에 usersByUsernameQuery 변수에 있는 쿼리문을 사용하고 있다. 이때 쿼리문의 조건으로 들어가는 계정 이름(로그인 아이디)를 new String[] {username}을 통해 파라미터로 전달받은 username을 넘겨주고 있다. 그리고 RowMapper를 정의하게 되는데 이 정의되는 내부 코드를 보자. ResultSet으로 넘어오는 쿼리문의 에서 row 하나하나를 매핑하는 코드들이다. rs.getString(1)을 통해 row에서 계정 이름(로그인 아이디)를 얻어 username 변수에 넣고 rs.getString(2)를 통해 계정 비밀번호를 얻어 password 변수에 넣고, rs.getBoolean(3)을 통해 row에서 계정 사용 가능 여부를 얻어 enabled에 넣은 뒤에 이전 글에서 UserDetail 인터페이스 설명 때 언급했던 User 클래스의 생성자에 넣는 식으로 User 객체를 만들고 있다.

 

User 클래스 객체를 만드는 과정을 보자. username, password, enabled 변수는 DB에서 조회한 값을 넣고 있지만 나머지 부분은 ture와 AuthorityUtils.NO_AUTHORITIES를 넣고 있다. User 클래스의 생성자를 Spring Security API 문서를 통해 살펴보면 3개의 true가 들어가는 부분은 계정 만료가 아닌지의 여부, 계정 패스워드 만료가 아닌지의 여부, 계정 잠김이 아닌지의 여부이다. 이 시점에서는 권한을 조회하지는 않기 때문에 AuthorityUtils.NO_AUTHORITIES를 넣어서 권한이 일단은 아무것도 없다고 생각하고 나중에 그것을 채우는 방식으로 진행하게 된다.

 

이 생성자를 보면 UserDetails 인터페이스에 정의된 메소드가 동작하도록 하기 위해 관련 정보를 생성자에서 셋팅 하는 것을 알 수 있다. 여기서 우리는 다음과 같이  생각해 볼 수 있다. UserDetails 인터페이스에서 언급하는 정보들이 아닌 추가적인 정보(예를 들면 사용자의 이름이나 주소)를 넣을려고 하면 loadUsersByUsername 메소드에서 조회하면 되지 않을까? 맞는말이다. 실제로 그런 추가적인 정보를 넣는 작업을 이 loadUSersByUsername 메소드에서 작업할 것이다. 그런 생각을 하고 이제 우리가 만들 CustomJdbcDaoImpl 클래스의 loadUsersByUsername 메소드를 보도록 하자.

 

getJdbcTemplate().query(getUsersByUsernameQuery(), new String[] {username}, new RowMapper<UserDetails>() {
	public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
		String username = rs.getString(1);
		String password = rs.getString(2);
		String name = rs.getString(3);
		return new MemberInfo(username, password, name, AuthorityUtils.NO_AUTHORITIES);
	}
});

 

JdbcDaoImpl 클래스의 loadUsersByUsername 메소드와 어느 부분이 차이가 있는지 알 수 있겠는가? JdbcDaoImpl 클래스의 loadUsersByUsername 메소드에서는 결과셋의 3번째 컬럼이 계정 사용 가능 여부를 조회하기 때문에 rs.getBoolean(3)으로 받아 boolean형 변수에 넣었지만 여기서는 rs.getString(3)으로 String으로 받고 있다. 바로 이 부분이 Spring Security에서는 직접적으로 이용하지는 않지만 우리가 만드는 웹 어플리케이션에서는 이용하게 되는 사용자의 이름(계정 이름, 로그인 아이디가 아닌 사용자 실명을 의미한다) 부분이다. 그리고 이렇게 조회한 값들을 이전 글에서 보여주었던 UserDetails 인터페이스를 구현한 MemberInfo 클래스 객체로 만들어 주고 있다. 여기서는 사용 가능 계정 여부 등은 사용할 일이 없기 때문에 생성자에 이를 넣지 않고 MemberInfo 클래스에서 true로 리턴하도록 했지만 이것도 DB에서 조회해야 한다면 DB에서 조회한 뒤에 생성자나 객체의 setter 함수를 통해 넣어주면 된다. 이런 조회를 해야 하기 때문에 쿼리문을 저장하는 변수인 usersByUsernameQuery에는 쿼리에서 조회되는 결과의 3번째 컬럼에 사용자 이름이 조회되는 그런 쿼리여야 한다. 그리고 그런 쿼리가 usersByUSernameQuery 의 getter 메소드인 getUsersByUsernameQuery 메소드를 통해 쿼리문을 얻어오게 된다. 그래서 이렇게 얻어온 값을 UserDetails 인터페이스를 구현한 클래스 객체에 넣어주면 된다.

 

정리하자면 다음의 글박스와 같다

 

1. JdbcDaoImpl 클래스를 상속받은 클래스를 제작한다

2. 그 클래스에서 업무적으로 필요한 사용자 정보(Spring Security가 직접 이용하지 않는 정보(사용자 실명 이름, 주소등)를 조회하는 쿼리를 loadUsersByUsername 메소드에서 실행하도록 한다.

3. loadUsersByUsername 메소드에서 얻어진 사용자 정보를 UserDetails 인터페이스를 구현한 클래스 객체에 넣어주도록 한다

 

이렇게 실행된 loadUsersByUsername 메소드는 누가 호출하는가? 누군가 호출을 해야 조회를 할 것 아닌가? UserDetailsService 인터페이스에 정의된 loadUserByUsername 메소드에서 실행한다. UserDetailsService 인터페이스는 JdbcDaoImpl 클래스가 구현하고 있고 우리는 JdbcDaoImpl 클래스를 상속받고 있기 때문에 loadUserByUsername 메소드의 override가 가능한 것이다. 다음의 코드는 위에 언급했던 CustomJdbcDaoImpl 클래스에서 loadUserByUsername 메소드 부분만 추출한 것이다.

 

@Override
public UserDetails loadUserByUsername(String username)	throws UsernameNotFoundException {
	// TODO Auto-generated method stub
	List<UserDetails> users = loadUsersByUsername(username);

	if (users.size() == 0) {
		logger.debug("Query returned no results for user '" + username + "'");
		
		UsernameNotFoundException ue = new UsernameNotFoundException(messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
		throw ue;
	}

	MemberInfo user = (MemberInfo)users.get(0); // contains no GrantedAuthority[]

	Set<GrantedAuthority> dbAuthsSet = new HashSet<GrantedAuthority>();

	if (getEnableAuthorities()) {
		dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
	}

	if (getEnableGroups()) {
		dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
	}

	List<GrantedAuthority> dbAuths = new ArrayList<GrantedAuthority>(dbAuthsSet);
	user.setAuthorities(dbAuths);

	if (dbAuths.size() == 0) {
		logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");

		UsernameNotFoundException ue = new UsernameNotFoundException(messages.getMessage("JdbcDaoImpl.noAuthority", new Object[] {username}, "User {0} has no GrantedAuthority"));
		throw ue;
	}

	return user;
}

 

메소드의 맨 첫줄을 보면 loadUsersByUsername 메소드를 실행하고 있다. 검색된 사용자가 없으면(users.size() == 0) UsernameNotFoundException을 던지고 있다. 검색된 사용자가 있으면 loadUsersByUsername 메소드에서 MemberInfo 객체를 return 하고 있기 때문에 loadUSerByUsername 메소드에서는 첫번째 요소를 꺼낸뒤에 MemberInfo 클래스로 캐스팅하고 있다. 사용자 정보를 조회했기 때문에 이제는 이 사용자 정보에 해당 사용자가 가지고 있는 권한을 조회해서 셋팅해야 한다. 그 부분이 dbAuthsSet.addAll(loadUserAuthorities(user.getUsername())); 이다. 이따가 설명하겠지만 loadUserAuthorities 메소드에 사용자 계정 이름(로그인 아이디)을 넘겨서 해당 사용자의 권한을 조회한다. 권한을 조회하면 List 객체로 변환한 뒤에 user.setAuthorities(dbAuths);을 이용해 사용자 정보에 권한을 셋팅하게 된다. 결국 loadUserByUsername 메소드는 사용자의 기본 정보와 권한을 조회한뒤 이를 담은 객체를 return 하고 있다. 그리고 loadUserByUsername 메소드는 UserDetailService 인터페이스에 정의된 메소드이기 때문에 Spring Security가 USerDetailService 인터페이스에 의존해서 실행할 수 있게 되는 것이다.

 

방금 권한 조회를 얘기했는데 권한 조회는 loadUserAuthorities 메소드에서 하고 있다. CustomJdbcDaoImpl 클래스에서 loadUserAuthorities 메소드는 다음과 같다.

 

protected List loadUserAuthorities(String username) {
	// TODO Auto-generated method stub
	return getJdbcTemplate().query(getAuthoritiesByUsernameQuery(), new String[] {username}, new RowMapper() {
		public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
			String roleName = getRolePrefix() + rs.getString(1);

			return new SimpleGrantedAuthority(roleName);
		}
	});
}

 

이 메소드도 코드는 loadUsersByUsername과 비슷하다. 단 권한을 조회하는 것이기 때문에 여기서는 Query를 실행하면 권한 이름이 조회가 되어야 한다. 이렇게 권한 이름을 조회하여 이전 글에서 언급했던 SimpleGrantedAuthority 클래스 객체로 만들어져 들어가 있는 List 객체를 return한다. 여기서 보면 getRolePrefix()란 getter 메소드가 있다. rolePrefix 프로퍼티의 getter 메소드인 getRolePrefix()는 권한 이름 앞에 고정적으로 붙는 문자열을 지정한다. 일반적으로 Spring Security를 default 설정을 할 경우 이 프로퍼티엔 ROLE_ 란 값이 들어간다. 즉 사용자 권한을 Spring Security에서 비교할땐 권한 앞에 ROLE_ 가 붙는다는 것이다. 그러나 개인적으로 이 부분은 권장하지 않고 싶다. 예를 들어서 DB에서 권한 이름을 ADMIN 이라고 했다면 Spring Security에서는 ROLE_ADMIN이란 권한 이름으로 권한 비교를 한다는 의미이다. 즉 저장된 권한명과 실제 Spring Security에서의 사용 권한 명이 다른 부분이 있다. 그래서 이 프로퍼티는 빈 문자열("")로 셋팅해 주어서 일치시켜주는 것이 좋다.

 

여기서 설명하지 않은 부분이 있는데 loadUserByUsername에 있는 다음의 코드와 CustomJdbcDaoImpl 클래스의 다음의 메소드이다.

 

if (getEnableGroups()) {
	dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
}

@Override
protected List loadGroupAuthorities(String username) {
	// TODO Auto-generated method stub
	return super.loadGroupAuthorities(username);
}

 

이 부분은 그룹 권한을 조회하는 개념이다. 그룹 권한이라는 것은 여러개의 권한을 묶어 하나의 그룹으로 묶은 개념이다. 예를 들자면 권한 중에 준회원 게시판 조회, 준회원 게시판 등록, 준회원 게시판 수정, 준회원 게시판 삭제가 있고 이 4개의 권한을 하나로 묶어서 준회원 이란 그룹으로 만들어서 관리한다는 것을 의미한다. 즉 데이터베이스에서 사용자 계정 이름(로그인 아이디)에 그룹명을 매핑시키고 그룹 아이디에 그룹아 가지고 있는 권한들을 조회해서 사용자 권한을 설정한다는 것이다. 이 기능을 쓸려고 한다면 enableGroups 프로퍼티를 true로 셋팅해주고 loadGroupAuthorities 메소드를 구현해주면 된다. 구체적인 예시 코드는 JdbcDaoImpl 클래스를 보면 알 수 있다. 여기서는 그것을 쓸 일이 없어서 나중에 설정에서 enableGroups 프로퍼티를 false로 설정할 것이다. 또한 메소드는 부모클래스의 loadGroupAuthorities 메소드를 호출하도록 했다.

 

이렇게 사용자 정보와 권한을 조회하는 클래스를 만들었다면 이젠 이 클래스를 spring security의 설정 XML에 넣어서 사용해야 할 것이다. 이전에 언급했던 Spring Security의 초간단 셋팅 블로그 글을 보면 <authentication-manager> 태그와 그 하위 태그인 <authentication-provider> 태그를 언급한 적이 있다. 사용자의 계정과 권한을 조회하는 부분을 설정하는 것이 바로 이 태그에서 설정하게 되는 것인데 이 부분을 다음과 같이 바꿔준다.

 

<authentication-manager>
	<authentication-provider user-service-ref="customJdbcDaoImpl" />
</authentication-manager>

<beans:bean id="customJdbcDaoImpl" class="com.terry.springsecurity.common.security.jdbc.CustomJdbcDaoImpl">
	<beans:property name="dataSource" ref="logDataSource_pos" />
	<beans:property name="rolePrefix" value="" />
	<beans:property name="usersByUsernameQuery" value="SELECT ID, PASSWORD, NAME FROM MEMBERINFO WHERE ID=?" />
	<beans:property name="authoritiesByUsernameQuery" value="SELECT ROLE_ID FROM MEMBER_ROLE WHERE ID=?" />
	<beans:property name="enableGroups" value="false" />
</beans:bean>

 

예전에 spring security 초간단 셋팅 블로그 글을 보면 <authentication-provider> 태그에 무엇이 있었는가? 사용자 계정 이름과 패스워드가 있었다. 그 부분을 이제는 DB를 조회하는 개념으로 바뀌었다. 그래서 <authentication-provider> 태그의 user-service-ref 속성에 우리가 위에서 만든 CustomJdbcDaoImpl 클래스를 bean으로 등록한 것을 설정한다. 이제 CustomJdbcDaoImpl 클래스 설정을 보자. dataSource 프로퍼티에는 DataSource를 설정한다. rolePrefix는 위에서 언급했던 권한 이름 앞에 붙는 문자열을 지정한다. 여기서는 빈 문자열("")을 지정함으로써 DB에서 지정하는 권한 이름을 그대로 Spring Security에서 사용하도록 했다. 위에서 언급했던 내용중 getRolePrefix() getter 함수 기억하는가? 그 getter 함수와 엮어지는 멤버 변수가 rolePrefix이다. 이것을 빈 문자열("")로 설정하지 않으면 기본값인 ROLE_를 DB로 조회한 권한 이름 앞에 붙이게 되고 권한에 대한 비교를 할때 DB에 명시한 권한 이름과 Spring Security가 내부적으로 사용하는 권한 이름이 달라지기 때문에(ROLE_ + DB 권한 이름) 이런 불일치를 없애기 위해서 빈 문자열로 설정했다. usersByUsernameQuery는 CustomJdbcDaoImpl 클래스의 위에서 설명했던 loadUsersByUsername 메소드에서 실행하는 query를 설정하는 것이다. 이 쿼리를 보면 ID 컬럼을 조건으로 해서 무언가 값(?)이 들어가는데 이 부분이 사용자 계정 이름(로그인 아이디)가 된다. 즉 사용자 계정 이름을 조건으로 주어 사용자 정보를 조회하는 query를 설정해주면 된다. 여기 query를 보면 NAME 컬럼을 조회하는데 이 NAME 컬럼은 사용자의 실명이다. 이 컬럼은 Spring Security가 사용할 일이 없다. 그러나 우리가 만드는 웹 어플리케이션에서는 사용할 것이기 때문에 여기서는 Spring Security가 직접적으로 이용을 하든 안하든 사용자 정보로 다루어 지는 것들은 조회하는 query로 만들어준다. authoritiesByUsernameQuery는 사용자 계정 이름(로그인 아이디)를 조건으로 주어 권한을 조회하는 쿼리를 설정해주면 된다. 이전의 DB 사용 버전 테이블 정의 블로그 글에 보면 DB 스키마를 볼수 있는데 MEMBER_ROLE 테이블이 있다. 이 테이블에 들어가는 데이터는 user1, admin 형태로 값이 들어간다. 즉 user1 아이디는 admin이란 권한 코드를 가지고 있다는 식이다. enableGroup 프로퍼티는 위에서 언급했던 그룹 권한 사용 여부를 설정하는 것으로 이 부분을 false로 해서 그룹 권한은 사용하지 않는 것으로 했다. 이렇게 만든 설정을 이용해서 로그인 과정을 거쳐보면 로그인 과정이 DB를 이용해서 진행됨을 알 수 있다. 테스트를 할 때는 회원 테이블(MEMBER_INFO) 테이블과 회원-권한 테이블(MEMBER_ROLE)에 값을 맞게 설정해야 테스트를 진행할 수 있다.

 

이번 글에서는 DB를 이용한 인증과 권한 설정에 대해 다루었다. 다음에는 이렇게 로그인 해서 설정된 사용자 정보를 jsp 페이지에서 어떻게 사용하는지를 설명하도록 하겠다.