Spring 실습

Spring 실습 19일차(시큐리티 마무리)

choco2706 2024. 5. 20. 16:24

CustomUserDetailsService.java

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 org.springframework.stereotype.Service;

import kr.or.ddit.mapper.EmployeeMapper;
import kr.or.ddit.vo.EmployeeVO;
import lombok.extern.slf4j.Slf4j;

/*
UserDetailsService : 스프링 시큐리티에서 제공해주고 있는
사용자 상세 정보를 갖고 있는 인터페이스
 */
@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {
	
	@Autowired
	private EmployeeMapper employeeMapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		//1) 사용자 정보를 검색
        //username : 로그인 시 입력 받은 회원의 아이디. <input type="text" name="username"
		log.info("CustomUserDetailsService >>> " + username);
		
		EmployeeVO employeeVO = this.employeeMapper.detail(username);
		log.info("CustomUserDetailsService -> employeeVO >>> " + employeeVO );
		
		//MVC에서는 Controller로 리턴하지 않고, CustomUser로 리턴함
	      //CustomUser : 사용자 정의 유저 정보. extends User를 상속받고 있음
	      //2) 스프링 시큐리티의 User 객체의 정보로 넣어줌 => 프링이가 이제부터 해당 유저를 관리
	      //User : 스프링 시큐리에서 제공해주는 사용자 정보 클래스
	      /*
	       employeeVO(우리) -> user(시큐리티)
	       -----------------
	       userId        -> username
	       userPw        -> password
	       enabled       -> enabled
	       auth들                -> authorities
	       */
		return employeeVO == null ? null : new CustomUser(employeeVO);
	}

}

어제의 코드에서 return이 추가됐다.

삼항 연산자로 employeeVO가 null일 경우 null을 반환하고 아니면 new Customer(employee)를 반환한다.

 

new Customer()에 빨간 줄이 뜨게 될텐데 f2를 눌러 class를 만들어준다

 

Superclass 우측 Browse를 누른 후

User 클래스를 extends 한다.

해당 클래스를 누른 후 만들어주면

 

이런 식으로 만들어진다.

 

이어서 작성해보자.

package kr.or.ddit.security;

import java.util.Collection;
import java.util.Set;
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.EmployeeVO;

// User : 스프링 시큐리티의 사용자 정보를 관리하는 사용자 최상위클래스
public class CustomUser extends User {
	
	/* User 클래스의 프로퍼티
	 private String password; // java(암호화)
  	 private final String username; // A004
	 private final Set<GrantedAuthority> authorities; // 권한
	 private final boolean accountNonExpired;
	 private final boolean accountNonLocked;
	 private final boolean credentialsNonExpired;
	 private final boolean enabled;
	 */

	// User의 생성자를 처리해줌
	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		// TODO Auto-generated constructor stub
		// super : 현재 클래스의 부모 => User
		super(username, password, authorities);
	}

	public CustomUser(EmployeeVO employeeVO) {
		//사용자가 정의한 (select한) EmployeeVO 타입의 객체 employeeVO
       //스프링 시큐리티에서 제공해주고 있는 UsersDetails 타입으로 변환
       //회원정보를 보내줄테니 관리해줘
		super(employeeVO.getEmpNo(), employeeVO.getEmpPwd(),
			  employeeVO.getEmployeeAuthVOList().stream()
			  .map(auth->new SimpleGrantedAuthority(auth.getAuth()))
			  .collect(Collectors.toList())
				);
        this.employeeVO = employeeVO;
	}
    
    public EmployeeVO getEmployeeVO() {
		return employeeVO;
	}

	public void setEmployeeVO(EmployeeVO employeeVO) {
		this.employeeVO = employeeVO;
	}
}

CustomerUser / Super

 

이제 notice / register에 접속해 로그인을 해보자(Admin 권한이 있는 걸로)

로그인 시도

 

 

정상적으로 로그인이 된다.

 

 

시큐리티 표현식

aside.jsp

<sec:authentication property="principal.member" var="member" />

 

principal에 담긴 member객체를 뽑아오겠다라는 코드. 

el태그를 사용해 member객체 안의 데이터를 받아올 수 있다.

 

 <!-- /// 로그인 함 시작 /// -->
    <sec:authorize access="isAuthenticated()">
       <sec:authentication property="principal.member" var="member" />
       <sec:authentication property="principal.member.memberAuthList"/>
       <div class="user-panel mt-3 pb-3 mb-3 d-flex">
         <div class="image">
           <img src="/resources/upload/2024/05/14/7fc538d6-999e-4067-a02e-aa4552495802_A007.jpg" class="img-circle elevation-2" alt="User Image">
         </div>
         <div class="info">
           <a href="#" class="d-block">${member.userName}님 환영합니다.</a>
           <form action="/logout" method="post">
              <button type="submit" class="btn btn-block btn-secondary btn-xs">로그아웃</button>
              <sec:csrfInput />
           </form>
         </div>
       </div>
    </sec:authorize>
    <!-- /// 로그인 함 끝 /// -->

 

로그인할 데이터베이스의 데이터 정보

 

로그인 결과

 

※ 응용

<sec:authentication property="principal.member.memberAuthList" var="memberAuthList"/>
   <c:forEach var="memberAuthList" items="${memberAuthList}" varStatus="stat">
        <p>${memberAuthList.auth}</p>
   </c:forEach>

 

 

자동로그인

CREATE TABLE PERSISTENT_LOGINS(
    USERNAME VARCHAR2(200),
    SERIES VARCHAR2(200),
    TOKEN VARCHAR2(200),
    LAST_USED DATE,
    CONSTRAINT PK_PL PRIMARY KEY(SERIES)
);

 

security-context.xml

<!-- data-source를 통해 저장한 Database의
    약속된 데이터(persistent_logins)를 이용하여
    기존 로그인 정보를 기록

    token-validity-seconds : 쿠키 유효시간(초) => 7일(604800초)
     -->
 <security:remember-me data-source-ref="dataSource" 
    token-validity-seconds="604800"
 />

<!-- 
로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화(session.invalidate())함 
/logout : post방식 요청URI->form의 action="/logout" 에서 사용 

로그아웃을 하면 자동 로그인에 사용된 쿠키도 함께 삭제해 줌
-->         
 <security:logout logout-url="/logout" 
    invalidate-session="true"
    delete-cookies="remember-me,JSESSION_ID" />

 

security:logout의 경우 이전에 작성해놓은게 있을테니 함께 붙혀넣어준다.

 

<!-- 로그인 상태 유지 체크박스.
 체크 시 : PERSISTENT_LOGINS 테이블에 로그인 로그 정보가 insert 됨
-->
<input name="remember-me" type="checkbox" id="remember"> 
<label for="remember"> Remember Me </label>

 

로그인 유지 체크박스를 클릭 시 자동 로그인기능을 활성화하겠다는 jsp코드

name값이 위 remember-me와 같아야 한다.

 

서버를 재기동하고 notice / register에 접속해 로그인을 하면

persistent_logins 테이블

 

SQL테이블에 데이터가 자동으로 들어가게 되고, 

 

notice / register를 껐다가 켜도 자동로그인이 된다.

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
// request 객체 안에 있는 쿠키들을 확인
Cookie[] cookies = request.getCookies();

out.print("<p>쿠키의 갯수 : " + cookies.length + "</p>");
for(int i = 0; i<cookies.length; i++){
	out.print(cookies[i].getName() + " : " + cookies[i].getValue() + "<br>");
}
%>
<h2>로그인한 관리자만 접근 가능</h2>
<h3>/notice/register.jsp</h3>

register.jsp에 스크립틀릿 위처럼 작성하면 쿠키 내용을 볼 수 있다.

 

아까 작성한 코드로 인해

 <security:logout logout-url="/logout" 
    invalidate-session="true"
    delete-cookies="remember-me,JSESSION_ID" />

JSESSIONID는 로그아웃을 누르면 삭제된다.

 

로그아웃을 누르지 않으면 아까 설정한 쿠키 유효시간만큼 쿠키가 남아있기 떄문에 로그인이 자동으로 된다.

 

 

스프링 시큐리티 어노테이션

이전에 만들어놓은 security-context를 일부 수정한다.

<!--          <security:intercept-url pattern="/board/list" access="permitAll"/>                   access="permitAll" : 누구나 접근가능 -->
<!--          <security:intercept-url pattern="/board/register" access="hasRole('ROLE_MEMBER')"/>      회원만 접근가능 -->
<!--          <security:intercept-url pattern="/notice/list" access="permitAll"/>                  access="permitAll" : 누구나 접근가능 -->
<!--          <security:intercept-url pattern="/notice/register" access="hasRole('ROLE_ADMIN')"/>      관리자만 접근가능 -->

 

위 내용을 주석처리하고, 어노테이션으로 대체해보자

 

servlet-context 추가

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:beans="http://www.springframework.org/schema/beans"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:security="http://www.springframework.org/schema/security" 
   xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
      http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/security 
      http://www.springframework.org/schema/security/spring-security-4.2.xsd">
<!-- 스프링 시큐리티 애너테이션을 활성화
    - pre-post-annotations="enabled" -> @PreAuthorize, @PostAuthorize 활성화
      *** PreAuthorize : 특정 메소드를 실행하기 전에 role 체킹
      PostAuthorize : 특정 메소드를 실행한 후에 role 체킹
    - secured-annotations="enabled" -> @Secured를 활성화
      Secured : 스프링 시큐리티 모듈을 지원하기 위한 애너테이션
    -->
<security:global-method-security pre-post-annotations="enabled"
		secured-annotations="enabled" />

 

 

BoardController

/*
 요청URI : /board/register
 요청파라미터 : 
 요청방식 : get
 */
@PreAuthorize("hasRole('ROLE_MEMBER')")
@RequestMapping(value="/register"
            , method=RequestMethod.GET)
public String registerForm() {
    log.info("registerForm에 왔다");
    //ModelAndView가 없음
    //mav.setViewName("board/register") 생략

    return "board/register";
}

 

@PreAuthorize("hasRole('ROLE_MEMBER')")는 아까 주석 처리한

<security:intercept url-pattern="/board/register" access="hasRole('ROLE_MEMBER')"/> 와 같은 의미를 가진다.

(권한이 ROLE_MEMBER를 가지고있는 아이디만 접속 가능)

정상적으로 로그인 페이지로 이동한다.
로그인 성공

 

같은 방식으로 NoticeController에도 적용시켜준다.

 

@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/register")
public String register() {


    return "notice/register";
}

로그인과 페이지 이동이 정상적으로 이루어진다.

 

 

BookController

create의 경우 로그인한 회원과 admin만 접속할 수 있게 해보자

//요청URI : /create
//요청파라미터 : 
//요청방식 : get
// 로그인(인증)한 관리자 또는 회원(인가)만 접근 가능
@PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_MEMBER')")
@RequestMapping(value="/create",method=RequestMethod.GET)
public ModelAndView create() {
    ModelAndView mav = new ModelAndView();

    mav.addObject("title", "도서생성");
    mav.setViewName("book/create");

    return mav;
}

 

조건이 여러개일 떄 or을 사용할 수 있다. 혹은

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_MEMBER')")

hasAnyRole로 사용할 수도 있다.

로그인 성공

 

혹은 아래 방식으로도 표현할 수 있다(초기 모델)

@Secured("{'ROLE_ADMIN','ROLE_MEMBER'}")

 

또한 클래스 레벨 자체에 조건을 걸어주게 되면

(EmployeeController)

@PreAuthorize("isAuthenticated()")
@Slf4j
@RequestMapping("/employee")
@Controller
public class EmployeeController {

employee/*

 

employee 하위에 있는 모든 .jsp파일에 접근할떄마다 로그인을 해야한다.

 

[스프링시큐리티 및 csrf토큰 비활성화 방법]

1. web.xml의 filter, contextConfigLocation
2. security-context.xml에서 <security:csrf disabled="true" />

 

 

시큐리티 끝