[Spring Framework]Spring Security 적용하기 - 3) 커스터마이징
- -
이번 장에서는 스프링 시큐리티의 로그인 페이지를 커스터마이징을 해보려 합니다.
1. 프로젝트 추가 설정
먼저 톰캣의 server.xml과 프로젝트의 servlet-context.xml 설정을 바꿔줘야 할 것이 있다. 첫 장에서 짚고 넘어갔어야 하는 부분인데 지나쳐버렸다...
경로는 다음과 같다.
1-1. server.xml 수정
직접 수정하지 않았다면 path의 경로가 최초 패키지 생성시 설정한 board(사용자가 설정한 패키지)로 되어있을 것이다. 이 경로를 “/”로 바꿔준다. 여기서 board를 빼주는 것은 “/board” 는 URL 상의 주소를 의미하기 때문이다.
docBase로 있는 pj_security가 Root 경로가 되며, localhost:8099/board 로 접근을 하게 된다. 만약 path 경로를 board인 상태로 둔다면 localhost:8099/login.jsp 를 검색할 때, localhost:8099/board/login.jsp 파일을 찾아 출력하게 된다.
1-2. servlet-context.xml 수정
1장에서 스프링 프레임워크를 설정함에 따라 @Controller, @Service, @repository 등의 어노테이션을 선언하여 사용한다. 여기서 base-package 옵션은 패키지를 어디서부터 스캔할지 지정한다. 따라서 최상위 경로를 지정해주면 해당 경로를 포함한 모든 하위 경로에 적용되고, 어노테이션을 명시한 자바 파일들이 빈으로 등록되어 사용 가능해진다.
이제 커스터마이징을 시작해도록 하자.
먼저, 로그인 페이지 이전에 접근 거부 페이지도 바꿔보고자 한다.
2. 접근 거부 페이지
2-1. 로그인 폴더를 만든다. 그리고 접근 거부 페이지 accessDenied.jsp 를 생성한다.
경로 : [WEB-INF]-[views]-[login]
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Insert title here</title>
</head>
<body>
권한이 없습니다!!!!!
</body>
</html>
2-2. 컨트롤러 역할을 하게 될 LoginController.java 를 만든다.
경로 : [main]-[java]-[com]-[company]-[login]
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping(value="/login/accessDenied.do")
public String accessDeniedPage() throws Exception {
return "/login/accessDenied";
}
}
2-3. 접근 불가 페이지를 핸들링하기 위해 <http> 태그 사이에 코드 한 줄을 추가하였다.
<access-denied-handler error-page="/login/accessDenied.do" />
security-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http auto-config='true'>
<intercept-url pattern="/**" access="ROLE_USER" />
<access-denied-handler error-page="/login/accessDenied.do" />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="admin" password="1234" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="guest" password="1234" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
2-4. 서버 재가동 후 테스트를 해보도록 하자.
권한이 없는 유저가 로그인할 경우 스프링 시큐리티는 접근 제한 페이지를 보여준다.
3. 로그인 페이지
3-1. 로그인 폼을 구현하기 전에 스프링 시큐리티 태그를 사용하기 위한 dependency를 추가한다.
pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
3-2. 로그인 화면을 보여줄 loginPage.jsp 파일을 생성한다.
경로 : [WEB-INF]-[views]-[login]
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html>
<html>
<head>
<title>Insert title here</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<style>
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
text-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
</style>
</head>
<body>
<form class="form-signin" action="/login.do" method="POST">
<img src="/resources/img/spring.png" style="width: 200px; height: 95px">
<h1 class="h3 mb-3 font-weight-normal"></h1>
<input type="text" name="userid" class="form-control" placeholder="ID" required autofocus>
<input type="password" name="userpw" class="form-control" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">LOGIN</button>
<c:if test="${param.err == true}">
<p style="color: red">ID와 Password를 확인해주세요.</p>
</c:if>
<p class="mt-5 mb-3 text-muted">shxrecord.tistory.com</p>
</form>
</body>
</html>
무료 템플릿을 제공하는 부트 스트랩의 로그인 페이지를 조금 수정해보았다.
여기서 중요한 것은 <form> 태그의 action URL과 method 방식, 그리고 아이디와 패스워드를 입력하는 <input> 태그의 name이다.(id가 아닌 것을 유의하자!)
로그인 체크 로직을 간단하게 구현하기 위해 jstl 태그를 이용했다.
3-3. 로그인 컨트롤러에 로그인 페이지와 매핑될 메서드를 만들어준다.
LoginController.java
package com.company.login;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping(value="/login/loginPage.do")
public String loginPage() {
return "/login/loginPage";
}
@RequestMapping(value="/login/accessDenied.do")
public String accessDeniedPage() throws Exception {
return "/login/accessDenied";
}
}
3-4. 스프링 시큐리티 설정 파일을 수정한다.
security-context.xml
<http auto-config='true' use-expressions="true">
<intercept-url pattern="/resources/**" access="permitAll"/>
<intercept-url pattern="/login/**" access="permitAll" />
<intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
<form-login login-page="/login/loginPage.do"
login-processing-url="/login.do"
authentication-failure-url="/login/loginPage.do?err=true"
default-target-url="/"
username-parameter="userid"
password-parameter="userpw" />
<session-management>
<concurrency-control max-sessions="1" expired-url="/" />
</session-management>
<access-denied-handler error-page="/login/accessDenied.do" />
</http>
<http></http> : access 속성으로 ROLE_USER 가 변경된 것을 확인할 수 있다. access 속성에는 몇 가지 표현식이 존재한다. 이런 표현식을 사용하기 위해 <http>태그의 use-expressions 속성을 true로 줘야한다.
*SpEL 문법
표현식 | 의미 |
hasRole('ROLE') | ROLE 권한을 가지고 있는 경우 |
hasAnyRole('ROLE1', 'ROLE2') | ROLE1, ROLE2 권한 중 하나라도 가지고 있는 경우 |
permitAll | 모두 접근 가능 |
denyAll | 모두 접근 불가능 |
inAnonymouse() | Anonymous 사용자일 경우 |
isRemeberMe() | Remember-me 기능으로 로그인한 사용자일 경우 |
isAuthenticated() | Anonymous 사용자가 아닌 경우 |
isFullyAuthenticated() | Anonymous 사용자가 아니고 Remeber-me 기능으로 로그인하지 않은 경우 |
<intercept-url /> : “/resouces/**”와 "/login/**" 경로는 접근 제한을 풀어주었다. resoruces에 있는 이미지 파일을 사용해야 하고, 로그인을 할 수 있도록 화면을 보여줘야 하기 때문이다.
<intercept-url> 태그를 사용할 때는 반드시 순서에 유의해야 한다. 가령 pattern="/**" 이 가장 맨 앞에 와버리면 모든 url이 "/**" 를 만족하기 때문에 누구나 접근이 가능해진다.
<form-login /> : 로그인 페이지를 커스텀할 수 있는 태그이다.
-login-page 속성에 앞서 만든 로그인 페이지의 url을 입력한다.
-login-processing-url 속성은 loginPage.jsp를 만들 때, <form>태그의 action URL이다. 즉, login을 처리하는 부분이 따로 필요하지 않다.
-authentication-failure-url 속성은 로그인을 실패했을 때(아이디와 패스워드가 틀린 경우) 이동할 url이다. 본 예제에서는 로그인 페이지에 err파라미터를 true로 보냈다. 그리고 loginPage.jsp에서 jstl로 예외 처리하였습니다.
-default-target-url 속성은 로그인이 성공했을 때 이동하는 주소이다.
-username-parameter와 password-parameter는 로그인시 파라미터로 전달받을 이름을 정의한다. loginPage.jsp에서 정의한 로그인 아이디와 비밀번호를 입력받는 <input> 태그의 name과 일치해야 한다.
<session-management> : 세션 수를 설정하여 중복 로그인을 방지할 수 있다. 중복 로그인이 발생하는 경우 expired-url로 이동한다.
4. 테스트
로그인이 정상적으로 된다면 왼쪽 화면이, 권한이 없다면 로그인 체크 로직에 의해 오른쪽 화면이 보이게 된다.
++++++++++
간혹 favicon.ico로 리다이렉트 되는 이슈가 발생할 수 있습니다. 구글링 결과, 문제 원인을 대략 알 것 같았습니다.
일반적으로 우리가 웹을 사용할 때, 홈페이지 왼쪽에 보면 아이콘이 있는 것을 확인할 수 있습니다. 이것은 “웹/favicon.ico”에서 가져오는데 아마도 그 과정에서 권한으로 인한 문제가 생기는 것 같았습니다.
<http> 태그안에 <intercept-url pattern="/favicon.ico" access="hasRole('ROLE_ANONYMOUS')" /> 를 추가하여 favicon.ico에 대한 권한을 풀어주면 해결됩니다.
+ 피드백은 언제나 환영입니다 :)
'Framework > Spring' 카테고리의 다른 글
[Error]net::ERR_ABORTED 404 (0) | 2020.03.05 |
---|---|
[Spring Framework]Spring Security 적용하기 - 4) DB 연동, MyBatis 연결 (4) | 2019.10.28 |
[Spring Framework]Spring Security 적용하기 - 2) 스프링 시큐리티 적용 (1) | 2019.10.23 |
[Spring Framework]Spring Security 적용하기 - 1) 스프링 프레임워크 설정 (0) | 2019.10.23 |
싱글톤 패턴(Singleton Pattern) (0) | 2019.02.24 |
소중한 공감 감사합니다.