Spring Security 를 이용해서 셈플링을 해보면 인증 및 허가에 대한 Exception 이 발생시 LoginForm으로 Redirect 하게 된다.
하지만 나는 그렇게 하고 싶지 않다!!!
일반적인 경우에는 별도의 페이지로 분기하고 싶고, AJAX나 외부 통신시에는 해당 메시지를 XML이나 JSON으로 리턴해주고 싶다!!!
방법을 고민해보다가.. 책도 찾아보고, 인터넷도 뒤져보고.. 방법을 찾아 적어놔본다...
위 그림에서와 같이 ExceptionTranslationFilter 에서 인증 및 권한에 대한 검사를 한다고 한다.
인증이 되지 않았을 경우(비로그인)에는 AuthenticationEntryPoint 부분에서 AuthenticationException 을 발생 시키고, 인증(로그인)은 되었으나 해당 요청에 대한 권한이 없을 경우에는 AccessDeniedHandler 부분에서 AccessDeniedException 이 발생된다.
그렇다면 AuthenticationEntryPoint 부분과 AccessDeniedHandler 부분을 재정의해서 처리해주면 될듯하다.
나의 경우 재정의 한부분에서 특정 Controller 로 Forward 처리 했다.
호출 케이스별로 jsp View, xml, json 으로 출력해줘야 하는데, 재정의 한부분에서 출력해주는 것보다 특정 Controller Method로 하게 되면 MVC 공통으로 처리한것으로 인해 알아서 케이스별로 출력될것이기에...
[build.gradle]
apply plugin: 'war'
apply plugin: 'eclipse-wtp'
apply plugin: 'eclipse'
// JAVA Version 1.6
sourceCompatibility = '1.6'
targetCompatibility = '1.6'
eclipse {
wtp {
component {
contextPath = "/"
}
facet {
facet name: 'jst.web', version: '2.5'
facet name: 'jst.java', version: '1.6'
}
}
}
// 의존성 설정에 사용할 프로퍼티
springVersion = '3.2.0.RELEASE'
springSecurityVersion = '3.1.3.RELEASE'
slf4jVersion = '1.7.5'
logbackVersion = '1.0.13'
// 메이븐 Central 저장소 사용
repositories {
mavenCentral()
}
List loggers = [
"ch.qos.logback:logback-classic:1.0.13",
"org.slf4j:jcl-over-slf4j:1.7.5",
]
// 의존성 설정
dependencies {
compile "org.springframework:spring-webmvc:$springVersion"
compile "org.springframework:spring-oxm:$springVersion"
compile "org.springframework.security:spring-security-web:$springSecurityVersion"
compile "org.springframework.security:spring-security-taglibs:$springSecurityVersion"
compile "org.springframework.security:spring-security-config:$springSecurityVersion"
compile "cglib:cglib-nodep:2.2.2"
compile "javax.servlet:jstl:1.2"
compile "org.codehaus.jackson:jackson-mapper-asl:1.9.13"
compile "net.sf.json-lib:json-lib:2.2.1:jdk15"
compile "net.sf.json-lib:json-lib-ext-spring:1.0.2"
compile "xom:xom:1.2.5"
compile "commons-lang:commons-lang:2.6"
compile loggers
testCompile "org.springframework:spring-test:$springVersion"
testCompile 'junit:junit:4.8.2'
testCompile 'org.mockito:mockito-core:1.9.0'
}
// commons-logging, log4j, jul 의존성 제거
configurations {
all.collect { configuration ->
configuration.exclude group: 'commons-logging', module: 'commons-logging'
configuration.exclude group: 'log4j', module: 'log4j'
}
}
[application-context-security.xml]
<?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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<security:http auto-config="true" use-expressions="true" entry-point-ref="authenticationEntryPoint">
<!-- <security:access-denied-handler error-page="/accessIsDenied" /> -->
<security:access-denied-handler ref="acessDeniedHandler" />
<security:intercept-url pattern="/login" access="permitAll" />
<security:intercept-url pattern="/accessIsDenied*" access="permitAll" />
<security:intercept-url pattern="/isNotLogin*" access="permitAll" />
<security:intercept-url pattern="/admin*" access="hasRole('admin')" />
<security:intercept-url pattern="/**" access="hasRole('user')" />
<security:form-login login-page="/login" default-target-url="/" />
<security:logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" />
</security:http>
<!-- 권한 없는 요청일 경우 -->
<bean id="acessDeniedHandler" class="pe.kr.ddakker.framework.security.web.access.AccessDeniedHandlerImpl">
<property name="redirect" value="false" />
<property name="errorPage" value="/assessDenied.do" />
</bean>
<!-- 인증전 일 경우 -->
<bean id="authenticationEntryPoint" class="pe.kr.ddakker.framework.security.web.AuthenticationEntryPointImpl">
<property name="redirect" value="false" />
<property name="errorPage" value="/isNotLogin.do" />
</bean>
<bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>
<security:authentication-manager>
<security:authentication-provider>
<security:password-encoder ref="encoder"/>
<security:user-service>
<!-- Password: "koala" for both -->
<security:user name="user"
password="4efe081594ce25ee4efd9f7067f7f678a347bccf2de201f3adf2a3eb544850b465b4e51cdc3fcdde"
authorities="user"/>
<security:user name="admin"
password="4efe081594ce25ee4efd9f7067f7f678a347bccf2de201f3adf2a3eb544850b465b4e51cdc3fcdde"
authorities="admin"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
[AccessDeniedHandlerImpl.java]
package pe.kr.ddakker.framework.security.web.access;
import static pe.kr.ddakker.framework.web.servlet.view.ServletHelper.convertorViewTypeErrorPage;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
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;
/**
* 권한이 없는 요청시 발생
* @auther ddakker 2013. 12. 6.
*/
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
private Boolean redirect = true;
public Boolean getRedirect() {
return redirect;
}
public void setRedirect(Boolean redirect) {
this.redirect = redirect;
}
private String errorPage;
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
public String getErrorPage() {
return errorPage;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 에러 페이지에 대한 확장자를 현재 호출한 확장자와 마추어준다.
String goErrorPage = convertorViewTypeErrorPage(request, errorPage);
if( redirect ){
response.sendRedirect(goErrorPage);
}else{
RequestDispatcher dispatcher = request.getRequestDispatcher(goErrorPage);
dispatcher.forward(request, response);
}
}
}
[AuthenticationEntryPointImpl.java]
package pe.kr.ddakker.framework.security.web;
import static pe.kr.ddakker.framework.web.servlet.view.ServletHelper.convertorViewTypeErrorPage;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* 인증하지 않은 상황에서 호출시 발생
* @auther ddakker 2013. 12. 6.
*/
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
private Boolean redirect = true;
public Boolean getRedirect() {
return redirect;
}
public void setRedirect(Boolean redirect) {
this.redirect = redirect;
}
private String errorPage;
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
public String getErrorPage() {
return errorPage;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 에러 페이지에 대한 확장자를 현재 호출한 확장자와 마추어준다.
String goErrorPage = convertorViewTypeErrorPage(request, errorPage);
if( redirect ){
response.sendRedirect(goErrorPage);
}else{
RequestDispatcher dispatcher = request.getRequestDispatcher(goErrorPage);
dispatcher.forward(request, response);
}
}
}
[servlet-context.xml]
<?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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven />
<context:component-scan base-package="pe.kr.ddakker" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>
<mvc:interceptors>
<bean class="pe.kr.ddakker.dakmoney.web.servlet.interceptor.TestInterceptor"/>
</mvc:interceptors>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="contentNegotiationManager">
<bean class="org.springframework.web.accept.ContentNegotiationManager">
<constructor-arg>
<bean
class="org.springframework.web.accept.PathExtensionContentNegotiationStrategy">
<constructor-arg>
<map>
<entry key="xml" value="application/xml" />
<entry key="json" value="application/json" />
<entry key="jsonp" value="application/javascript" />
</map>
</constructor-arg>
</bean>
</constructor-arg>
</bean>
</property>
<property name="defaultViews">
<list>
<bean class="pe.kr.ddakker.framework.web.servlet.view.xml.MappingXmlView">
<property name="contentType" value="application/xml"/>
</bean>
<bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
<property name="contentType" value="application/json"/>
</bean>
<bean class="pe.kr.ddakker.framework.web.servlet.view.json.MappingJsonpView">
<property name="contentType" value="application/javascript"/>
</bean>
</list>
</property>
<property name="ignoreAcceptHeader" value="false" />
</bean>
</beans>
[HomeController.java]
package pe.kr.ddakker.dakmoney;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import pe.kr.ddakker.framework.web.servlet.exception.SessionNotException;
@Controller
public class HomeController {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Simply selects the home view to render by returning its name.
*/
@RequestMapping(value = "/")
public String index(Locale locale, Model model) {
logger.info("Welcome home! The client locale is {}.", locale);
HomeBean helloBean = new HomeBean();
helloBean.setNm("이름 index");
helloBean.setAge(22);
model.addAttribute("helloBean", helloBean);
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
String formattedDate = dateFormat.format(date);
model.addAttribute("serverTime", formattedDate + " Hello 안녕 하세요" );
return "home";
}
/**
* Simply selects the home view to render by returning its name.
* @throws Exception
*/
@RequestMapping(value = "/json", method = RequestMethod.GET)
public String index2(Locale locale, Model model) throws Exception {
logger.info("Welcome home! The client locale is {}.", locale);
HomeBean helloBean = new HomeBean();
helloBean.setNm("이름 index2");
helloBean.setAge(22);
model.addAttribute("helloBean", helloBean);
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
String formattedDate = dateFormat.format(date);
model.addAttribute("serverTime", formattedDate + " Hello 안녕 하세요" );
return "home";
}
/**
* Simply selects the home view to render by returning its name.
* @throws Exception
*/
@RequestMapping(value = "/json2", method = RequestMethod.GET)
public String index22(Locale locale, Model model) throws Exception {
logger.info("Welcome home! The client locale is {}.", locale);
HomeBean helloBean = new HomeBean();
helloBean.setNm("이름 index22");
helloBean.setAge(22);
model.addAttribute("helloBean", helloBean);
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
String formattedDate = dateFormat.format(date);
if( true ){
throw new RuntimeException("와우");
}
model.addAttribute("serverTime", formattedDate + " Hello 안녕 하세요" );
return "home";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(Locale locale, Model model) throws Exception {
logger.debug("loginForm");
return "loginForm";
}
@RequestMapping(value = "/isNotLogin", method = RequestMethod.GET)
public void isNotLogin(Model model, @RequestParam Map map) throws Exception {
logger.debug("---------- isNotLogin()");
logger.debug("---------- isNotLogin() params: " + map);
if( true ) throw new SessionNotException("로그인 되지 않았습니다.");
}
@RequestMapping(value = "/assessDenied", method = RequestMethod.GET)
public String assessDenied(Locale locale, Model model) throws Exception {
logger.debug("---------- assessDenied()");
if( true ) throw new SessionNotException("권한이 없는 요청입니다.");
return "assessDenied";
}
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String admin(Locale locale, Model model) throws Exception {
logger.debug("---------- admin()");
return "admin";
}
}
[DefaultControllerAdvice.java]
package pe.kr.ddakker.dakmoney;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import pe.kr.ddakker.framework.web.servlet.exception.SessionNotException;
@ControllerAdvice
public class DefaultControllerAdvice {
private final Logger logger = LoggerFactory.getLogger(DefaultControllerAdvice.class);
@ExceptionHandler(Exception.class)
public ModelAndView handleException(HttpServletRequest request, Exception ex) {
logger.error("에러임 Exception: {}", ex.toString());
printExceptionInfo(request, ex);
ModelAndView mv = new ModelAndView("error");
mv.addObject("result", "9999");
mv.addObject("message", ex.getMessage());
return mv;
}
@ExceptionHandler(RuntimeException.class)
public ModelAndView handleRuntimeException(HttpServletRequest request, RuntimeException ex) {
logger.error("에러임 RuntimeException: {}", ex.toString());
printExceptionInfo(request, ex);
ModelAndView mv = new ModelAndView("error");
mv.addObject("result", "8888");
mv.addObject("message", ex.getMessage());
return mv;
}
@ExceptionHandler(SessionNotException.class)
public ModelAndView handleSessionNotException(HttpServletRequest request, SessionNotException ex) {
logger.error("에러임 SessionNotException: {}", ex.toString());
printExceptionInfo(request, ex);
ModelAndView mv = new ModelAndView("error");
mv.addObject("result", "7777");
mv.addObject("message", ex.getMessage());
return mv;
}
private void printExceptionInfo(HttpServletRequest request, Exception ex){
logger.error("PARMAS: {}", request.getParameterMap());
for( StackTraceElement s : ex.getStackTrace() ){
logger.error("STACK: {}", s);
}
}
}