읽은 책 정리/코드로 배우는 스프링 웹 프로젝트

[Spring] 32 JDBC를 이용하는 간편 인증/권한처리

포포015 2021. 2. 12. 15:53

인증과 권한에 대한 처리는 크게 보면 Authentication Manager를 통해 이루어지는데,

1) 이때 인증이나 권한 정보를 제공하는 존재(Provider)가 필요하고,

2) 다시 이를 위해 UserDetailsService 라는 인터페이스를 구현한 존재를 활용하게 된다.

 

UserDetailsService는 이미, 시큐리티 API 내에 이미 많은 클래스가 제공되고 있다.. 찾아보자,,,,,

이번에는 기존 DB가 존재하는 상황에서 Mybatis나 기타 프레임워크없이 사용하는 방법을 공부한다

 

JDBC를 이용하기 위한 테이블 설정

-JDBC를 이용하는 경우 사용하는 클래스는 JdbcUserDetailsManager 클래스 인데,

github 등에 공개된 코드를 보면 SQL문이 선언되있다

 

github.com/spring-projects/spring-security/blob/master/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java

 

spring-projects/spring-security

Spring Security. Contribute to spring-projects/spring-security development by creating an account on GitHub.

github.com

 

바로 실습을 시작한다. 기존 예제 security-context.xml 에서 <security:user-serivce>를 아래와 같이 변경해주자

root-context에 'dataSource' 빈(Bean)을 추가해준다.

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

 

만일 시큐리티에서 지정된 SQL문을 그대로 이용하고 싶다면 위의 링크에서 지정된 형식으로 테이블을 생성해서 데이터를 넣어주면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
create table users(
      username varchar2(50not null primary key,
      password varchar2(50not null,
      enabled char(1default '1');
 
      
 create table authorities (
      username varchar2(50not null,
      authority varchar2(50not null,
      constraint fk_authorities_users foreign key(username) references users(username));
  
-- unique인덱스 생성 (중복된값이 있으면 생성x 이미저장된 값 데이터 추가 x)   
 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 ('member00','ROLE_MANAGER'); 
insert into authorities (username, authority) values ('admin00','ROLE_MANAGER'); 
insert into authorities (username, authority) values ('admin00','ROLE_ADMIN');
commit;
cs

 

이대로 실행을 시키면,  자동으로 필요한 쿼리들이 호출된다

( 하지만 위의 코드는 패스워드가 평문으로 처리 되었기때문에 예외가 발생하게됨)

 

 

PasswordEncoder 문제 해결

스프링 시큐리티 5부터는 기본적으로 PasswordEncoder를 지정해야한다.

(임시로 {noop} 를 사용해 진행 했지만, DB를 이용하는경우 PasswordEncoder라는것을 이용해야한다)

 

PasswordEncoder는 인터페스로 설계 되어있고, 여러 종류의 구현 클래스가 존재한다.

예제에선 직접 암호화가 없는 PasswordEncoder를 구현해서 사용해보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Log4j
public class CustomNoOpPasswordEncoder implements PasswordEncoder{
 
    @Override
    public String encode(CharSequence rawPassword) {
 
        log.warn("before encoder: " + rawPassword);
        
        return rawPassword.toString();
    }
 
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
 
        log.warn("matches: " + rawPassword + ":" + encodedPassword);
        
        return rawPassword.toString().equals(encodedPassword);
    }
}
cs

 

security-context.xml에 CustomNoOpPasswordEncoder 클래스를 빈으로 등록하고,

password-encoder ref로 설정을해준다( 로그인을 해보면 JDBC를 이용해서 처리 되는것을 볼수 있다)

1
2
<bean id="customNoOpPasswordEncoder" class="org.zerock.security.CustomNoOpPasswordEncoder"></bean>


<security:authentication-manager>

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

</security:authentication-provider>

</security:authentication-manager>
cs

 

 

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

기본적으로 이용하는 테이블 구조를 그대로 생성해서 사용하는것도 나쁘진않지만,

이미 구축되어 있다면 이를 사용하는건더 복잡하게 느껴질수 있다.

JDBC를 이용하고 기존에 테이블이 있다면 약간의 지정된 결과를 반환하는 쿼리를 작성해주는 작업으로 처리가능하다

<security:jdbc-user-sevice>태그 에는 여러 속성이 있다, 속성에 적당한 쿼리문 지정하면 JDBC를 그대로 사용가능하다

 

인증/권한을 위한 테이블 설계

인코딩된 패스워드를 활용해서 현실적인 예제를 작성하겠다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create table tbl_member(
      userid varchar2(50not null primary key,
      userpw varchar2(100not null,
      username varchar2(100not null,
      regdate date default sysdate, 
      updatedate date default sysdate,
      enabled char(1default '1');
 
 
create table tbl_member_auth (
     userid varchar2(50not null,
     auth varchar2(50not null,
     constraint fk_member_auth foreign key(userid) references tbl_member(userid)
);
cs

 

BCryptPasswordEncoder 클래스를 이용한 패스워드 보호

-BCryptPasswordEncoder클래스를 이용해 패스워드를 암호화 해서 처리한다.

bcrypt는 태생자체가 패스워드를 저장하는 용도로 설계된 해시함수로 특정 문자열을 암호화 하고,

체크하는 쪽에서는 암호화된 패스워드가 가능한 패스워드 인지만 확인하고 다시 원문으로 되돌리기 불가

(BCryptPasswordEncoder는 이미 시큐리티 API에 포함되있으므로 security-context.xml에 빈으로 등록만한다.)

 

security-context.xml에 빈을 추가하고,  manager 부분에 ref 설정을 걸어준다

1
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />


<security:authentication-manager>


<security:authentication-provider>

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

</security:authentication-provider>

</security:authentication-manager>
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package org.zerock.security;
import java.sql.Connection;
import java.sql.PreparedStatement;
 
import javax.sql.DataSource;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import lombok.Setter;
import lombok.extern.log4j.Log4j;
 
@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 MemberTests {
 
  @Setter(onMethod_ = @Autowired)
  private PasswordEncoder pwencoder;
  
  @Setter(onMethod_ = @Autowired)
  private DataSource ds;
  
  @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 = ds.getConnection();
        pstmt = con.prepareStatement(sql);
       //비밀번호는 암호화로 등록. pw(i) 가비밀번호가됨
        pstmt.setString(2, pwencoder.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(Exception e) {
        e.printStackTrace();
      }finally {
        if(pstmt != null) { try { pstmt.close();  } catch(Exception e) {} }
        if(con != null) { try { con.close();  } catch(Exception e) {} }
        
      }
    }//end for
  }
  
  @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 = ds.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_MEMBER");
          
        }else {
          
          pstmt.setString(1"admin"+i);
          pstmt.setString(2,"ROLE_ADMIN");
          
        }
        
        pstmt.executeUpdate();
        
      }catch(Exception e) {
        e.printStackTrace();
      }finally {
        if(pstmt != null) { try { pstmt.close();  } catch(Exception e) {} }
        if(con != null) { try { con.close();  } catch(Exception e) {} }
        
      }
    }//end for
  }
 
  
}
cs

위와 같이 테스트 코드를 돌려보면, 아래와 같이 암호화 처리가 된다.

 

 

쿼리를 이용하는 인증

테이블 구조를 이용하는 경우에는

인증을 하는데 필요한 쿼리(users-by-username-query),

권한을 확인하는데 필요한 쿼리(authorities-by-username-query) 를이용해서 처리한다

 

1
2
3
4
5
6
7
            <!-- 테이블 구조를 이용하는 인증을 위한 쿼리 -->
                <!-- 권한을 확인하는데 필요한 쿼리 -->
            <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:password-encoder ref="bcryptPasswordEncoder"/>
            
cs

admin90/pw90 으로 로그인하면 정상처리가되는것을 확인할수 있다.

 

JDBC만 이용하면 제한적인 부분이 많아 아쉬운게 많으니 33장에서 활용해서 사용하도록하겠따