LoginController
@Controller
public class LoginController {
@RequestMapping(value = "/loginPage", method = RequestMethod.GET)
public String login() {
return "login_page";
}
}
login_page.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SPRING</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
</head>
<body>
<h1>로그인</h1>
<hr>
<form action="<c:url value="/loginPage"/>" method="post" id="loginForm">
<table>
<tr>
<td>아이디</td>
<td><input type="text" name="userid" id="userid"></td>
</tr>
<tr>
<td>비밀번호</td>
<td><input type="password" name="passwd" id="passwd"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">로그인</button></td>
</tr>
</table>
<%-- CSRF 공격을 방어하기 위해 Spring Security에 의해 발급된 CSRF Token을 hidden 타입으로 전달 --%>
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
</form>
</body>
</html>
security-context.xml
...
<http auto-config="true" use-expressions="true">
...
...
<form-login login-page="/loginPage" login-processing-url="/loginPage"
username-parameter="userid" password-parameter="passwd" default-target-url="/"/>
...
- form-login : form 방식의 로그인 페이를 설정하기 위한 엘리먼트
- 속성을 사용하지 않은 경우 Spring Security의 로그인 페이지를 제공한다.
- login-page : 로그인 페이지를 요청하는 URL 주소를 속성값으로 설정한다.
- login-processing-url : 아이디와 비밀번호를 전달받아 로그인 처리하는 페이지의 URL 주소 설정한다.
- username-parameter : 아이디를 전달하기 위한 이름을 속성값으로 설정한다.
- default-target-url : 로그인 성공시 호출된 페이지의 URL 주소를 속성값으로 설정한다.
home.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- Security 태그 라이브러리를 JSP 문서에 포함 - Spring Security 태그 사용 가능 -->
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SPRING</title>
</head>
<body>
<h1>메인페이지</h1>
<hr>
<h3><a href="<c:url value="/guest/page"/>">Guest</a></h3>
<h3><a href="<c:url value="/user/page"/>">User</a></h3>
<h3><a href="<c:url value="/manager/page"/>">Manager</a></h3>
<h3><a href="<c:url value="/admin/page"/>">Administrator</a></h3>
<hr>
<!-- authorize : 권한을 비교하여 비교하여 태그의 포함 여부를 설정하기 위한 태그 -->
<!-- access 속성 : 권한(Role)을 속성값으로 설정 - SpEL 사용 가능 -->
<!-- 인증을 받지 않은 사용자인 경우 태그 포함되도록 설정 -->
<sec:authorize access="isAnonymous()">
<h3><a href="<c:url value="/login"/>">로그인</a></h3>
</sec:authorize>
</body>
</html>
CSRF 공격을 방어하기 위해 Spring Security에 의해 발급된 CSRF Token을 hidden 타입으로 전달해야 한다.
- 서버에 전달된 요청이 실제 서버에서 허용된 요청이 맞는지를 확인하기 위해 CSRF Token을 발행하는 것이다.
- 서버에서는 뷰페이지를 생성할 때마다 랜덤으로 토큰을 발행하여 세션에 저장하고 사용자가 서버에 페이지를 요청할 때 Hidden 타입으로 토큰을 서버에 전달하여 세션의 저장된 토큰과 비교히여 사용자를 확인한다.
- 일치 여부를 확인한 토큰은 삭제하고 새로운 뷰에 대한 토큰을 다시 발행한다.
* CSRF(Cross-Site Request Forgery) 공격 : 사이트간 요청을 위조하는 공격
login_page.jsp
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
대신 crsfInput 태그를 사용해도 된다.
<sec:csrfInput/>
* csrfInput : CSRF Token을 hidden 타입으로 서버에 전달하기 위한 태그
에러메시지 출력하기
login_page.jsp
...
<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
<h3 style="color: red">아이디 또는 비밀번호가 맞지 않습니다.</h3>
<c:remove var="SPRING_SECURITY_LAST_EXCEPTION"/>
</c:if>
...
SPRING_SECURITY_LAST_EXCEPTION : Spring Security에 의해 마지막에 발생된 예외가 세션의 속성값으로 저장된 세션의 속성명

Srping Security의 메세지를 가지고 오는것도 가능하다.

security_message.properties
AbstractUserDetailsAuthenticationProvider.badCredentials = \uC544\uC774\uB514 \uB610\uB294 \uBE44\uBC00\uBC88\uD638\uAC00 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD655\uC778\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.credentialsExpired = \uBE44\uBC00\uBC88\uD638\uC758 \uC720\uD6A8\uAE30\uAC04\uC774 \uB9CC\uB8CC \uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.disabled = \uACC4\uC815\uC774 \uBE44\uD65C\uC131\uD654 \uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.expired = \uACC4\uC815\uC774 \uB9CC\uB8CC \uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
AbstractUserDetailsAuthenticationProvider.locked = \uACC4\uC815\uC774 \uC7A0\uACA8 \uC788\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD574 \uC8FC\uC138\uC694.
root-context.xml
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="messageSource">
<property name="basenames">
<list>
<value>/WEB-INF/message/security_message</value>
</list>
</property>
<property name="cacheSeconds" value="60"/>
<property name="defaultEncoding" value="utf-8"/>
</bean>
login_page.jsp
<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
<h3 style="color: red;">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message }</h3>
<!-- 예외가 저장된 세션의 속성값 삭제 -->
<c:remove var="SPRING_SECURITY_LAST_EXCEPTION"/>
</c:if>
AccessDeniedException이 발생된 경우 access-denied-handler 엘리먼트를 이용해 403 에러코드 대신 응답 결과를 제공할 수 있다.
security-context.xml
<access-denied-handler error-page="/accessDenied"/>
LoginController
@RequestMapping(value = "/accessDenied", method = RequestMethod.GET)
public String accessDenied() {
return "access_denied";
}
access_denied.jsp
<body>
<h1>에러페이지</h1>
<hr>
<h3 style="color: red;">권한이 없어 페이지에 접근 불가능합니다. 관리자에게 문의해 주세요.</h3>
</body>
커스터마이징
단순한 페이지 이동 뿐만 아니라, 접근 제한에 대한 다양한 처리를 위해서 AccessDeniedHandler 인터페이스를 상속받은 자식클래스를 이용할 수 있다.
//접근이 제한된 페이지를 요청한 경우 실행될 기능을 제공하기 위한 클래스
// => AccessDeniedHandler 인터페이스를 상속 받아 작성
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
//접근 제한에 대한 명령 실행 - 계정 잠금 기능 활성화 등의 명령 작성
response.sendRedirect(request.getContextPath()+"/accessDenied");
}
}
Spring Security 관련 클래스를 Spring Bean으로 등록해준다.
security-context.xml
<beans:bean class="xyz.itwill.security.CustomAccessDeniedHandler" id="customAccessDeniedHandler"/>
ref 속성에는 AccessDeniedHandler 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자(beanName)를 속성값으로 설정해준다.
<access-denied-handler ref="customAccessDeniedHandler"/>
Spring Security는 인증 성공 후 기본적으로 SaveRequestAwareAuthenticationSuccessHandler 클래스를 이용하여 사용자가 원래 요청한 페이지의 정보를 유지하여 요청 페이지로 이동되도록 처리한다.
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {}
내가 원하는대로 커스터마이징한 핸들러 클래스를 작성하기 위해서는 AuthenticationSuccessHandler 인터페이스를 상속받아서 작성하면 된다.
로그인이 성공했을 때 동작하는 핸들러 클래스 만들기
로그인 계정의 권한을 확인하여 특정 페이지를 무조건 요청되도록 설정해보자.
CustomLoginSuccessHandler
//로그인 성공 후에 실행될 기능을 제공하기 위한 클래스
// => AuthenticationSuccessHandler 인터페이스를 상속받아 작성
// => 사용자의 마지막 로그인 날짜를 변경 처리하는 기능 구현
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
//로그인 계정의 권한을 확인하여 특정 페이지를 무조건 요청되도록 설정
//Authentication 객체 : 인증 및 인가(권한)와 관련된 모든 정보를 저장하기 위한 객체
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//로그인 사용자의 권한이 저장되기 위한 List 객체 생성
List<String> roleNames=new ArrayList<String>();
//Authentication.getAuthorities() : 인증된 계정된 모든 권한(GrantedAuthority 객체)을
//List 객체로 반환하는 메소드
//GrantedAuthority 객체 : 사용자에게 부여된 권한에 대한 정보를 저장한 객체
for(GrantedAuthority authority : authentication.getAuthorities()) {
//GrantedAuthority.getAuthority() : GrantedAuthority 객체에 저장된 권한을 반환하는 메소드
roleNames.add(authority.getAuthority());
}
//Collection<T>.contains(List 객체에 저장된 요소를 비교
if(roleNames.contains("ROLE_ADMIN")) {
response.sendRedirect(request.getContextPath()+"/admin/page");
return;
}
if(roleNames.contains("ROLE_MANAGER")) {
response.sendRedirect(request.getContextPath()+"/manager/page");
return;
}
if(roleNames.contains("ROLE_USER")) {
response.sendRedirect(request.getContextPath()+"/user/page");
return;
}
response.sendRedirect(request.getContextPath()+"/guest/page");
}
}
security-context.xml
authentication-success-handler-ref 속성
: AuthenticationSuccessHandler 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자(beanName)를 속성값으로 설정
<form-login login-page="/loginPage" login-processing-url="/loginPage"
username-parameter="userid" password-parameter="passwd" default-target-url="/"
authentication-success-handler-ref="customLoginSuccesshandler"
/>
...
<!-- Spring Security 관련 클래스를 Spring Bean으로 등록 -->
<beans:bean class="xyz.itwill.security.CustomLoginSuccessHandler" id="customLoginSuccesshandler"/>
CustomLoginFailuerHandler
//로그인 성공 후에 실행될 기능을 제공하기 위한 클래스
//=> 로그인 실패 횟수 누적 등의 기능을 구현
public class CustomLoginFailuerHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
request.getSession().setAttribute("userid", request.getParameter("userid"));
setDefaultFailureUrl("/loginPage");//로그인 실패시 로그인 페이지로
super.onAuthenticationFailure(request, response, exception);
}
}
security-context.xml
authentication-failure-handler-ref 속성 : AuthenticationFailuerHandler 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자(beanName)를 속성값으로 설정
<form-login login-page="/loginPage" login-processing-url="/loginPage"
username-parameter="userid" password-parameter="passwd" default-target-url="/"
...
authentication-failure-handler-ref="customLoginFailuerHandler"
/>
...
<beans:bean class="xyz.itwill.security.CustomLoginFailuerHandler" id="customLoginFailuerHandler"/>
로그아웃 구현
home.jsp
로그아웃할 때는 무조건 form 태그를 이용해서 post 방식으로 토큰을 전달해주어야 한다.
=> CSRF 토큰을 전달하여 처리되도록 설정해야 하기 때문
<!-- 인증된 사용자인 경우 태그가 포함되도록 설정 -->
<sec:authorize access="isAuthenticated">
<!-- 로그아웃 처리 기능을 제공하는 페이지는 반드시 form 태그를 사용하여 요청해야 한다.
=> CSRF 토큰을 전달하여 처리되도록 설정해야 하기 때문 -->
<form action="<c:url value="/logout"/>" method="post">
<sec:csrfInput/>
<button type="submit">로그아웃</button>
</form>
</sec:authorize>
security-context.xml
<logout logout-url="/logout" logout-success-url="/" invalidate-session="true" delete-cookies="JSESSIONID"/>
logout : 로그아웃 처리 기능을 제공하는 엘리먼트
- logout-url 속성 : 로그아웃 처리를 위해 요청하기 위한 URL 주소를 속성값으로 설정
- logout-success-url 속성 : 로그아웃 처리 후 요청할 페이지의 URL 주소를 속성값으로 설정
- invalidate-session 속성 : 세션을 무효화 처리하기 위한 논리값(false 또는 true)을 속성값으로 설정
- delete-cookies 속성 : 삭제할 쿠키에 대한 이름을 속성값으로 설정
authentication 태그 : 인증된 사용자에 대한 정보를 제공하기 위한 태그
=> 인증된 사용자에게만 필요한 정보를 제공 가능
property 속성 : 인증 사용자(UserDetails 객체)에 정보를 제공받기 위한 속성명(필드명)을 속성값으로 설정
- principal 속성값 : UserDetails 인터페이스를 상속받은 User 객체가 저장된 속성
- User 객체의 username 필드 : 사용자의 식별자(아이디)가 저장된 필드
- User 객체의 accountNonExpired 필드 : 사용자의 유효날짜 관련 논리값이 저장된 필드
- User 객체의 AccountNonLocked 필드 : 사용자 잠금 관련 논리값이 저장된 필드
- User 객체의 grantedAuthorities 필드 : 사용자의 권한 정보를 요소로 가진 List 객체가 저장된 필드
- User 객체의 credentialsNonExpired 필드 : 비밀번호의 유효날짜 관련 논리값이 저장된 필드
- User 객체의 enabled 필드 : 사용자의 활성 관련 논리값이 저장된 필드
home.jsp
<sec:authorize access="isAuthenticated()">
<h3><sec:authentication property="principal.username"/></h3>
</sec:authorize>

'학원 > 복기' 카테고리의 다른 글
[Spring Security] rememberme 예제 (0) | 2023.09.11 |
---|---|
[Spring] Spring Security (users테이블, security_users 테이블 이용한 인증) (0) | 2023.09.11 |
[Spring] Spring Security (0) | 2023.09.06 |
[Spring] 스프링 검증(Spring Validation) (0) | 2023.09.04 |
[Spring] @RestController - 수정 (0) | 2023.08.19 |