【Kotlin Spring Boot 服务端开发: 问题集锦】 Spring Security : 自定义AccessDeniedHandler 处理 Ajax 请求
AccessDeniedHandler 接口定义:
package org.springframework.security.web.access;
import org.springframework.security.access.AccessDeniedException;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Used by {@link ExceptionTranslationFilter} to handle an
* <code>AccessDeniedException</code>.
*
* @author Ben Alex
*/
public interface AccessDeniedHandler {
// ~ Methods
// ========================================================================================================
/**
* Handles an access denied failure.
*
* @param request that resulted in an <code>AccessDeniedException</code>
* @param response so that the user agent can be advised of the failure
* @param accessDeniedException that caused the invocation
*
* @throws IOException in the event of an IOException
* @throws ServletException in the event of a ServletException
*/
void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException;
}
Spring 的默认实现是 AccessDeniedHandlerImpl :
package org.springframework.security.web.access;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.WebAttributes;
/**
* Base implementation of {@link AccessDeniedHandler}.
* <p>
* This implementation sends a 403 (SC_FORBIDDEN) HTTP error code. In addition, if an
* {@link #errorPage} is defined, the implementation will perform a request dispatcher
* "forward" to the specified error page view. Being a "forward", the
* <code>SecurityContextHolder</code> will remain populated. This is of benefit if the
* view (or a tag library or macro) wishes to access the
* <code>SecurityContextHolder</code>. The request scope will also be populated with the
* exception itself, available from the key {@link WebAttributes#ACCESS_DENIED_403}.
*
* @author Ben Alex
*/
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
// ~ Static fields/initializers
// =====================================================================================
protected static final Log logger = LogFactory.getLog(AccessDeniedHandlerImpl.class);
// ~ Instance fields
// ================================================================================================
private String errorPage;
// ~ Methods
// ========================================================================================================
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
if (errorPage != null) {
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
// Set the 403 status code.
response.setStatus(HttpStatus.FORBIDDEN.value());
// forward to error page.
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else {
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
}
/**
* The error page to use. Must begin with a "/" and is interpreted relative to the
* current context root.
*
* @param errorPage the dispatcher path to display
*
* @throws IllegalArgumentException if the argument doesn't comply with the above
* limitations
*/
public void setErrorPage(String errorPage) {
if ((errorPage != null) && !errorPage.startsWith("/")) {
throw new IllegalArgumentException("errorPage must begin with '/'");
}
this.errorPage = errorPage;
}
}
自定义实现 MyAccessDeniedHandler
package com.ksb.ksb_with_security.handler
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.WebAttributes
import org.springframework.security.web.access.AccessDeniedHandler
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class MyAccessDeniedHandler : AccessDeniedHandler {
val logger = LoggerFactory.getLogger(MyAccessDeniedHandler::class.java)
var errorPage: String? = null
constructor(errorPage: String?) {
this.errorPage = errorPage
}
override fun handle(request: HttpServletRequest, response: HttpServletResponse, accessDeniedException: AccessDeniedException) {
val isAjax = ControllerTools.isAjaxRequest(request)
if (!response.isCommitted) {
if (isAjax) {
val msg = accessDeniedException.message
logger.info("accessDeniedException.message = $msg")
val accessDenyMsg = """
{
"code":"403",
"msg": "没有权限"
}
""".trimIndent()
ControllerTools.print(response, accessDenyMsg)
} else if (errorPage != null) {
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException)
// Set the 403 status code.
response.status = HttpStatus.FORBIDDEN.value()
// forward to error page.
val dispatcher = request.getRequestDispatcher(errorPage)
dispatcher.forward(request, response)
}
}
}
}
object ControllerTools {
fun isAjaxRequest(request: HttpServletRequest): Boolean {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"), true)
}
fun print(response: HttpServletResponse, msg: String) {
response.characterEncoding = "UTF-8"
response.contentType = "application/json; charset=utf-8"
val out = response.writer
out.append(msg)
out.flush()
}
}
然后,在自定义的继承 WebSecurityConfigurerAdapter的配置类WebSecurityConfig 中这样使用上面自定义的MyAccessDeniedHandler
package com.ksb.ksb_with_security.security
import com.ksb.ksb_with_security.handler.MyAccessDeniedHandler
import com.ksb.ksb_with_security.service.MyUserDetailService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.access.AccessDeniedHandler
/**
prePostEnabled :决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 决定是否Spring Security的保障注解 [@Secured] 是否可用
jsr250Enabled :决定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
*/
@Configuration
@EnableWebSecurity
// 开启 Spring Security 方法级安全
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
class WebSecurityConfig : WebSecurityConfigurerAdapter() {
@Bean
fun myAccessDeniedHandler(): AccessDeniedHandler {
return MyAccessDeniedHandler("/403")
}
@Bean
override fun userDetailsService(): UserDetailsService {
return MyUserDetailService()
}
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.csrf().disable()
http.authorizeRequests()
.antMatchers("/", // 首页不拦截
"/css/**",
"/fonts/**",
"/js/**",
"/images/**" // 不拦截静态资源
).permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")// url 请求路径,对应 LoginController 里面的 @GetMapping("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/main").permitAll()
.and()
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler())
// .exceptionHandling().accessDeniedPage("/403")
.and()
.logout().permitAll()
http.logout().logoutSuccessUrl("/")
}
@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
//AuthenticationManager 使用我们的 lightSwordUserDetailService 来获取用户信息
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder())
}
/**
* 密码加密算法
*
* @return
*/
@Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder();
}
}