※ pom.xml 및 root-context.xml 설정(JDBC)
- 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(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)); ); | 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} 키워드로 처리 했다.
- 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 |
암호화 및 별도의 데이터 베이스 사용
※암호화를 사용하는 경우(별도의 데이터 베이스 사용)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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, authority varchar2(50) not 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 컬럼의 데이터가 암호화 된 것을 확인 가능
- 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 로 값을 가져오는 방법도 있으나, 확장성과 재사용성을 위해 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 |
- 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 == null? null: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 가 요구됨
※ 표현식을 이용한 동적 화면 구성
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(64) not null, series varchar(64) primary key, token varchar(64) not null, last_userd timestamp not null ) | cs |
- <security:http> 태그 내부에 아래의 태그를 추가
- delete-cookies : 삭제 대상이 되는 쿠키
※ JSESSION_ID : 현재 로그인한 계정 세션
실습자료
댓글
댓글 쓰기