[Spring]JDBC 응용 Spring Security WEB

※ pom.xml 및 root-context.xml 설정(JDBC)
- JDBC 설정 사전준비


※ DB 테이블 준비
- Spring-Security 에서의 이용을 위한 table 설정
- Spring-Security 에서 지정된 기본 SQL 구문의 사용을 위해서는 아래와 같은 테이블 구성이 필요

1
2
3
4
5
6
7
8
9
10
11
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));
);
cs


※ Spring-Security 제공 SQL 구문을 사용하는 경우
1
2
3
4
5
<security:authentication-manager>
    <security:authentication-provider>
        <security:jdbc-user-service data-source-ref="dataSource"/>
    </security:authentication-provider>
</security:authentication-manager>
cs

xml 파일에서 접근 계정의 추가는 <security:user> 태그로 추가하며, 이에 대한 password encoding 문제는 {noop} 키워드로 처리 했다.

String-Security 에서 password encoding 또한 사용자에 의해 커스텀이 가능하다.
- 상단 Bean 객체 등록

- Password encoding 처리

- 별도의 암호화가 없는 CustomNoOpPasswordEncoder  클래스를 생성
- PasswordEncoder 를 상속 받아 사용


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
package org.zerock.security;
 
import org.springframework.security.crypto.password.PasswordEncoder;
 
import lombok.extern.log4j.Log4j;
 
//passwordencode를 상속
//별도의 직접적인 암호화는 사용하지 않음
@Log4j
public class CustomNoOpPasswordEncoder implements PasswordEncoder{
 
    //패스워드 encoding
    @Override
    public String encode(CharSequence rawPassword) {
        // TODO Auto-generated method stub
        log.warn("before encode : "+rawPassword);
        
        return rawPassword.toString();
    }
 
    //password 일치 여부 확인
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // TODO Auto-generated method stub
        log.warn("match : "+rawPassword+":"+encodedPassword);
        return rawPassword.toString().equals(encodedPassword);
    }
 
}
 
 
cs


암호화 및 별도의 데이터 베이스 사용
- Bean 객체 등록 후


- security:password-encoder ref 등록



※암호화를 사용하는 경우(별도의 데이터 베이스 사용)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,
    authority varchar2(50not null,
    constraint fk_member_authorities_auth foreign key(userid) references tbl_member(userid));
);
 
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
package org.zerock.controller;
 
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 testInsertMemeber() {
        String sql = "insert into tbl_memeber(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);
                
                        
                //PasswordEncoder.encode(문자열)
                //해당 문자열에 대해 암호화 수행
                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) {
                // TODO: handle exception
                e.printStackTrace();
            }finally {
                if(pstmt != null) {try{pstmt.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 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) {
                // TODO: handle exception
                e.printStackTrace();
            }finally {
                if(pstmt != null) {try{pstmt.close();}catch(Exception e) {}}
                if(con != null) {try{con.close();}catch(Exception e) {}}
            }
            
        }
        
    }
    
}
 
cs

- userpw 컬럼의 데이터가 암호화 된 것을 확인 가능

- 계정에 대한 로그인은 security-context.xml 에서 설정

- security:jdbc-user-service 속성 값으로 설정
- users-by-username-query : DB를 통해 로그인 기능(계정 조회 등)
- authorities-by-username-query : DB를 통해 해당 계정에 대한 권한 조회

사용자 설정 DB 사용 시, 사용자 설정 컬럼을 이용하기 위해서는 security:authentication-provider 옵션을 설정할 필요가 있다.

- tbl_member, tbl_member_auth 에 대한 DTO(VO) 생성
- 해당 값에 대한 조회 SQL 구문 작성(mapper)
- 동작을 위한 service 작성

※DTO(VO) 설정
- tbl_member DTO







-tbl_member_auth DTO




tbl_member DTO의 경우 tbl_member_auth 의 값을 함께 가져오는 방식(조인 후 조회)을 이용하기 위해 위와 같이 작성함

조인 후 하나의 DTO 로 값을 가져오는 방법도 있으나, 확장성과 재사용성을 위해 mapper에서 resultMap 태그를 이용하여 조인한 후 조회한 값을 저장하는 방식을 사용한다.

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.zerock.mapper.MemberMapper">
 
<!--tbl_member 값을 저장하기 위한 vo, tbl_member_auth 의 권한 값을 조인하여 함께 저장함-->
<resultMap type="org.zerock.domain.MemberVO" id="memberMap">
    <id property="userid" column="userid"/>
    <result property="userid" column="userid"/>
    <result property="userpw" column="userpw"/>
    <result property="userName" column="username"/>
    <result property="regDate" column="regdate"/>
    <result property="updateDate" column="updatedate"/>
    <!--auth 에 대한 값을 저장-->
    <collection property="authList" resultMap="authMap"></collection>
</resultMap>
 
<!--tbl_member_auth 의 값을 저장-->
<resultMap type="org.zerock.domain.AuthVO" id="authMap">
    <result property="userid" column="userid"/>
    <result property="auth" column="auth"/>
</resultMap>
 
 
<!--tbl_memberm, tbl_member_auth 의 테이블을 조인하여 값을 저장-->
<select id = "read" resultMap="memberMap">
select
    mem.userid, userpw, username, enabled, regdate, updatedate, auth
from 
    tbl_member mem left outer join tbl_member_auth auth on mem.userid = auth.userid
where
    mem.userid = #{userid}
 
</select>
 
 
</mapper>
cs

- MemberMapper interface
- 해당 userid 값에 대해 select(join 포함) 수행


이때 mapper에 의해 반환된 tbl_member, tbl_member_auth 의 값을 저장할 별도의 VO 사용

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
package org.zerock.security.domain;
 
import java.util.Collection;
import java.util.stream.Collectors;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.zerock.domain.MemberVO;
 
import lombok.Data;
 
@Data
public class CustomUser extends User {
 
    private static final long serialVersionUID = 1L;
 
    private MemberVO member;
 
    public CustomUser(String username, String password, 
            Collection<extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }
 
    public CustomUser(MemberVO vo) {
 
        super(vo.getUserid(), vo.getUserpw(), vo.getAuthList().stream()
                .map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
 
        this.member = vo;
    }
}
 
cs
- mapper를 통해 받은 MemberVO 의 값을 spring-security에서 사용 가능한 인스턴스로의 가공이 요구된다.
- MemberVO는 User 클래스(Spring-sercurity 기본 제공 user 용)로 변환
- AuthVO 는 GrantedAuthority 클래스 객체로의 변환이 필요하다.


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
package org.zerock.security;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.zerock.domain.MemberVO;
import org.zerock.mapper.MemberMapper;
import org.zerock.security.domain.CustomUser;
 
import lombok.Setter;
import lombok.extern.log4j.Log4j;
 
//UserDetailsService을 상속 받아 커스텀된 로그인 방식을 
@Log4j
public class CustomUserDetailService implements UserDetailsService {
    @Setter(onMethod_ = @Autowired)
    private MemberMapper mapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        //로그인 유저에 대한 유저명 출력
        log.warn("load user by username : "+username);
        
        //로그인 유저에 대한 정보 및 권한 저장
        MemberVO vo = mapper.read(username);
        
        log.warn("queried by member mapper : "+vo);
        
 
        //값이 출력 되었을 때만 객체 반환, CustomUser = MemberVO(AuthVO 포함)
        return vo == nullnull:new CustomUser(vo);
    }
 
}
 
cs


- security-context 설정
- 상단에 Bean 객체 생성

- security:authentication-provider user-service-ref 설정


로그인 사용자 정보의 확인
jsp 파일에서 로그인 한 사용자의 정보 객체의 사용





- admin.jsp 파일
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
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>/sample/admin page</h1>
 
<p>principal : <sec:authentication property="principal"/></p>
<p>MemberVO : <sec:authentication property="principal.member"/></p>
<p>사용자이름 : <sec:authentication property="principal.member.userName"/></p>
<p>사용자아이디 : <sec:authentication property="principal.member.userid"/></p>
<p>사용자 권한 리스트 : <sec:authentication property="principal.member.authList"/></p>
 
 
<a href="/customLogout">logout</a>
</body>
</html>
 
cs

- 로그인 후 jsp 페이지 에서 로그인한 사용자의 정보를 이용하기 위해서는 아래의  taglib 의 import 가 요구됨 


- sec:authentication property="principal. ~" 방식으로 진행됨



※ 표현식을 이용한 동적 화면 구성

- sec taglib을 이용하여 로그인 여부에 따라 페이지의 동적 구성이 가능


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
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>    
    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<!-- all or member or admin -->
<h1>/sample/all page</h1>
 
<!--어떠한 권한을 가진 사용자로 로그인 하지 않은 경우-->
<sec:authorize access="isAnonymous()">
 
  <a href="/customLogin">로그인</a>
 
</sec:authorize>
 
<!--어떠한 권한을 가진 사용자로 로그인 한 -->
<sec:authorize access="isAuthenticated()">
 
  <a href="/customLogout">로그아웃</a>
 
</sec:authorize>
 
</body>
</html>
 
 
cs


※표현식 항목


자동 로그인 remember-me
-cookie 를 이용하여 remember-me 기능 구현
-security:remember-me 태그 이용

스프링 시큐리티 기본 사용 DB 테이블의 구조는 아래와 같다

1
2
3
4
5
6
create table persistent_logins(
    username varchar(64not null,
    series varchar(64primary key,
    token varchar(64not null,
    last_userd timestamp not null
)
cs


- <security:http> 태그 내부에 아래의 태그를 추가


- customlogin.jsp 내부에 remember-me name의 태그 추가



- remember 체크 후 로그인 시 persistent_logins 테이블에 로그인 정보 추가


로그 아웃 시 쿠키 삭제 
- invalidate-session, delete-cookies 옵션을 추가
- invaludate-session : 로그 아웃 시, 로그인 정보의 session 을 삭제
- delete-cookies : 삭제 대상이 되는 쿠키
※ JSESSION_ID : 현재 로그인한 계정 세션


실습자료

댓글