danDevlog

Spring - 19 (스프링 시큐리티 - 2) 본문

Spring 게시판 만들기

Spring - 19 (스프링 시큐리티 - 2)

단데기이 2022. 5. 2. 11:54
728x90

home.jsp를 수정한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="includes/header.jsp"%>

<script>
	self.location = "/board/list";
</script>
<!-- 로그아웃 했을때 / 로 주소접근하게 된다.
hom.jsp로 넘어온다면, 다시 /board/list로 이동 -->

<%@ include file="includes/footer.jsp"%>

 

CommonController.java 새로운 매핑을 추가해준다.

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class CommonController {
	
	@GetMapping("/customLogin")
	public void loginInput(String error, String logout, Model model) {
		if(error != null)
			model.addAttribute("error","계정을 확인해 주세요");
		if(logout != null)
			model.addAttribute("logout","로그아웃");
	}
	
	@GetMapping("/customLogout")
	public void logoutGet() {
		log.info("custom logout");
	}
}

 

header.jsp에 taglib을 추가해주고 기존 내용들을 수정 및 추가한다.

<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
<!-- Nav Item - User Information -->
                        <li class="nav-item dropdown no-arrow">
                            <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
                                data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                <sec:authorize access="isAuthenticated()">
                                	<span class="mr-2 d-none d-lg-inline text-gray-600 small">
                                	<sec:authentication property="principal.username"/>                                
                                	</span>
	                                <img class="img-profile rounded-circle"
	                                    src="/resources/img/undraw_profile.svg">
                                </sec:authorize>
                                
                                <sec:authorize access="isAnonymous()">
                                	<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
                                </sec:authorize>
                                
                                <span class="mr-2 d-done d-lg-inline text-gray-600 small"></span>
                            </a>
                            
                            <!-- Dropdown - User Information -->
                            <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
                                aria-labelledby="userDropdown">
                                <a class="dropdown-item" href="#">
                                    <i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
                                    Profile
                                </a>
                                <a class="dropdown-item" href="#">
                                    <i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
                                    Settings
                                </a>
                                <a class="dropdown-item" href="#">
                                    <i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
                                    Activity Log
                                </a>
                                <div class="dropdown-divider"></div>
                                
                                <sec:authorize access="isAuthenticated()">
                                	<a class="dropdown-item" href="/customLogout">
                                    <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
                                    Logout
                               		 </a>
                                </sec:authorize>
                                
                                <sec:authorize access="isAnonymous()">
                                	<a class="dropdown-item" href="/customLogin">
                                		<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
                                		Login
                                	</a>
                                </sec:authorize>
                                
                            </div>
                        </li>

 

회원정보가 담길 테이블과 자식테이블인 tbl_member_auth 테이블을 만들어준다.

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)
);

commit;

 

domain패키지에 MemberVO와 AuthVO 클래스를 생성한다.

package kr.icia.domain;

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class MemberVO {
	private String userid;
	private String userpw;
	private String userName;
	private boolean enabled; // 계정 정지 유무
	
	private Date regDate;
	private Date updateDate;
	private List<AuthVO> authList;
	// 하나의 아이디는 여러개의 권한 소유 가능.
}
package kr.icia.domain;

import lombok.Data;

@Data
public class AuthVO {
	private String userid; // 사용자 아이디
	private String auth; // 권한.
	// 즉, 사용자별 권한 등록.
}

 

mapper패키지에 MemberMapper 인터페이스를 생성한다.

package kr.icia.mapper;

import kr.icia.domain.MemberVO;

public interface MemberMapper {
	public MemberVO read(String userid);
	// 사용자가 아이디를 입력하면, 그에 해당하는 계정 정보를 디비에서 추출
	
}

 

쿼리를 사용하기위해 MemberMapper와 매칭되는 MemberMapper.xml 을 생성한다.

<?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="kr.icia.mapper.MemberMapper">
  
  	<!-- resultType : 자동으로 설정된 리턴 타입. resultMap : 수동으로 설정된 리턴 타입. -->
  	<resultMap type="kr.icia.domain.AuthVO" id="authMap">
  		<result property="userid" column="userid" />
  		<result property="auth" column="auth" />
  	</resultMap>
  	
  	<resultMap type="kr.icia.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"/>
  		<collection property="authList" resultMap="authMap"/>
  	</resultMap>
  	
  	<!-- 회원 정보 테이블과 회원 권한 테이블을 조인하여 1개의 타입으로 회원 관련 정보를 리턴 -->
  	
  	<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>
  	<!-- left outer join : 좌 테이블, 우 테이블 이 있을때,
  	좌 테이블을 기준으로 레코드 추출. 좌 테이블은 모두 추출, 우 테이블은 일치하는 값들 추출.
  	로그인 창에서 입력한 사용자 계정을 넘겨 받아서 일치하는 데이터 검색. -->
  
  </mapper>

 

kr.icia.security.domain 이라는 새로운 패키지의 경로에 CustomUser 클래스를 생성해준다.

package kr.icia.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 kr.icia.domain.MemberVO;

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);
		// 상속을 받으면서 의무적으로 구현한 생성자.
		// <? extends 클래스명> : 제너릭 타입의 상위 제한.
		// <? super 클래스명> : 제너릭 타입의 하위 제한.
		// <?> : 제너릭 타입 제한 없음.
	}
	
	
	public CustomUser(MemberVO vo) {
		super(vo.getUserid(), vo.getUserpw()
				, vo.getAuthList().stream()
				.map(auth -> new SimpleGrantedAuthority(
						auth.getAuth())).collect(Collectors.toList()));
		this.member = vo;
		// 사용자 아이디, 패스워드, 권한 목록으로 초기화.
	}
	
	// 사용자가 로그인 창에서 아이디와 패스워드를 입력하면,
	// 해당 아이디를 가지고 일치하는 회원 정보를 찾기. (서비스 처리)
}

 

kr.co.icia.security라는 새로운 패키지 경로에 CustomUserDetailService 클래스를 생성한다.

package kr.co.icia.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 kr.icia.domain.MemberVO;
import kr.icia.mapper.MemberMapper;
import kr.icia.security.domain.CustomUser;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Log4j
public class CustomUserDetailsService implements UserDetailsService {
   @Setter(onMethod_ = { @Autowired })
   private MemberMapper memberMapper;
   // 쿼리 조작을 위한 멥퍼 인터페이스 초기화

   @Override
   public UserDetails loadUserByUsername(String username) 
         throws UsernameNotFoundException {
      log.warn("load user by userName : " + username);
      MemberVO vo = memberMapper.read(username);
      // 전달된 id로 사용자 정보를 검색.

      return vo == null ? null : new CustomUser(vo);
      // 검색되지 않으면 널, 검색되면 해당 정보 리턴.
   }

}

 

security-context.xml에 암호 관련 내용을 추가한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


	<!-- 암호화 처리, 사용자 계정 정보 처리 -->
	<bean id="bcryptPasswordEncoder"
		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
        <!-- BCrypt 암호화 객체 생성. -->
	<bean id="customUserDetailService"
		class="kr.co.icia.security.CustomUserDetailsService" />
        <!-- 아이디 패스워드로 로그인 처리 객체 -->

	<security:http> <!-- 웹 접근에 대해서, -->
		<security:form-login login-page="/customLogin"/>
		<security:logout logout-url="/customLogout"
			invalidate-session="true"/>
	</security:http>
	
	<security:authentication-manager> <!-- 인증 처리 관리자 -->
		<security:authentication-provider
			user-service-ref="customUserDetailService"> <!-- 인증처리 공급자 -->
			<security:password-encoder ref="bcryptPasswordEncoder"/>	
		</security:authentication-provider>
	</security:authentication-manager>

</beans>

 

테스트를 위하여 src/test/java 경로에 kr.icia.security 패키지를 만들고 MeberTests.java 클래스를 만들어준다.

package kr.icia.security;

import java.sql.Connection;
import java.sql.PreparedStatement;

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 kr.icia.mapper.MemberMapper;
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 javax.sql.DataSource ds;
	
	@Setter(onMethod_ = @Autowired)
	private MemberMapper memberMapper;
	
	@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);
				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) {}
				}
			}
		}
	}
}

테스트가 잘 실행된다면 Sqldeveloper에 100개의 값이 잘 들어갈것이다.

여기서 글쓴이는 USERNAME을 UESRNAME으로 테이블을 만들때 오타를 내어,

ORA-00904 오류가 발생하여 오타를 찾을려고 2시간을 해매였다. 

 

이후 해당 ID와 PW로 로그인페이지에서 로그인을 시도해도 처리되지않는다.

왜냐하면 아직 검증 및 권한을 주지 않았기 때문이다.

 

따라서 테스트코드를 추가해서 권한을 얻는다.

MemberTests 파일에 권한을 주는 코드를 추가한다.

	@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) {}
				}
			}
		}
	}

권한까지 주었다면 이제 로그인을 해보자

테스트코드로 만들었던 아이디와 비밀번호로 로그인을 하면된다.

로그인 성공

 

로그아웃도 잘 되었다.

'Spring 게시판 만들기' 카테고리의 다른 글

Spring - 20(스프링 시큐리티-3)  (0) 2022.05.08
Spring - 18 (스프링 시큐리티)  (0) 2022.04.29
Spring - 17 (첨부파일-3)  (0) 2022.04.29
Spring - 16 (첨부파일 - 2)  (0) 2022.04.29
Spring - 15 (첨부파일)  (0) 2022.04.26
Comments