[스프링 시큐리티] JDBC를 이용한 인증/인가 처리

charco·2021년 6월 10일
0

출처: 코드로 배우는 스프링 웹 프로젝트

이전에 in-memory 유저를 생성하여 인증과 인가를 구현했었다.
실제 서비스에는 유저의 데이터를 데이터베이스에 저장해야하니 JDBC를 이용해 인증과 인가를 처리해야 한다.

일단 MyBatis나 기타 프레임워크 없이 사용하는 방법을 익혀보자.

여기서는 oracle을 사용할 것이고 log4j2-log4jdbc로 쿼리를 확인할 것이다.
추후에 MyBatis, HikariCP를 사용할 것이니 DB연동에 대해서는 미리 설정하고 진행하자


테이블 설정

스프링 시큐리티에서는 UserDetailsService 의 구현체로 유저 정보를 불러올 수 있다. 이전에는 구현체인 InMemoryUserDetailsManager 를 이용한 것이었다.
이번엔 JdbcUserDetailsManager를 이용할 것이다.
JdbcUserDetailsManager는 특정한 쿼리로 유저 정보를 불러온다. 그대로 이용하고자 한다면 쿼리에 맞게 테이블을 짜면 된다. 다음은 sql에 날릴 쿼리문이다.

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

create table authorities(
    username varchar2(50) not null,
    authority varchar2(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);
--authorities 의 username, authority 칼럼의 조합의 중복을 허용하지않음.
--예) username = "user" authority = "ROLE_MEMBER"
--    username = "user" authority = "ROLE_ADMIN" -> 칼럼의 조합이 다르기 때문에 가능
create unique index ix_auth_username on authorities (username ,authority);

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('user00', 'ROLE_MANAGER');
insert into authorities(username, authority) values('admin00', 'ROLE_MANAGER');
insert into authorities(username, authority) values('admin00', 'ROLE_ADMIN');

--오라클은 커밋을 꼭 해주자
commit;

테이블이 생성되고 데이터가 잘 들어갔는지 확인하고 security-context.xml의 일부를 수정하자.

<security:authentication-manager>
  <security:authentication-provider>
    <!--  dataSource빈이 등록돼있어야 함 -->
    <security:jdbc-user-service data-source-ref="dataSource"/>

  </security:authentication-provider>
</security:authentication-manager>

테이블을 만들어 유저를 넣었고 dataSource빈까지 넣어줬으니 이제 애플리케이션을 실행하고 user00으로 로그인해보자.
There is no PasswordEncoder mapped for the id "null"
라는 에러가 뜰 것이다.
전에는 {noop}을 이용해 인메모리 유저의 패스워드의 인코딩 없이 구현했지만 이제는 PasswordEncoder 인터페이스의 구현체로 인코딩을 해줘야한다.
쉽게 말해 PasswordEncoder는 패스워드를 암호화해준다.

간단한 PasswordEncoder 구현체 만들어 패스워드 인코딩하기

PasswordEncoder를 상속받는 CustomNoOpPasswordEncoder를 생성하자.

public class CustomNoOpPasswordEncoder implements PasswordEncoder {

	@Override
	//인코딩하는 메서드
	public String encode(CharSequence rawPassword) {
		//패스워드를 암호화없이 그대로 저장
		return rawPassword.toString();
	}

	@Override
	//요청받은 패스워드가 인코딩된 패스워드와 일치하는지 확인하는 메서드
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		//패스워드를 인코딩 없이 저장했기때문에 문자열들을 그대로 비교
		return rawPassword.toString().equals(encodedPassword);
	}

}

이제 스프링 빈에 등록해야 한다.
security-context.xml을 수정하자.

  <bean id="customAccessDeniedHandler" class="com.green.security.CustomAccessDeniedHandler"></bean>
  <bean id="customLoginSuccess" class="com.green.security.CustomLoginSuccessHandler"></bean>
  <!--빈 등록-->
  <bean id="customPasswordEncoder" class="com.green.security.CustomNoOpPasswordEncoder"></bean>

  <security:http>
	(...생략)
	
  </security:http>

  <security:authentication-manager>
    <security:authentication-provider>

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

      <security:password-encoder ref="customPasswordEncoder"/>
      
    </security:authentication-provider>
  </security:authentication-manager>

이제 서버를 재시작해서 로그인해보자.
로그를 보면 쿼리들이 날아가는 것을 볼 수 있을 것이다.


기존의 테이블을 이용하는 경우

스프링 시큐리티가 기본적으로 이용하는 테이블 구조를 그대로 사용해도 나쁘지 않지만 로그인에 대한 테이블이 이미 만들어져있는 상태라면 다르게 구현하는 것이 좋다.
일단 스프링 시큐리티가 이용하는 테이블 구조와 다른 회원, 권한 테이블을 만들자.

create table tbl_member(
    userid varchar2(50) not null primary key,
    userpw varchar2(100) not null,
    username varchar2(100) not null,
    regdate date default sysdate,
    updatedate date default sysdate,
    enabled char(1) default '1');

create table tbl_member_auth (
    userid varchar2(50) not null,
    auth varchar2(50) not null,
    constraint fk_member_auth foreign key(userid) references tbl_member(userid)
);

BCryptPasswordEncoder를 이용해 패스워드 암호화하기

이전에 구현해봤던 PasswordEncoder는 여러 구현체를 가지고 있는데 그 중에 가장 많이 쓰이는게 BCryptPasswordEncoder이다. 해시알고리즘으로 패스워드를 길이가 60인 문자열로 암호화해준다. 스프링 시큐리티 API안에 이미 포함돼있으므로 바로 security-context.xml을 수정한다.

<bean id="customAccessDeniedHandler" class="com.green.security.CustomAccessDeniedHandler"></bean>
<bean id="customLoginSuccess" class="com.green.security.CustomLoginSuccessHandler"></bean>
<!-- <bean id="customPasswordEncoder" class="com.green.security.CustomNoOpPasswordEncoder"></bean> -->
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>

<security:http>
(...생략)
</security:http>

<security:authentication-manager>
  <security:authentication-provider>
    
    <security:jdbc-user-service data-source-ref="dataSource"/>
    <!-- <security:password-encoder ref="customPasswordEncoder"/> -->
    <security:password-encoder ref="bcryptPasswordEncoder"/>
    
  </security:authentication-provider>
</security:authentication-manager>

BCryptPasswordEncoder를 이용하기 위한 설정이 끝났다.
데이터베이스에는 패스워드가 암호화되어 저장되어있어야 한다. 테스트 코드를 작성해 더미 데이터를 넣어보자.

@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 MemberTest {
	
	@Setter(onMethod_=@Autowired)
	private PasswordEncoder passwordEncoder;
	
	@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 pstmt = null;
			
			try {
				con = dataSource.getConnection();
				pstmt = con.prepareStatement(sql);
				
				pstmt.setString(2, passwordEncoder.encode("pw" + i));
				
				if(i < 80) {
					
					pstmt.setString(1, "user" + i);
					pstmt.setString(3, "일반사용자" + i);
				
				} else if(i < 90) {
					
					pstmt.setString(1, "manager" + i);
					pstmt.setString(3, "운영자" + i);
				
				} else {

					pstmt.setString(1, "admin" + i);
					pstmt.setString(3, "관리자" + i);
				
				}
				
				pstmt.executeUpdate();
			} catch (SQLException e) {
				e.printStackTrace();
			} finally {
				if(pstmt!=null) {try {
					pstmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}}
				if(con!=null) {try {
					con.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}}
			}
			
		}//for문의 끝
	}
	

}

콘솔을 보면 아래와 같이 암호화된 패스워드가 저장되는 것을 확인할 수 있다.

INFO : jdbc.sqltiming - insert into tbl_member(userid, userpw, username) values('admin99','$2a$10$4jaAFnouYZUH781bmrM3aeVSQDfHZtPZpE1wT3YcDIumlrFm8Abgu','관리자99') 
 {executed in 1 msec}

이제 사용자의 권한을 담고있는 테이블에도 데이터를 추가하자.

@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 pstmt = null;
			
			try {
				con = dataSource.getConnection();
				pstmt = con.prepareStatement(sql);
				
				if(i < 80) {
					
					pstmt.setString(1, "user" + i);
					pstmt.setString(2, "ROLE_USER");
				
				} else if(i < 90) {
					
					pstmt.setString(1, "manager" + i);
					pstmt.setString(2, "ROLE_MANAGER");
				
				} else {

					pstmt.setString(1, "admin" + i);
					pstmt.setString(2, "ROLE_ADMIN");
				
				}
				
				pstmt.executeUpdate();
				
			} catch (SQLException e) {
				e.printStackTrace();
			} finally {
				if(pstmt!=null) {try {
					pstmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}}
				if(con!=null) {try {
					con.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}}
			}
			
		}//for문의 끝
	}
	

데이터들이 잘 들어갔는지 확인하자.

데이터를 이용해 로그인했을때 처리하는 법을 알아보자.
전에는 스프링 시큐리티가 날리는 쿼리에 맞춰 테이블을 설계했지만 이제는 우리의 테이블에 맞는 쿼리가 필요하다.
security-context.xml 을 아래와 같이 설정한다.

<security:authentication-manager>
  <security:authentication-provider>
    <!--  dataSource빈이 등록돼있어야 함 -->
    <security:jdbc-user-service data-source-ref="dataSource"
                                <!--유저의 id로 유저의 정보를 가져오는 속성-->
                                users-by-username-query="select userid, userpw, enabled from tbl_member where userid = ?"
    				<!--유저의 id로 유저의 권한 정보를 가져오는 속성-->
                                authorities-by-username-query="select userid, auth from tbl_member_auth where userid = ?"
                                />
    <!-- <security:password-encoder ref="customPasswordEncoder"/> -->
    <security:password-encoder ref="bcryptPasswordEncoder"/>
  </security:authentication-provider>
</security:authentication-manager>

이제 서버를 재시작하여 로그인해보자. 정상적으로 처리되어야 한다.

다음에는 UserDetailsService를 구현해본다.

profile
아직 배우는 중입니다

0개의 댓글