JDBC의 인증/권한 데이터와 시큐리티 연결하기

인증과 권한 처리 : Authentication Manager
인증/권한 정보 제공 : Provider <- UserDetailsService 인터페이스 구현체
UserDetailsService 구현체 API : CachingUserDetailsService, InMemoryUserDetailsManager, JdbcDaoImpl, JdbcUserDetailsManager, LdapUserDetailsManager, LdapUserDetailsService 등

1. 스프링 시큐리티가 JDBC를 이용하기 위한 테이블 설정

JdbcUserDetailsManager 클래스 이용
스프링 시큐리티가 미리 정해놓은 인증/권한 관련 쿼리 :참조

1-1. 스프링 시큐리티가 정해놓은 테이블 스키마를 생성해서 사용하는 방법

지정된 테이블 스키마 작성

. MySql 기준

create table users(
	username varchar(50) not null,
    password varchar(50) not null,
    enabled char(1)default '1',
    primary key (username));
    
create table authorities(
	username varchar(50) not null,
    authority varchar(50) not null);

-- 테스트를 위한 dump 데이터를 입력
insert into users (username, password ) values ('user00','pw00');
insert into users (username, password ) values ('member00','pw00');
insert into users (username, password ) values ('admin00','pw00');

insert into authorities (username, authority) values ('user00', 'ROLE_USER');
insert into authorities (username, authority) values ('member00', 'ROLE_MANAGER');
insert into authorities (username, authority) values ('admin00', 'ROLE_MANAGER');
insert into authorities (username, authority) values ('admin00', 'ROLE_ADMIN');

security-context.xml 설정

authentication-provider 하위에 jdbc-user-service 를 설정한다.

data-source-ref의 값인 ‘dataSource' 가 root-context.xml에 등록되어 있어야 한다.

	<security:authentication-manager>
		<security:authentication-provider>
			<security:jdbc-user-service data-source-ref="dataSource"/>
		</security:authentication-provider>
	</security:authentication-manager>

로그인 테스트

탐캣을 실행하고 http://localhost:8080/sample/admin 으로 접속하여 ‘admin00', ‘pw00' 으로 로그인 한다.

console에 아래와 같이 쿼리가 실행되는 것을 확인한다.

아직, PasswordEncoder를 등록하지 않아서 에러가 발생하지만 쿼리 결과가 실행되는 것을 확인 할 수 있다.

jdbc.sqltiming - select username,password,enabled from users where username = 'admin00' 
INFO : jdbc.resultsettable - 
|---------|-------------|
|username |authority    |
|---------|-------------|
|[unread] |ROLE_MANAGER |
|[unread] |ROLE_ADMIN   |
|---------|-------------|

(..생략..)

// PasswordEncoder 없다는 에러 발생, spring5는 반드시 있어야 함.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

1-2. 기존에 작성된 테이블 스키마를 사용하는 방법

기존의 테이블 스키마

create table tbl_member(
	userid varchar(50) not null primary key,
    userpw varchar(100) not null,
    username varchar(100) not null,
    regdate timestamp default current_timestamp,
    updatedate timestamp default current_timestamp,
    enabled char(1) default '1'
);


create table tbl_member_auth(
	userid varchar(50) not null,
    auth varchar(50) not null
);

BCryptPasswordEncoder 사용하여 PasswordEncoder 등록하기

BCryptPasswordEncoder : 태생자체가 패스워드를 저장하는 용도로 설계됨, 암호화는 가능하지만 복호화는 안됨.
BCryptPasswordEncoder는 스프링 시큐리티 API 에 이미 정의되어 있음.

security-context.xml 에 스프링 시큐리티 API 인 BCryptPasswordEncoder 를 빈으로 등록하고, password-encoder 로 추가한다.

<!-- 생략 -->
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!-- 생략 -->
<security:authentication-manager>
    <security:authentication-provider>
        <security:password-encoder ref="bcryptPasswordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

PasswordEncoder 구현하여 사용자 PasswordEncoder를 만들어보기

encode()와 matches() 만 구현하면 되는데, 하기 예제는 인코딩을 하지 않는 로직이다.

package org.example.security;

import org.springframework.security.crypto.password.PasswordEncoder;

public class CustomNoOpPasswordEncoder implements PasswordEncoder{

	@Override
	public String encode(CharSequence rawPassword) {
		// TODO Auto-generated method stub
		return rawPassword.toString();
	}

	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		// TODO Auto-generated method stub
		return rawPassword.toString().equals(encodedPassword);
	}

}

작성한 사용자 passwordEncoder 를 Bean 으로 등록하고, security 설정에 적용한다.

<bean id="customNoOpPasswordEncoder" class="org.example.security.CustomNoOpPasswordEncoder"></bean>
<!-- 생략 -->
	<security:authentication-manager>
		<security:authentication-provider>
 			<security:password-encoder ref="customNoOpPasswordEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>

스프링 시큐리티 기본 스키마에 맞추어 인증/권한 쿼리 등록하기

security-context.xml 에 user-by-username-queryauthorities-by-username-query 속성에 스프링 시큐리티에서 필요로 하는 인증/권한 정보를 가져 올 수 있는 쿼리를 등록한다.

	<security:authentication-manager>
		<security:authentication-provider>
			<security:password-encoder ref="bcryptPasswordEncoder"/>
			<security:jdbc-user-service data-source-ref="dataSource" 
            users-by-username-query="select userid, userpw, enabled from tbl_member where userid = ?"
			authorities-by-username-query="select userid, auth from tbl_member_auth where userid = ?"/>
		</security:authentication-provider>
	</security:authentication-manager>

테스트 하기

Junit Test 를 이용해 사용자 정보와 권한 정보의 덤프 데이터를 등록한다. 그러면 패스워드가 인코딩된 사용자 정보가 입력될 것이다.

package org.example.security;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/root-context.xml",
					  	"file:src/main/webapp/WEB-INF/spring/security-context.xml" })
@Log4j
public class MapperTests {

	@Setter(onMethod_ = @Autowired)
	private PasswordEncoder pwencoder;
	
	@Setter(onMethod_ = @Autowired)
	private DataSource dataSource;
	
//	@Test
//  사용자 정보 등록 
	public void testInsertMember() {
		
		String sql = "insert into tbl_member(userid, userpw, username) values (?, ?, ?);";
		
		for(int i = 0; i < 100; i++) {
			
			Connection con = null;
			PreparedStatement psmt = null;
			try {
				con = dataSource.getConnection();
				psmt = con.prepareStatement(sql);
				
				psmt.setString(2, pwencoder.encode("pw"+i));
				
				if(i<80) {
					psmt.setString(1, "user"+i);
					psmt.setString(3, "일반사용자"+i);
				}else if(i<90) {
					psmt.setString(1, "manager"+i);
					psmt.setString(3, "운영자"+i);
				}else {
					psmt.setString(1, "admin"+i);
					psmt.setString(3, "관리자"+i);
				}
				
				psmt.executeUpdate();
				
			} catch (SQLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally {
				if(psmt != null) {try {psmt.close();} catch(Exception e) {}}
				if(con != null) {try {con.close();}catch(Exception e) {}}
			}
		}
	}
	

    // 권한 정보 등록 
	@Test
	public void testInsertAuth() {
		
		String sql = "insert into tbl_member_auth (userid, auth) values(?, ?);";
		
		for(int i = 0; i < 100; i++) {
			
			Connection con = null;
			PreparedStatement psmt = null;
			try {
				 con = dataSource.getConnection();
				 psmt = con.prepareStatement(sql);
				 
				 if(i<80) {
					 psmt.setString(1, "user"+i);
					 psmt.setString(2, "ROLE_USER");
				 }else if(i<90) {
					 psmt.setString(1, "manager"+i);
					 psmt.setString(2, "ROLE_MEMBER");
				 }else {
					 psmt.setString(1, "admin"+i);
					 psmt.setString(2, "ROLE_ADMIN");
				 }
				 
				 psmt.executeUpdate();
				 
			} catch (SQLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally {
				if (psmt != null) {try{psmt.close();}catch(Exception e) {}}
				if (con != null) {try{con.close();}catch(Exception e) {}}
			}
		};
	}
}

탐캣을 실행 시키고, http://localhost:8080/sample/admin 으로 접속하여 유효한 ID와 PW로 로그인한다.
admin 페이지이므로, ‘admin90', ‘pw90' 으로 로그인하면 될것이다. 로그인이 성공하면, 콘솔에 아래와 같이 정상적으로 조회된 정보가 보여진다.

INFO : jdbc.sqltiming - select userid, userpw, enabled from tbl_member where userid = 'admin90' 
...
INFO : jdbc.resultsettable - 
|--------|-------------------------------------------------------------|--------|
|userid  |userpw                                                       |enabled |
|--------|-------------------------------------------------------------|--------|
|admin90 |$2a$10$P42FSZc7yaZBkgrH0HwKyOBzUqiS7aVt2FyFuBPCUcQ/JDsSklKcK |true    |
|--------|-------------------------------------------------------------|--------|
...
INFO : jdbc.sqltiming - select userid, auth from tbl_member_auth where userid = 'admin90' 
...
INFO : jdbc.resultsettable - 
|---------|-----------|
|userid   |auth       |
|---------|-----------|
|[unread] |ROLE_ADMIN |
|---------|-----------|