Spring 실습 19일차(시큐리티 마무리)
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를 누른 후
해당 클래스를 누른 후 만들어주면
이런 식으로 만들어진다.
이어서 작성해보자.
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;
}
}
이제 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에 접속해 로그인을 하면
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 하위에 있는 모든 .jsp파일에 접근할떄마다 로그인을 해야한다.
[스프링시큐리티 및 csrf토큰 비활성화 방법]
1. web.xml의 filter, contextConfigLocation
2. security-context.xml에서 <security:csrf disabled="true" />
시큐리티 끝