Spring 23 - security (+ ajax에 csrf값 설정)
1. Spring Security를 설치, 환경설정 및 활용
2. 인증(Authentication) : 로그인
3. 인가(Authorization) : 로그인 후 권환
<form action = "/login" method="post>
<input type="text" name="username">
< // name="password">
<sec:csrfInput> : 해킹 위협 대응
</form>
4. 위에 세가지가 폼태그 안에 있어야함
5. users 사용자 Auth 권한은 일대다
mvn repository에서검색
- spring-security-web 5.0.7
- spring-security-config 5.0.7
- spring-security-core 5.0.7
- spring-security-taglibs 5.0.7
<pom.xml>
<!-- 스프링 시큐리티 라이브러리 의존관계 정의 시작 -->
<!-- 스프링 시큐리티를 웹에서 동작하도록 해줌 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리트 설정을 도와줌 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티 일반기능 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티와 태그라이브러리를 연결해줌 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티 라이브러리 의존관계 정의 끝 -->
run as - maven build
<web.xml>
context-param - param-value 안에 security-context 추가
<!--
web.xml : tomcat 서버의 설정
-->
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<!-- 스프링 시큐리티 설정 파일을 지정함 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml
/WEB-INF/spring/security-context.xml
</param-value>
</context-param>
Multipart filter 아래에 추가
<!-- 서블릿 필터 클래스를 서블릿 컨테이너에 등록함 -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<security-context.xml>
- <security:authentication-manager> : 나중에는 DB를 이용해 설정 , 지금은 이해하기 위해 익혀두자
<?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>
<!-- 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를 지정함 -->
<security:form-login login-page="/login"/>
<!--
로그인이 된 회원중에 권한이 없을 때..
접근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>
<!-- 지정된 아이디와 패스워드로 로그인이 가능하도록 설정 -->
<security:authentication-provider>
<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>
<BoardController.java>
package kr.or.ddit.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/board")
@Controller
public class BoardController {
// 요청 URI : /board/list : 모두가 접근 가능
@GetMapping("/list")
public String list() {
//forwarding
//board 폴더의 list.jsp를 포워딩
return "board/list";
}
// 요청 URI : /board/register : 로그인한 회원만 접근 가능
@GetMapping("/register")
public String register() {
//forwarding
return "board/register";
}
}
<NoticeController.java>
package kr.or.ddit.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/notice")
@Controller
public class NoticeControlelr {
// 요청 URI : /notice/list : 모두 접근 가능
@GetMapping("/list")
public String list() {
//forwarding
return "notice/list";
}
// 요청 URI : /notice/register : 로그인한 관리자만 접근 가능
@GetMapping("/register")
public String register() {
//forwarding
return "notice/register";
}
}
board
<list.jsp>
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<h3>Board List : 모두 접근 가능</h3>
<register.jsp>
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<h3>Board Register : 로그인한 회원만 접근 가능</h3>
notice
<list.jsp>
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<h3>Notice List : 모두 접근 가능</h3>
<register.jsp>
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<h3>Notice Register : 로그인한 관리자만 접근 가능</h3>
요청을 해보면
list는 접근 가능하지만
권한을 준 register는
security-context에서 설정해놓은 name과 password를 입력하면
접근 거부 처리자의 URI 지정
<CommonContoroller.java>
package kr.or.ddit.security;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
public class CommonContoller {
// 접근 거부 처리자의 URI를 지정
// 요청 URI : /accessError
@GetMapping("/accessError")
public String accessDenied(Authentication auth, Model model) {
log.info("access Denied : " + auth);
model.addAttribute("msg", "Access Denied");
//forwarding
//security 폴더의 accessError.jsp를 forwarding함
return "security/accessError";
}
}
<accessError.jsp>
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div class="text-center">
<div class="error mx-auto" data-text="${msg}">${msg}</div>
<p class="lead text-gray-800 mb-5">Server Error</p>
<p class="text-gray-500 mb-0">
${SPRING_SECURITY_403_EXCEPTION.getMessage()}
</p>
<a href="/lprod/list">← 처음으로</a>
</div>
- localhost/notice/register 를 입력하고 로그인시
- 관리자 권한이 아니라 member 권한을 입력하면
- acessError 페이지가 나온다. ( (로그인 된 회원 중에 권한이 없을때)
<CustomAccessDenidehandler.java>
<!-- customAccessDenied를 자바빈(객체)으로 등록함 --> <bean id = "customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler" ></bean>
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.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("handle에 왔다");
response.sendRedirect("/accessError");
}
}
- /localhost/notice/register 에서 member로 로그인하면
- 콘솔에 log가 출력되는 것을 볼 수 있다.
<CustomAccessDeniedHandler.java> 추가
package kr.or.ddit.security;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("handle에 왔다");
Map<String,Object> map = new HashMap<String, Object>();
map.put("remoteAddr", request.getRemoteAddr());
map.put("requestURI", request.getRequestURI());
map.put("serverName", request.getServerName());
map.put("serverPort",request.getServerPort());
map.put("contextPath",request.getContextPath());
log.info("map : " + map);
response.sendRedirect("/accessError");
}
}
<LoginContoller.java>
<!-- 폼 기반 인증 기능을 사용 -->
<!-- 접근 제한에 걸리면 시큐리티가 기본적으로 제공하는 로그인 페이지로 이동 -->
<!-- <security:form-login/> -->
<!-- 사용자가 정의한 로그인 페이지의 URI를 지정함 -->
<security:form-login login-page="/login"/>
package kr.or.ddit.security;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
public class LoginController {
//<secutiry:form-login login-page="/login" />
//요청 URI : /login
//요청방식 : get
//오류 메시지와 로그아웃 메시지를 파라미터로 사용해보자(없을 수도 있음)
@GetMapping("/login")
public String loginForm(String error, String logout, Model model) {
log.info("error : " + error);
log.info("logout : "+ logout );
if(error != null) {
model.addAttribute("error", "Login Error");
}
if(logout != null) {
model.addAttribute("logout", "Logout!!");
}
//forwarding
return "security/loginForm";
}
}
<loginForm.jsp>
https://startbootstrap.com/previews/sb-admin-2
outerHTML로 로그인 창 가져오기
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
</div>
<form class="user">
<div class="form-group">
<input type="email" class="form-control form-control-user"
id="exampleInputEmail" aria-describedby="emailHelp"
placeholder="Enter Email Address...">
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user"
id="exampleInputPassword" placeholder="Password">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox small">
<input type="checkbox" class="custom-control-input"
id="customCheck"> <label class="custom-control-label"
for="customCheck">Remember Me</label>
</div>
</div>
<a href="index.html" class="btn btn-primary btn-user btn-block">
Login </a>
<hr>
<a href="index.html" class="btn btn-google btn-user btn-block">
<i class="fab fa-google fa-fw"></i> Login with Google
</a> <a href="index.html"
class="btn btn-facebook btn-user btn-block"> <i
class="fab fa-facebook-f fa-fw"></i> Login with Facebook
</a>
</form>
<hr>
<div class="text-center">
<a class="small" href="forgot-password.html">Forgot
Password?</a>
</div>
<div class="text-center">
<a class="small" href="register.html">Create an Account!</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
주소창 입력하면 귀여운..(?) 강아지가 나온다~~
<loginForm.jsp> 수정
- 불필요한 코드 삭제
- action="/login" method="post"
- <input type="text" class="form-control form-control-user"
id="username" name="username" aria-describedby="username"
placeholder="아이디를 입력해주세요"> - password
- <button type="submit" href="" class="btn btn-primary btn-user btn-block">
Login </button> - <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<%@ 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"%>
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
<h2>${error}</h2>
<h2>${logout}</h2>
</div>
<form class="user" action="/login" method="post">
<div class="form-group">
<input type="text" class="form-control form-control-user"
id="username" name="username" aria-describedby="username"
placeholder="아이디를 입력해주세요" required="required">
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user"
id="password" name="password" placeholder="비밀번호를 입력해주세요" required="required">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox small">
<input type="checkbox" class="custom-control-input"
id="customCheck"> <label class="custom-control-label"
for="customCheck">Remember Me</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-user btn-block">
Login </button>
<hr />
<!--
CSRF(Cross-site request forgery)
크로스 사이트 요청 위조는 웹 사이트 취약점 공격의 하나로,
사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위
(수정, 삭제, 등록)를 웹사이트에 요청하게 하는 공격
-->
<sec:csrfInput/>
</form>
<hr>
<div class="text-center">
<a class="small" href="register.html">Create an Account!</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<security-context.xml> 추가
- <bean id="customLoginSuccess"
class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean> - <!-- 사용자가 정의한 로그인 페이지의 URI를 지정함 -->
<!-- customLoignSuccess를 인증(로그인) 성공 처리자로 지정함 -->
<security:form-login login-page="/login"
authentication-success-handler-ref="customLoginSuccess"
/>
<?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>
<!-- 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>
<!-- 지정된 아이디와 패스워드로 로그인이 가능하도록 설정 -->
<security:authentication-provider>
<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>
<CustomLoginSuccessHandler.java>
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.core.userdetails.User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication auth )
throws ServletException, IOException {
// ~했을 때 로그인을 성공
log.info("onAuthemticationSuccess");
User costomUser = (User) auth.getPrincipal();
log.info("username : " + costomUser.getUsername());
super.onAuthenticationSuccess(request, response, auth);
}
}
실행하면 콘솔로 출력됨
ajax 데이터 전송 전 헤더에 csrf값 설정
$.ajax({
url : "/project/syncPromemProfile",
data : JSON.stringify(altVO),
contentType: "application/json;charset=utf-8",
type : "post",
beforeSend : function(xhr) { // 데이터 전송 전 헤더에 csrf값 설정
xhr.setRequestHeader("${_csrf.headerName}", "${_csrf.token}");
},
success : function(res) {
}
});
폼태그 안에 꼭 다 csrf까지 다 써줘야 완성됨! 기억하자!