romworld

Spring 24 - security (테이블을 생성해 실습해보자 DB사용), LEFT OUTER JOIN, 구글 카멜변환, 로그인, 로그아웃 본문

Spring

Spring 24 - security (테이블을 생성해 실습해보자 DB사용), LEFT OUTER JOIN, 구글 카멜변환, 로그인, 로그아웃

inderrom 2023. 2. 14. 17:19

데이터 베이스 생성

  • COMMENT로 설명을 꼭 설정해주는 게 좋다! 필수!
  • AUTHORITIES에서 기본키를 복합키로 해주는 이유는 한 사용자당 권한을 여러개 줄 수 있기 때문
--사용자
CREATE TABLE USERS(
    USERNAME VARCHAR2(50),
    PASSWORD VARCHAR2(300),
    ENABLED VARCHAR2(1),
    CONSTRAINT PK_USERS PRIMARY KEY(USERNAME)
);

COMMENT ON TABLE USERS IS '사용자';
COMMENT ON COLUMN USERS.USERNAME IS '아이디';
COMMENT ON COLUMN USERS.PASSWORD IS '비밀번호';
COMMENT ON COLUMN USERS.ENABLED IS '사용여부';

CREATE TABLE AUTHORITIES(
    USERNAME VARCHAR2(50),
    AUTHORITY VARCHAR2(300),
    CONSTRAINT PK_AUTHORITIES PRIMARY KEY(USERNAME, AUTHORITY),
    CONSTRAINT FK_AUTHORITIES FOREIGN KEY(USERNAME) REFERENCES USERS(USERNAME)
);

 

  • ENABLED 컬럼 기본값 1로 설정

 


USER테이블 INSERT

 

AUTHORITIES 테이블 INSERT

  • ROLE_(언더바 있음)
  • admin은 member와 admin 권한이 있다.

 

 

 

<security-context.xml>

  • 기존에는 사용자 정의로 설정해줬다면
  • 이제는 DB를 사용해보자
  • <security:jdbc-user-service data-source-ref="dataSource"/>
  • ref에 root-context에서 설정한 데이터 소스를 적어준다.
  • <security:password-encoder ref="customPasswordEncoder" />
  • - 암호화를 안 쓰겠다는 의미
  • <bean id="customPasswordEncoder"></bean>
  • 자바빈 객체 등록

    <!-- 사용자가 정의한 비밀번호 암호화 처리기를 빈으로 등록함 -->
    <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean>

	<security:authentication-manager>
		<!-- 지정된 아이디와 패스워드로 로그인이 가능하도록 설정 -->
		<security:authentication-provider>
			<!-- DB를 사용하겠다는 의미. 데이터 소스(root-context.xml)를 지정. -->
			<security:jdbc-user-service data-source-ref="dataSource"/>
			<!-- 사용자가 정의한 비밀번호 암호화 처리기를 지정(암호화를 안쓰겠다는 의미) -->
			<security:password-encoder ref="customPasswordEncoder"/>
<!-- 			<security:user-service> -->
			<!-- 메모리상에 아이디와 패스워드를 지정하고 로그인을 처리함
			스프링 시큐리티 5버전부터는 패스워드 암호화 처리기를 반드시 이용해야 함
			암호화 처리기를 사용하지 않도록 noop 문자열을 비밀번호 앞에 사용함  
				
			 -->
<!-- 				<security:user name="member" password="{noop}1234" authorities="ROLE_MEMBER"/> -->
<!-- 				<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN"/> -->
<!-- 			</security:user-service> -->
		</security:authentication-provider>
	</security:authentication-manager>

 

<CustomNoPasswordEncoder.java>

  • security 패키지에 클래스 생성
package kr.or.ddit.security;

import org.springframework.security.crypto.password.PasswordEncoder;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomNoOpPasswordEncoder implements PasswordEncoder {
	
	/*
	스프링 시큐리티 5부터 기본적으로 PasswordEncoder를 지정해야 함
	제대로 하려면 USERS 테이블의 PASSWORD 컬럼에 들어갈 비밀번호를 암호화하여 저장해야 함
	테스트를 위해서 생성한 데이터는 암호화를 처리하지 않았으므로 아무 처리를 하지 않고 로그인하면 당연히
	로그인 오류가 발생할 것임
	그래서, 암호화를 하지 않는 PasswordEncoder를 직접 구현하여 지정하면
	로그인 시 암호화를 고려하지 않으므로 로그인이 잘 됨
	*/
	@Override
	public String encode(CharSequence rawPassword) {
		log.warn("before encode : " + rawPassword);
		// 인코딩 프로세스가 없음. 인코딩은 안하겠다라는 의미
		return rawPassword.toString();
	}

	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		log.warn("matches : " + rawPassword + " : " + encodedPassword);
		return rawPassword.toString().equals(encodedPassword);
	}
	
}

 

<security:intercept-url pattern="/board/list" access="permitAll" /> <security:intercept-url pattern="/board/register" access="hasRole('ROLE_MEMBER')" /> <security:intercept-url pattern="/notice/list" access="permitAll" /> <security:intercept-url pattern="/notice/register" access="hasRole('ROLE_ADMIN')" />

url를 입력해서 주소를 들어가면 권한을 준 사용자만 접근 가능하다.

 

 


MEM 테이블 생성

CREATE TABLE MEM(
    USER_NO NUMBER,
    USER_ID VARCHAR2(50),
    USER_PW VARCHAR2(300),
    USER_NAME VARCHAR2(300),
    COIN NUMBER,
    REG_DATE DATE,
    UPD_DATE DATE,
    ENABLED VARCHAR2(1),
    CONSTRAINT PK_MEM PRIMARY KEY(USER_NO)
);

 

  • COMMENTS 추가
  • 기본값 설정: UPD_DATE : SYSDATE , ENABLED : '1' 
  • 컬럼에 _ (언더바) 두 단어를 이어준 것

 

 

MEM 테이블 INSERT

  • COIN까지 값을 넣어주고 나머지 컬럼들은 NULL

 

MEM_AUTH 테이블 생성

CREATE TABLE MEM_AUTH(
    USER_NO NUMBER,
    AUTH VARCHAR2(50),
    CONSTRAINT PK_MEM_AUTH PRIMARY KEY(USER_NO, AUTH),
    CONSTRAINT FK_MEM_AUTH FOREIGN KEY (USER_NO) REFERENCES MEM(USER_NO)
);

 

MEM_AUTH 테이블 INSERT

 

 

<security-context.xml>

암호화를 처리기를 사용하는 경우

  • <security:password-encoder ref="bcryptPasswordEncoder"/>
  • 자바빈 객체 등록

 <!-- 스프링 시큐리티에서 제공하는 BCrypPasswordEncoder 클래스를 자바빈으로 등록함 -->  
    <bean id="bcryptPasswordEncoder" 
    	class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>

 

** 다만 지금은 사용하지 않을 거라 다시 주석처리 해줌 

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

** <!-- 지정된 아이디와 패스워드로 로그인이 가능하도록 설정 
authentication-provider : 인증(로그인) 제공자
-->
<security:authentication-provider user-service-ref="customUserDetailsService">

 

 

<!-- 사용자정의 사용자 상세 기능 -->
<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>

<?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">
      
    <!--  customAccessDenied를 자바빈(객체)으로 등록함 --> 
    <bean id = "customAccessDenied" 
    	class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
    	
    <bean id="customLoginSuccess"
    class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>	
    
    <!-- 사용자가 정의한 비밀번호 암호화 처리기를 빈으로 등록함 -->
    <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean>
    
    <!-- 사용자정의 사용자 상세 기능 -->
    <bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>
      
    <!-- 스프링 시큐리티에서 제공하는 BCrypPasswordEncoder 클래스를 자바빈으로 등록함 -->  
<!--     <bean id="bcryptPasswordEncoder"  -->
<!--     	class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>   -->
      
	<!-- xmlns : xml namespace의 약자  
		Role : 권한(authorization)
		permitAll : 누구나 접근 가능 / hasRole : 권한을 갖은자인가?
	-->
		
	<security:http>
	<!-- URI 패턴으로 접근 제한을 설정 -->
		<security:intercept-url pattern="/board/list" access="permitAll" />
		<security:intercept-url pattern="/board/register" 
						access="hasRole('ROLE_MEMBER')" />
		<security:intercept-url pattern="/notice/list" access="permitAll" />
		<security:intercept-url pattern="/notice/register" 
						access="hasRole('ROLE_ADMIN')" />
	
		<!-- 폼 기반 인증 기능을 사용 -->
		<!-- 접근 제한에 걸리면 시큐리티가 기본적으로 제공하는 로그인 페이지로 이동 -->
<!-- 		<security:form-login/> -->

		<!-- 사용자가 정의한 로그인 페이지의 URI를 지정함  -->
		<!-- customLoignSuccess를 인증(로그인) 성공 처리자로 지정함 -->
		<security:form-login login-page="/login"
			authentication-success-handler-ref="customLoginSuccess"
		/>
		
		<!--
		로그인이 된 회원중에 권한이 없을 때..
		 접근access 거부 denided 처리자 handler 의 URI를 지정 -->
<!-- 		<security:access-denied-handler error-page="/accessError" /> -->
	
		<!-- 등록한 CustomAccessDeniedHandler를 접근 거부 처리자로 지정함 
		customAccessDenied 객체를 reference(참조-바라본다)함-->
		<security:access-denied-handler ref="customAccessDenied"/>
	</security:http>
	
	
	<!-- authentication : 인증(로그인)
	1) 회원 게시판(Board)
		가) 목록("/board/list") : 모두가 접근 가능
		나) 등록 ("/board/register"): 로그인한 회원만 접근 가능
	2) 공지사항 게시판(Notice)
		가) 목록 ("/notice/list"): 모두가 접근 가능	 
		나) 등록 ("/notice/register"): 로그인한 관리자만 접근 가능
		
		1  회원(USERS)테이블 : USERNAME, PASSWORD, ENABLED
		
		N 권한(AUTH)테이블 : USERNAME, AUTHORIZE
	-->
	<security:authentication-manager>
		<!-- 지정된 아이디와 패스워드로 로그인이 가능하도록 설정 
		authentication-provider : 인증(로그인) 제공자
		-->
		<security:authentication-provider user-service-ref="customUserDetailsService">
			<!-- DB를 사용하겠다는 의미. 데이터 소스(root-context.xml)를 지정. -->
<!-- 			<security:jdbc-user-service data-source-ref="dataSource"/> -->
			<!-- 사용자가 정의한 비밀번호 암호화 처리기를 지정(암호화를 안쓰겠다는 의미) -->
			<security:password-encoder ref="customPasswordEncoder"/>
			<!-- BCryptPasswordEncoder 비밀번호 암호화 처리기를 사용하겠다는 의미 -->
<!-- 			<security:password-encoder ref="bcryptPasswordEncoder"/> -->
<!-- 			<security:user-service> -->
			<!-- 메모리상에 아이디와 패스워드를 지정하고 로그인을 처리함
			스프링 시큐리티 5버전부터는 패스워드 암호화 처리기를 반드시 이용해야 함
			암호화 처리기를 사용하지 않도록 noop 문자열을 비밀번호 앞에 사용함  
				
			 -->
<!-- 				<security:user name="member" password="{noop}1234" authorities="ROLE_MEMBER"/> -->
<!-- 				<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN"/> -->
<!-- 			</security:user-service> -->
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

 결국 나중에는 주석처리한 부분을 빼고 사용한다.

 

 

LEFT OUTER JOIN ( 쿼리문 작성 )

  • mem과 mem_auth 테이블에서 mem을 추가했을 때 일반 내부 조인을 쓴다면
  • 원하는 값이 출력이 되지 않는다
  • mem_auth 테이블 데이터에 권한이 없기 때문에
  • 그래서 LEFT OUTER JOIN으로 묶어줘야지 원하는 값을 출력할 수 있다.
SELECT A.USER_NO, A.USER_ID, A.USER_PW, A.USER_NAME, A.COIN, A.REG_DATE, A.UPD_DATE, A.ENABLED
     , B.USER_NO, B.AUTH
FROM   MEM A LEFT OUTER JOIN MEM_AUTH B
ON(A.USER_NO = B.USER_NO)
WHERE  A.USER_ID = 'member1';

--> mem 테이블의 member1에 admin 권한을 추가해주자!

-->mem_auth 테이블에 member1에 admin 추가

 

 

<emp_SQL.xml>

  • 편의상 전에 작성해 두었던 매퍼 이용
<resultMap type="memVO" id="memMap">
    <result property="userNo" column="USER_NO" /> 
    <result property="userId" column="USER_ID" /> 
    <result property="userPw" column="USER_PW" /> 
    <result property="userName" column="USER_NAME" /> 
    <result property="coin" column="COIN" /> 
    <result property="regDate" column="REG_DATE" /> 
    <result property="updDate" column="UPD_DATE" /> 
    <result property="enabled" column="ENABLED" /> 

    <collection property="memAuthVOList" resultMap="memAuthMap"></collection>
</resultMap>

<resultMap type="memAuthVO" id="memAuthMap">
    <result property="userNo" column="USER_NO" />
    <result property="auth" column="AUTH" />
</resultMap>

<!-- 회원 로그인 -->
<select id="memLogin" parameterType="memVO" resultMap="memMap">
    SELECT A.USER_NO, A.USER_ID, A.USER_PW, A.USER_NAME, A.COIN, A.REG_DATE, A.UPD_DATE, A.ENABLED
         , B.USER_NO, B.AUTH
    FROM   MEM A LEFT OUTER JOIN MEM_AUTH B
    ON(A.USER_NO = B.USER_NO)
    WHERE  A.USER_ID = #{userId}
</select>

 

<EmpMapper.java>

// 회원 로그인
	// <select id="memLogin" parameterType="memVO" resultMap="memMap">
	public MemVO memLogin(MemVO memVO);

 

<EmpServiceImple.java>

	// 회원 로그인
	@Override
	public MemVO memLogin(MemVO memVO) {
		return this.empMapper.memLogin(memVO);
	}

 

<EmpService.java>

	// 회원로그인
	public MemVO memLogin(MemVO memVO);

 

<mybatisAlias.xml> 추가

     <typeAlias type="kr.or.ddit.vo.MemVO" alias="memVO" />      
      <typeAlias type="kr.or.ddit.vo.MemAuthVO" alias="memAuthVO" />

 

구글 카멜변환(VO에 들어갈 변수명 카멜표기법으로 출력)

-- 구글카멜변환
SELECT COLUMN_NAME
, DATA_TYPE
, CASE WHEN DATA_TYPE='NUMBER' THEN 'private int ' || FN_GETCAMEL(COLUMN_NAME) || ';'
WHEN DATA_TYPE IN('VARCHAR2','CHAR') THEN 'private String ' || FN_GETCAMEL(COLUMN_NAME) || ';'
WHEN DATA_TYPE='DATE' THEN 'private Date ' || FN_GETCAMEL(COLUMN_NAME) || ';'
ELSE 'private String ' || FN_GETCAMEL(COLUMN_NAME) || ';'
END AS CAMEL_CASE
, '' RESULTMAP
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME = 'MEM';

 

 

<MemVO.java>

vo패키지에 생성

  • 1 : N 관계 추가
package kr.or.ddit.vo;

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

import lombok.Data;

//자바빈 클래스
@Data
public class MemVO {
	private String enabled;
	private int userNo;
	private String userId;
	private String userPw;
	private String userName;
	private int coin;
	private Date regDate;
	private Date updDate;
	
	//MemVO : MemAuthVO = 1 : N
	private List<MemAuthVO> memAuthVOList;
}

 

<MamAuthVO.java>

package kr.or.ddit.vo;

import lombok.Data;

//자바빈 클래스
@Data
public class MemAuthVO {
	private int userNo;
	private String auth;
}

 

***<CustomUserDetailsService.java> ****

  • security 패키지 안에 클래스 생성
  • CustomUser 클래스를 만들어주자
package kr.or.ddit.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.or.ddit.mapper.EmpMapper;
import kr.or.ddit.vo.MemVO;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
	
	//DI(의존성 주입)
	@Autowired
	EmpMapper empMapper;
	
	//요청파라미터 : <input type="text" name="username"...
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 파라미터 준비(public MemVO memLogin(MemVO memVO))
		MemVO memVO = new MemVO();
		//WHERE A.USER_ID = #{userId}
		memVO.setUserId(username);
		//<resultMap type="memVO" id="memMap">
		memVO = empMapper.memLogin(memVO);
		
		log.warn("EmpMapper에 의해 쿼리를 실행할 것임 : " + memVO );
	
		//삼항 연산자. memVO가 null이면 null을 리턴하고, null이 아니면 USER를 리턴
		return memVO==null?null:new CustomUser(memVO);
	}

}

 

 

<CustomUser.java>

  • security 패키지에 클래스 생성
  • 생성자, getter/setter
package kr.or.ddit.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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.or.ddit.vo.MemAuthVO;
import kr.or.ddit.vo.MemVO;

//User : 스프링 시큐리티가 제공하고 있는 사용자 정보 클래스
public class CustomUser extends User {
	//이 memVO 객체는 JSP에서 사용할 수 있음
	private MemVO memVO;
	
	public CustomUser(String username, String password, 
			Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
	
	//생성자
	//memVO : select한 사용자 및 권한 정보가 들어있음
	//MemVO 타입의 객체 memVO를 스프링 시큐리티에서 제공해주고 있는 UsersDetails 타입으로 변환
	//회원 정보를 주면 스프링이 관리
	public CustomUser(MemVO memVO) {
		//1) username / 2) password / 3) authorities
		//memVO.getMemAuthVOList() : 
		//	2	ROLE_ADMIN
		//	2	ROLE_MEMBER
		//memVO.getMemAuthVOList().stream() : (stream -> 나란히 출력)
		//  2	ROLE_ADMIN	2	ROLE_MEMBER
		/*
		 * 이 방법을 써도 되고
		 memVO.getMemAuthVOList().stream()
			.map(auth->new SimpleGrantedAuthority(auth.getAuth()))
			.collect(Collectors.toList())
		 */
		super(memVO.getUserId(), memVO.getUserPw(), 
				getCollect(memVO));
		this.memVO = memVO;
	}
	// 이 방법을 써도 됨 . 이해하기 위해
	public static List<SimpleGrantedAuthority> getCollect(MemVO memVO){
		List<SimpleGrantedAuthority> authorities 
			= new ArrayList<SimpleGrantedAuthority>();
		
		/*
		 2	ROLE_ADMIN
		 2	ROLE_MEMBER
		 */
		List<MemAuthVO> memAuthVOList = memVO.getMemAuthVOList();		
		//memAuthVO : 2	ROLE_ADMIN
		for(MemAuthVO memAuthVO : memAuthVOList) {			
			//memAuthVO.getAuth() : ROLE_ADMIN
			SimpleGrantedAuthority authority = 
					new SimpleGrantedAuthority(memAuthVO.getAuth());
			authorities.add(authority);
		}
		
		return authorities;
	}
	
	public MemVO getMemVO() {
		return memVO;
	}

	public void setMemVO(MemVO memVO) {
		this.memVO = memVO;
	}
	
}

 

 


  • localhost/notice/register
  • member1 , 1234 입력하면 admin으로 접근 가능


연습이므로 비밀번호 암호를 테이블 생성시 해주지 않는다.

원래면 이런 식으로 설정을 해줘야함.

 

 


타일즈의 <header.jsp> 수정

  • 로그인 했을 때와 안 했을 때 권한 주는 것

  • <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 추가
  • <sec:authorize access="isAuthenticated()">
  • <sec:authorize access="isAnnoymous()">
  • <sec:authentication property="principal.memVO" var="memVO" />
  • 이미지 바꾸기
  • 로그아웃
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
   <nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">

   <!-- Sidebar Toggle (Topbar) -->
   <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
       <i class="fa fa-bars"></i>
   </button>

   <!-- Topbar Search -->
   <form
       class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
       <div class="input-group">
           <input type="text" class="form-control bg-light border-0 small" placeholder="Search for..."
               aria-label="Search" aria-describedby="basic-addon2">
           <div class="input-group-append">
               <button class="btn btn-primary" type="button">
                   <i class="fas fa-search fa-sm"></i>
               </button>
           </div>
       </div>
   </form>

   <!-- Topbar Navbar -->
   <ul class="navbar-nav ml-auto">

       <!-- Nav Item - Search Dropdown (Visible Only XS) -->
       <li class="nav-item dropdown no-arrow d-sm-none">
           <a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
               data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
               <i class="fas fa-search fa-fw"></i>
           </a>
           <!-- Dropdown - Messages -->
           <div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
               aria-labelledby="searchDropdown">
               <form class="form-inline mr-auto w-100 navbar-search">
                   <div class="input-group">
                       <input type="text" class="form-control bg-light border-0 small"
                           placeholder="Search for..." aria-label="Search"
                           aria-describedby="basic-addon2">
                       <div class="input-group-append">
                           <button class="btn btn-primary" type="button">
                               <i class="fas fa-search fa-sm"></i>
                           </button>
                       </div>
                   </div>
               </form>
           </div>
       </li>

       <!-- Nav Item - Alerts -->
       <li class="nav-item dropdown no-arrow mx-1">
           <a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
               data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
               <i class="fas fa-bell fa-fw"></i>
               <!-- Counter - Alerts -->
               <span class="badge badge-danger badge-counter">3+</span>
           </a>
           <!-- Dropdown - Alerts -->
           <div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
               aria-labelledby="alertsDropdown">
               <h6 class="dropdown-header">
                   Alerts Center
               </h6>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="mr-3">
                       <div class="icon-circle bg-primary">
                           <i class="fas fa-file-alt text-white"></i>
                       </div>
                   </div>
                   <div>
                       <div class="small text-gray-500">December 12, 2019</div>
                       <span class="font-weight-bold">A new monthly report is ready to download!</span>
                   </div>
               </a>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="mr-3">
                       <div class="icon-circle bg-success">
                           <i class="fas fa-donate text-white"></i>
                       </div>
                   </div>
                   <div>
                       <div class="small text-gray-500">December 7, 2019</div>
                       $290.29 has been deposited into your account!
                   </div>
               </a>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="mr-3">
                       <div class="icon-circle bg-warning">
                           <i class="fas fa-exclamation-triangle text-white"></i>
                       </div>
                   </div>
                   <div>
                       <div class="small text-gray-500">December 2, 2019</div>
                       Spending Alert: We've noticed unusually high spending for your account.
                   </div>
               </a>
               <a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
           </div>
       </li>

       <!-- Nav Item - Messages -->
       <li class="nav-item dropdown no-arrow mx-1">
           <a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
               data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
               <i class="fas fa-envelope fa-fw"></i>
               <!-- Counter - Messages -->
               <span class="badge badge-danger badge-counter">7</span>
           </a>
           <!-- Dropdown - Messages -->
           <div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
               aria-labelledby="messagesDropdown">
               <h6 class="dropdown-header">
                   Message Center
               </h6>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="dropdown-list-image mr-3">
                       <img class="rounded-circle" src="/resources/sbadmin2/img/undraw_profile_1.svg"
                           alt="...">
                       <div class="status-indicator bg-success"></div>
                   </div>
                   <div class="font-weight-bold">
                       <div class="text-truncate">Hi there! I am wondering if you can help me with a
                           problem I've been having.</div>
                       <div class="small text-gray-500">Emily Fowler · 58m</div>
                   </div>
               </a>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="dropdown-list-image mr-3">
                       <img class="rounded-circle" src="/resources/sbadmin2/img/undraw_profile_2.svg"
                           alt="...">
                       <div class="status-indicator"></div>
                   </div>
                   <div>
                       <div class="text-truncate">I have the photos that you ordered last month, how
                           would you like them sent to you?</div>
                       <div class="small text-gray-500">Jae Chun · 1d</div>
                   </div>
               </a>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="dropdown-list-image mr-3">
                       <img class="rounded-circle" src="/resources/sbadmin2/img/undraw_profile_3.svg"
                           alt="...">
                       <div class="status-indicator bg-warning"></div>
                   </div>
                   <div>
                       <div class="text-truncate">Last month's report looks great, I am very happy with
                           the progress so far, keep up the good work!</div>
                       <div class="small text-gray-500">Morgan Alvarez · 2d</div>
                   </div>
               </a>
               <a class="dropdown-item d-flex align-items-center" href="#">
                   <div class="dropdown-list-image mr-3">
                       <img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60"
                           alt="...">
                       <div class="status-indicator bg-success"></div>
                   </div>
                   <div>
                       <div class="text-truncate">Am I a good boy? The reason I ask is because someone
                           told me that people say this to all dogs, even if they aren't good...</div>
                       <div class="small text-gray-500">Chicken the Dog · 2w</div>
                   </div>
               </a>
               <a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
           </div>
       </li>

       <div class="topbar-divider d-none d-sm-block"></div>
       
       <!-- 스프링 시큐리티 표현식
       인증 및 권한 정보에 따라 화면을 동적으로 구성할 수 있고, 로그인 한 사용자 정보를 보여줄 수도 있음
       - hasRole("ROLE_MEMBER") : ROLE_MEMBER 권한이 있으면 true
       - hasAnyRole("ROLE_MEMBER","ROLE_ADMIN") : 여러 권한 중 하나라도 해당하는 권한이 있으면 true
       - principal : 인증된 사용자의 사용자 정보(UserDetails 인터페이스를 구현한 클래스(customUser))의 객체를 의미
       - authentication : 인증된 사용자의 인증 정보
       - permitAll : 모든 사용자에게 허용
       - denyAll : 모든 사용자를 거부
       - isAnonymous() : 로그인 하지 않은 경우 해당
       - isAuthenticated() : 로그인 한 경우 true
       - isFullyAuthenticated() : Remember-me(로그인 저장)로 인증된 것이 아닌 일반적인 방법으로 인증된 사용자의 경우  true
        -->
		
		<!-------------------------- 로그인 했을 때 시작 -------------------------->
		<sec:authorize access="isAuthenticated()">
		<!-- CustomUser.java에서 private MemVO memVO 멤버변수를 principal객체를 통해 사용 가능 -->
		<sec:authentication property="principal.memVO" var="memVO" />
       <!-- 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">
               <span class="mr-2 d-none d-lg-inline text-gray-600 small">${memVO.userName}</span>
               <img class="img-profile rounded-circle"
                   src="/resources/sbadmin2/img/타일러더크리에이터.jpg">
           </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>
                <a class="dropdown-item" href="/logout" data-toggle="modal" data-target="#logoutModal">
                    <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
                    Logout
                </a>
            </div>
        </li>
        </sec:authorize>
		<!-------------------------- 로그인 했을 때 끝 -------------------------->
		<!-------------------------- 로그인 안되었을 때 시작 -------------------------->
		<sec:authorize access="isAnonymous()">
		<li class="nav-item dropdown no-arrow">
			<a class="nav-link dropdown-toggle" href="/login" id="userDropdown" role="button"
               data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
               <span class="mr-2 d-none d-lg-inline text-gray-600 small">로그인 해주세요.</span>
               <img class="img-profile rounded-circle"
                   src="/resources/sbadmin2/img/프랭크.jpg">
           </a>
        </li>
		</sec:authorize>
		<!-------------------------- 로그인 안되었을 때 끝 -------------------------->
    </ul>

</nav>

 

 

<security-context.xml>

  • 로그아웃 처리

   <!--CustomLogoutSuccessHandler(사용자정의 로그아웃 성공 처리자)  -->
     <bean id="customLogoutSuccessHandler" class="kr.or.ddit.security.CustomLogoutSuccessHandler"></bean>

<!-- 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화 함 
		customLogoutSuccessHandler(사용자정의 로그아웃 성공 처리자) -->
		<security:logout logout-url="/logout" invalidate-session="true"
			success-handler-ref="customLogoutSuccessHandler" />

 

 

타일즈<index.jsp>에서 로그아웃 부분 Modal 수정

<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<!DOCTYPE html>
<html  class>
<head>
<script type="text/javascript" src="/resources/js/jquery-3.6.0.js"></script>
    <title>SB Admin 2 - Dashboard</title>

    <!-- Custom fonts for this template-->
    <link href="/resources/sbadmin2/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
    <link
        href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
        rel="stylesheet">

    <!-- Custom styles for this template-->
    <link href="/resources/sbadmin2/css/sb-admin-2.min.css" rel="stylesheet">

</head>

<body id="page-top  class="sidebar-mini sidebar-closed sidebar-collapse"">

    <!-- Page Wrapper -->
    <div id="wrapper">

        <!-- Sidebar /////////// aside 시작 ///////////
        /WEB-INF/views/tiles/aside.jsp를 include 
        -->
     	<tiles:insertAttribute name="aside" />
        <!-- End of Sidebar /////////// aside 끝 ///////////-->

        <!-- Content Wrapper -->
        <div id="content-wrapper" class="d-flex flex-column">

            <!-- Main Content -->
            <div id="content">

                <!-- Topbar /////////// header 시작 ///////////
				/WEB-INF/views/tiles/header.jsp를 include                
                -->
             	<tiles:insertAttribute name="header" />
                <!-- End of Topbar /////////// header 끝 ///////////  -->

                <!-- Begin Page Content /////////// body 시작 /////////// -->
                <div class="container-fluid">
					<tiles:insertAttribute name="body" />
                </div>
                <!-- /.container-fluid /////////// body 끝 /////////// -->

            </div>
            <!-- End of Main Content -->
			
            <!-- Footer /////////// footer 시작 ///////////
            /WEB-INF/views/tiles/footer.jsp를 include 
            -->
   			<tilse:insertAttribute name="footer" />
            <!-- End of Footer /////////// footer 끝 ///////////-->

        </div>
        <!-- End of Content Wrapper -->

    </div>
    <!-- End of Page Wrapper -->

    <!-- Scroll to Top Button-->
    <a class="scroll-to-top rounded" href="#page-top">
        <i class="fas fa-angle-up"></i>
    </a>

    <!-- Logout Modal-->
    <div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
        aria-hidden="true">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="exampleModalLabel">로그아웃 하시겠습니까?</h5>
                    <button class="close" type="button" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">×</span>
                    </button>
                </div>
                <div class="modal-body">세션을 종료하려면 아래의 로그아웃 버튼을 클릭해주세요</div>
                <div class="modal-footer">
                    <button class="btn btn-secondary" type="button" data-dismiss="modal">취소</button>
                  <form action="/logout" method="post">
                   	<button type="submit" class="btn btn-primary">로그아웃</button>
                   	<sec:csrfInput />
                </form>    
                </div>
            </div>
        </div>
    </div>

    <!-- Bootstrap core JavaScript-->
    <script src="/resources/sbadmin2/vendor/jquery/jquery.min.js"></script>
    <script src="/resources/sbadmin2/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>

    <!-- Core plugin JavaScript-->
    <script src="/resources/sbadmin2/vendor/jquery-easing/jquery.easing.min.js"></script>

    <!-- Custom scripts for all pages-->
    <script src="/resources/sbadmin2/js/sb-admin-2.min.js"></script>

    <!-- Page level plugins -->
    <script src="/resources/sbadmin2/vendor/chart.js/Chart.min.js"></script>

    <!-- Page level custom scripts -->
    <script src="/resources/sbadmin2/js/demo/chart-area-demo.js"></script>
    <script src="/resources/sbadmin2/js/demo/chart-pie-demo.js"></script>

</body>

</html>

 

<CustomLogoutSuccessHandler.java>

  • security 패키지에 클래스 생성
package kr.or.ddit.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

	@Override
	public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
			throws IOException, ServletException {
		//auth : 로그인 정보
		if(auth != null && auth.getDetails()!=null) {
			try {
				request.getSession().invalidate();
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
		
		response.setStatus(HttpServletResponse.SC_OK);
		
		response.sendRedirect("/");
	}

}

로그아웃 버튼을 누르면 모달창 나오고 로그아웃 됨

 

Comments