54

springboot + shiro 权限注解、统一异常处理、请求乱码解决-架构的路上

 5 years ago
source link: http://blog.51cto.com/wyait/2125708
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

springboot + shiro 权限注解、统一异常处理、请求乱码解决

后台权限管理系统

20200808新版本更新

版本升级及内容优化版本,改动内容:

  1. 版本更新,springboot从1.5+升级到2.1+;
  2. 权限缓存使用redis;
  3. 验证码使用redis;
  4. 权限验证完善。

基于前篇,新增功能:

  1. 新增shiro权限注解;
  2. 请求乱码问题解决;
  3. 统一异常处理。

源码已集成到项目中:

github源码: https://github.com/wyait/manage.git
码云:https://gitee.com/wyait/manage.git
github对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0

shiro注解的使用

shiro权限注解

Shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用AOP 的功能来进行判断,如Spring AOP;Shiro 提供了Spring AOP 集成用于权限注解的解析和验证。

    @RequiresAuthentication
  表示当前Subject已经通过login 进行了身份验证;即Subject.isAuthenticated()返回true。

  @RequiresUser
  表示当前Subject已经身份验证或者通过记住我登录的。

  @RequiresGuest
  表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。

  @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
  @RequiresRoles(value={“admin”})
  @RequiresRoles({“admin“})
  表示当前Subject需要角色admin 和user。

  @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
  表示当前Subject需要权限user:a或user:b。

Shiro的认证注解处理是有内定的处理顺序的,如果有多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):

RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest

以上注解既可以用在controller中,也可以用在service中使用;
建议将shiro注解放在controller中,因为如果service层使用了spring的事物注解,那么shiro注解将无效。

shiro权限注解springAOP配置

shiro权限注解要生效,必须配置springAOP通过设置shiro的SecurityManager进行权限验证。

/**
     * 
     * @描述:开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * </br>Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor(保证实现了Shiro内部lifecycle函数的bean执行) has run
     * </br>不使用注解的话,可以注释掉这两个配置
     * @创建人:wyait
     * @创建时间:2018年5月21日 下午6:07:56
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

springboot异常处理原理

场景:当用户正常访问网站时,因为某种原因后端出现exception的时候,直接暴露异常信息或页面显示给用户;

这种操作体验不是我们想要的。所以要对异常进行统一管理,能提高用户体验的同时,后台能详细定位到异常的问题点。

springboot异常概况

Spring Boot提供了默认的统一错误页面,这是Spring MVC没有提供的。在理解了Spring Boot提供的错误处理相关内容之后,我们可以方便的定义自己的错误返回的格式和内容。

编写by zero异常

在home页面,手动创建两个异常:普通异常和异步异常!

  • 前端页面:
<p>
    普通请求异常:
    <a href="/error/getError">点击</a>
</p>
<p>
    ajax异步请求异常:
    <a href="javascript:void(0)" onclick="ajaxError()">点击</a>
</p>
... 
//js代码
function ajaxError(){
    $.get("/error/ajaxError",function(data){
        layer.alert(data);
    });
}
  • 后端代码:
/**
 * 
 * @描述:普通请求异常
 * @创建人:wyait
 * @创建时间:2018年5月24日 下午5:30:50
 */
@RequestMapping("getError")
public void toError(){
    System.out.println(1/0);
}
/**
 * 
 * @描述:异步异常
 * @创建人:wyait
 * @创建时间:2018年5月24日 下午5:30:39
 */
@RequestMapping("ajaxError")
@ResponseBody
public String ajaxError(){
    System.out.println(1/0);
    return "异步请求成功!";
}
  • 普通异常:
    image
    console错误信息:
[2018-05-25 09:30:04.669][http-nio-8077-exec-8][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
    at com.wyait.manage.web.error.IndexErrorController.toError(IndexErrorController.java:18) ~[classes/:?]
    ...
    at java.lang.Thread.run(Thread.java:748) [?:1.8.0_131]
...
[2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)]
[2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)]
[2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
[2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
...
[2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][263]:Requested media types are [text/html, text/html;q=0.8] based on Accept header types and producible media types [text/html])
[2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][263]:Requested media types are [text/html, text/html;q=0.8] based on Accept header types and producible media types [text/html])
[2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'error'
[2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'error'
[2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][338]:Returning [org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView@6ffd99fb] based on requested media type 'text/html'
[2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][338]:Returning [org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView@6ffd99fb] based on requested media type 'text/html'
...

通过日志可知,springboot返回的错误页面,是通过:org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml处理返回ModelAndView。

  • 异步异常:
    image
    console日志信息:
[2018-05-25 09:31:19.958][http-nio-8077-exec-6][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
    at com.wyait.manage.web.error.IndexErrorController.ajaxError(IndexErrorController.java:29) ~[classes/:?]
    ...
    at java.lang.Thread.run(Thread.java:748) [?:1.8.0_131]
...
[2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
[2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
[2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
[2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
...
[2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor][234]:Written [{timestamp=Fri May 25 09:31:19 CST 2018, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, message=/ by zero, path=/error/ajaxError}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2729eae5]
[2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor][234]:Written [{timestamp=Fri May 25 09:31:19 CST 2018, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, message=/ by zero, path=/error/ajaxError}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2729eae5]
[2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.DispatcherServlet][1048]:Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
...

通过日志可知,springboot返回的错误信息,是通过:org.springframework.boot.autoconfigure.web.BasicErrorController.error处理返回ResponseEntity<String,Object>。

  • 异常都是通过org.springframework.boot.autoconfigure.web.BasicErrorController控制处理的。

springboot异常处理解析

查看org.springframework.boot.autoconfigure.web包下面的类,跟踪springboot对error异常处理机制。自动配置通过一个MVC error控制器处理错误
通过spring-boot-autoconfigure引入

查看springboot 处理error的类
image

springboot的自动配置,在web中处理error相关的自动配置类:ErrorMvcAutoConfiguration。查看与处理error相关的类:

  • ErrorMvcAutoConfiguration.class
  • ErrorAttibutes.class
  • ErrorController.class
  • ErrorProperties.class
  • ErrorViewResolver.class

ErrorAutoConfiguration类源码//TODO

ErrorAutoConfiguration注册的bean

//4个BEAN
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
}

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
            this.errorViewResolvers);
}

@Bean
public ErrorPageCustomizer errorPageCustomizer() {
    return new ErrorPageCustomizer(this.serverProperties);
}

@Bean
public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
    return new PreserveErrorControllerTargetClassPostProcessor();
}
  1. DefaultErrorAttributes类
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
        implements ErrorAttributes, HandlerExceptionResolver, Ordered {

        ...    
    }

ErrorAttributes:

public interface ErrorAttributes {

    Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
            boolean includeStackTrace);

    Throwable getError(RequestAttributes requestAttributes);

}

HandlerExceptionResolver:

public interface HandlerExceptionResolver {
    /**
     * Try to resolve the given exception that got thrown during handler execution,
     * returning a {@link ModelAndView} that represents a specific error page if appropriate.
     */
    ModelAndView resolveException(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}

DefaultErrorAttributes类:

  • 实现了ErrorAttributes接口,当处理/error错误页面时,可以在该bean中读取错误信息响应返回;
  • 实现了HandlerExceptionResolver接口。

debug跟踪源码:即DispatcherServlet在doDispatch过程中有异常抛出时:

一. 先由HandlerExceptionResolver.resolveException解析异常并保存在request中;
二. 再DefaultErrorAttributes.getErrorAttributes处理;DefaultErrorAttributes在处理过程中,从request中获取错误信息,将错误信息保存到RequestAttributes中;
三. 最后在获取错误信息getError(RequestAttributes)时,从RequestAttributes中取到错误信息。

  1. BasicErrorController类
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    private final ErrorProperties errorProperties;

    ...
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
    }

    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<Map<String, Object>>(body, status);
    }

    ...

}

resolveErrorView方法(查找=error/“错误状态码”;的资源):
如果不是异常请求,会执行resolveErrorView方法;该方法会先在默认或配置的静态资源路径下查找error/HttpStatus(错误状态码)的资源文件,如果没有;使用默认的error页面。

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
    ...
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
            Map<String, Object> model) {
            //status:异常错误状态码
        ModelAndView modelAndView = resolve(String.valueOf(status), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //视图名称,默认是error/+“status”错误状态码;比如:error/500、error/404
        String errorViewName = "error/" + viewName;

        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
                .getProvider(errorViewName, this.applicationContext);
        if (provider != null) {
            return new ModelAndView(errorViewName, model);
        }
        return resolveResource(errorViewName, model);
    }
    //在资源文件中查找error/500或error/404等页面
    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        for (String location : this.resourceProperties.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }
    ...
}

BasicErrorController根据Accept头的内容,输出不同格式的错误响应。比如针对浏览器的请求生成html页面,针对其它请求生成json格式的返回。

可以通过配置error/HttpStatus页面实现自定义错误页面。

  1. ErrorPageCustomizer类
/**
 * {@link EmbeddedServletContainerCustomizer} that configures the container's error
 * pages.
 */
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

    private final ServerProperties properties;

    protected ErrorPageCustomizer(ServerProperties properties) {
        this.properties = properties;
    }

    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
                + this.properties.getError().getPath());
        errorPageRegistry.addErrorPages(errorPage);
    }

    @Override
    public int getOrder() {
        return 0;
    }

}

将错误页面注册到内嵌的tomcat的servlet容器中。

  1. PreserveErrorControllerTargetClassPostProcessor实现BeanFactoryPostProcessor接口,可以修改BEAN的配置信息

ErrorAutoConfiguration内的两个配置

//2个config配置
@Configuration
static class DefaultErrorViewResolverConfiguration {

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
            ResourceProperties resourceProperties) {
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
    }

    @Bean
    @ConditionalOnBean(DispatcherServlet.class)
    @ConditionalOnMissingBean
    public DefaultErrorViewResolver conventionErrorViewResolver() {
        return new DefaultErrorViewResolver(this.applicationContext,
                this.resourceProperties);
    }

}

@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

    private final SpelView defaultErrorView = new SpelView(
            "<html><body><h1>Whitelabel Error Page</h1>"
                    + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
                    + "<div id='created'>${timestamp}</div>"
                    + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
                    + "<div>${message}</div></body></html>");

    @Bean(name = "error")
    @ConditionalOnMissingBean(name = "error")
    public View defaultErrorView() {
        return this.defaultErrorView;
    }

    // If the user adds @EnableWebMvc then the bean name view resolver from
    // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
    @Bean
    @ConditionalOnMissingBean(BeanNameViewResolver.class)
    public BeanNameViewResolver beanNameViewResolver() {
        BeanNameViewResolver resolver = new BeanNameViewResolver();
        resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
        return resolver;
    }

}
  1. DefaultErrorViewResolverConfiguration:默认的error视图解析配置;

  2. WhitelabelErrorViewConfiguration:默认设置了/error的页面,和Whitelabel Error Page页面响应内容。

如果Spring MVC在处理业务的过程中抛出异常,会被 Servlet 容器捕捉到,Servlet 容器再将请求转发给注册好的异常处理映射 /error 做响应处理。

springboot配置文件默认error相关配置

springboot配置文件application.properties中关于error默认配置:

server.error.include-stacktrace=never # When to include a "stacktrace" attribute.
server.error.path=/error # Path of the error controller.
server.error.whitelabel.enabled=true # Enable the default error page displayed in browsers in case of a server error.

springboot 自定义异常处理

通过跟踪springboot对异常处理得源码跟踪,根据业务需要,可以细分前端响应的错误页面,也可以统一使用/error页面+错误提示信息进行处理。

根据自己的需求自定义异常处理机制;具体可实施的操作如下:

  1. 可以通过配置error/HttpStatus(错误状态码)页面实现自定义错误页面【底层实现,详见:BasicErrorController源码】;

  2. 可以实现BasicErrorController,自定义普通请求的异常页面响应信息和异步请求的响应信息,统一使用/error页面进行错误响应提示;

  3. 自定义实现ErrorAttributes接口,覆盖DefaultErrorAttributes实现,或是继承DefaultErrorAttributes类,重写里面的方法【TODO,不推荐】。

1和2的方法可单独使用,也可以结合使用。

自定义异常页面

可以根据不同的错误状态码,在前端细分不同的响应界面给用户进行提示;资源路径必须是:静态资源路径下/error/HttpStats(比如:/error/404等)

  1. 自定义异常页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"></meta>
    <title>404友情提示</title>
</head>
<body>
<h1>访问的资源未找到(404)</h1>
</body>
</html>

404.html
500.html等,这里只演示404。

统一异常处理

普通请求,前端使用error页面+自定义错误响应信息;
其他请求(异步),统一自定义错误响应信息,规范处理异步响应的错误判断和处理。

使用springMVC注解ControllerAdvice

/**
 * 
 * @项目名称:wyait-manage
 * @类名称:GlobalExceptionHandler
 * @类描述:统一异常处理,包括【普通调用和ajax调用】
 * </br>ControllerAdvice来做controller内部的全局异常处理,但对于未进入controller前的异常,该处理方法是无法进行捕获处理的,SpringBoot提供了ErrorController的处理类来处理所有的异常(TODO)。
 * </br>1.当普通调用时,跳转到自定义的错误页面;2.当ajax调用时,可返回约定的json数据对象,方便页面统一处理。
 * @创建人:wyait
 * @创建时间:2018年5月22日 上午11:44:55 
 * @version:
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory
            .getLogger(GlobalExceptionHandler.class);

    public static final String DEFAULT_ERROR_VIEW = "error";

    /**
     * 
     * @描述:针对普通请求和ajax异步请求的异常进行处理
     * @创建人:wyait
     * @创建时间:2018年5月22日 下午4:48:58
     * @param req
     * @param e
     * @return
     * @throws Exception
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ModelAndView errorHandler(HttpServletRequest request,
            HttpServletResponse response, Exception e) {
        logger.debug(getClass().getName() + ".errorHandler】统一异常处理:request="+request);
        ModelAndView mv=new ModelAndView();
        logger.info(getClass().getName() + ".errorHandler】统一异常处理:"+e.getMessage());
        //1 获取错误状态码
        HttpStatus httpStatus=getStatus(request);
        logger.info(getClass().getName() + ".errorHandler】统一异常处理!错误状态码httpStatus:"+httpStatus);
        //2 返回错误提示
        ExceptionEnum ee=getMessage(httpStatus);
        //3 将错误信息放入mv中
        mv.addObject("type", ee.getType());
        mv.addObject("code", ee.getCode());
        mv.addObject("msg", ee.getMsg());
        if(!ShiroFilterUtils.isAjax(request)){
            //不是异步请求
            mv.setViewName(DEFAULT_ERROR_VIEW);
            logger.debug(getClass().getName() + ".errorHandler】统一异常处理:普通请求。");
        }
        logger.debug(getClass().getName() + ".errorHandler】统一异常处理响应结果:MV="+mv);
        return mv;
    }
    ...
}

运行测试:先走GlobalExceptionHandler(使用注解@ControllerAdvice)类里面的方法,而后又执行了BasicErrorController方法;被springboot自带的BasicErrorController覆盖。

实现springboot的AbstractErrorController

自定义实现AbstractErrorController,添加响应的错误提示信息。

@RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {

        ModelAndView mv = new ModelAndView(ERROR_PATH);

        /** model对象包含了异常信息 */
        Map<String, Object> model = getErrorAttributes(request,
                        isIncludeStackTrace(request, MediaType.TEXT_HTML));

        // 1 获取错误状态码(也可以根据异常对象返回对应的错误信息)
        HttpStatus httpStatus = getStatus(request);

        // 2 返回错误提示
        ExceptionEnum ee = getMessage(httpStatus);
        Result<String> result = new Result<String>(
                String.valueOf(ee.getType()), ee.getCode(), ee.getMsg());
        // 3 将错误信息放入mv中
        mv.addObject("result", result);
        logger.info("统一异常处理【" + getClass().getName()
                + ".errorHtml】统一异常处理!错误信息mv:" + mv);
        return mv;
    }

    @RequestMapping
    @ResponseBody
    //设置响应状态码为:200,结合前端约定的规范处理。也可不设置状态码,前端ajax调用使用error函数进行控制处理
    @ResponseStatus(value=HttpStatus.OK)
    public Result<String> error(HttpServletRequest request, Exception e) {

        /** model对象包含了异常信息 */
        Map<String, Object> model = getErrorAttributes(request,
                        isIncludeStackTrace(request, MediaType.TEXT_HTML));

        // 1 获取错误状态码(也可以根据异常对象返回对应的错误信息)
        HttpStatus httpStatus = getStatus(request);

        // 2 返回错误提示
        ExceptionEnum ee = getMessage(httpStatus);
        Result<String> result = new Result<String>(
                String.valueOf(ee.getType()), ee.getCode(), ee.getMsg());
        // 3 将错误信息返回
//      ResponseEntity
        logger.info("统一异常处理【" + getClass().getName()
                + ".error】统一异常处理!错误信息result:" + result);
        return result;
    }

针对异步请求,统一指定响应状态码:200;也可以不指定,前端在处理异步请求的时候,可以通过ajax的error函数进行控制。

这里是继承的AbstractErrorController类,自定义实现统一异常处理,也可以直接实现ErrorController接口。

前端ajax异步统一处理:

通过约定,前端ajax异步请求,进行统一的错误处理。

/**
 * 针对不同的错误可结合业务自定义处理方式
 * @param result
 * @returns {Boolean}
 */
function isError(result){
    var flag=true;
    if(result && result.status){
        flag=false;
        if(result.status == '-1' || result.status=='-101' || result.status=='400' || result.status=='404' || result.status=='500'){
            layer.alert(result.data);
        }else if(result.status=='403'){
            layer.alert(result.data,function(){
                //跳转到未授权界面
                window.location.href="/403";
            });
        }
    }
    return flag;//返回true
}

使用方式:

        ...
        success:function(data){
            //异常过滤处理
            if(isError(data)){
                alert(data);
            }
        },
        ...

error.html页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:include="layout :: htmlhead" th:with="title='wyait后台管理'">
    <meta charset="UTF-8"></meta>
    <title th:text="${result.status}"></title>
</head>
<body>
<h1>出错了</h1>
<p><span th:text="${result.message}"></span>(<span th:text="${result.data}"></span>)</p>
</body>
</html>

普通请求:
image
异步请求:
image

线上get请求乱码

前台通过html页面,发送请求到后台查询数据,在日志中打印的sql语句显示传入的参数乱码:

 SELECT ... 
[2018-05-11 09:15:00.582][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:==> Parameters: 1(Integer), çè´º(String)
[2018-05-11 09:15:00.585][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:<==      Total: 1
...

本地windows开发环境测试没有乱码问题;

image
前端页面发送get请求,浏览器默认对get请求路径进行URL编码处理。

后台Controller打印的日志

分页查询用户列表!搜索条件:userSearch:UserSearchDTO{page=1, limit=10, uname='çç', umobile='', insertTimeStart='', insertTimeEnd=''},page:1,每页记录数量limit:10,请求编码:UTF-8

Controller层在接收到这个uname参数时,已经是乱码,ISO-8859-1解码后的结果。

请求参数编码流程

  1. 前端页面发送get请求,浏览器默认在中文的UTF-8后加上上%得到URL编码,比如:%e8%b4%b9%e7...;
  2. get请求到tomcat应用服务器后,会以默认的ISO-8859-1进行解码;
  3. 在controller中,接收到的是经过URL编码和iso-8859-1解码后的参数值。

具体编码细节:TODO

项目编码配置【可以不配置】

开发前,默认必须统一编码环境;正常都是设置为utf-8。

spring boot 与spring mvc不同,在web应用中,spring boot默认的编码格式为UTF-8,而spring mvc的默认编码格式为iso-8859-1。

spring boot项目中如果没有特殊需求,该编码不需要修改。如果要强制其他编码格式,spring boot提供了设置方式:

  1. 通过application.properties配置文件设置:
# 默认utf-8配置
spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

此时拦截器中返回的中文已经不乱码了,但是controller中返回的数据可能会依旧乱码。

  1. 参考spring MVC的方式,自定义实现WebMvcConfigurerAdapter类,处理响应数据乱码问题:
@Bean
public HttpMessageConverter<String> responseBodyConverter() {
    StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
    return converter;
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    super.configureMessageConverters(converters);
    converters.add(responseBodyConverter());
}

也可以在controller方法@RequestMapping上添加:

produces="text/plain;charset=UTF-8"

这种方法的弊端是限定了数据类型。

乱码解决方案

表单采用get方式提交,中文乱码解决方案:

  1. 改为post请求;
  2. 手动编解码:
param = new String(param.getBytes("iso8859-1"), "utf-8");
  1. 修改tomcat配置server.xml文件:
    找到如下代码:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

在这里添加一个属性:URIEncoding,将该属性值设置为UTF-8,即可让Tomcat(默认ISO-8859-1编码)以UTF-8的编码处理get请求。

修改完成后:

<Connector port="8080"  protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
  1. 发送get请求前,浏览器中两次URL编码:
    两次编码两次解码的过程为:
    ==UTF-8编码->UTF-8(iso-8859-1)编码->iso-8859-1解码->UTF-8解码,编码和解码的过程是对称的,所以不会出现乱码。==
    //js代码
    param = encodeURI(param);
    // alert("第一次URL编码:" + param);
    param = encodeURI(param);
    // alert("第二次URL编码:" + param);

后台代码:

//两次解码
URLDecoder.decode(URLDecoder.decode(param,"utf-8"),"utf-8");

以上四种解决方案,可结合具体情况进行使用。

no session异常

异常日志1:

[2018-05-21 18:00:51.574][http-nio-8280-exec-6][DEBUG][org.apache.shiro.web.servlet.SimpleCookie][389]:Found 'SHRIOSESSIONID' cookie value [fc6b7b64-6c59-4f82-853b-e2ca20135b99]
[2018-05-21 18:00:51.575][http-nio-8280-exec-6][DEBUG][org.apache.shiro.mgt.DefaultSecurityManager][447]:Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous (session-less) Subject instance.
org.apache.shiro.session.UnknownSessionException: There is no session with id [fc6b7b64-6c59-4f82-853b-e2ca20135b99]
    at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170) ~[shiro-all-1.3.1.jar:1.3.1]

异常日志2【偶尔出现】:

Caused by: javax.crypto.BadPaddingException: Given final block not properly padded
    at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:811) ~[sunjce_provider.jar:1.7.0_85]

UnknownSessionException
UnknownSessionException: There is no session with id [...]

结合项目配置,分析问题原因:
1,用户退出后,浏览器中的SHIROSESSIONID依然存在;
2,再次发送请求时,携带SHIROSESSIONID,会在shiro的DefaultWebSecurityManager.getSessionKey(context)中,逐层跟踪对应在sessionManager中session值,没有的话,最终在AbstractSessionDAO.readSession(sessionID)中抛出异常。

  1. 在程序中退出的地方,清除cookie:
//删除cookie
Cookie co = new Cookie("username", "");
co.setMaxAge(0);// 设置立即过期
co.setPath("/");// 根目录,整个网站有效
servletResponse.addCookie(co);
  1. 设置SimpleCookie的过期时间,和session、ehcache缓存时间保持一致;
@Bean
public SimpleCookie sessionIdCookie() {
    //DefaultSecurityManager
    SimpleCookie simpleCookie = new SimpleCookie();
    //如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能防止XSS×××。
    simpleCookie.setHttpOnly(true);
    simpleCookie.setName("SHRIOSESSIONID");
    simpleCookie.setMaxAge(86400000*3);
    return simpleCookie;
}
  1. 手动实现shiro的logout方法,清除浏览器cookie;

  2. 重写AbstractSessionDAO.readSession方法,如果session为null,清空浏览器cookie;
  3. 不做处理;实际项目运行中,不影响功能执行。

源码已集成到项目中:

github源码: https://github.com/wyait/manage.git
码云:https://gitee.com/wyait/manage.git
github对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0

20200808新版本更新

版本升级及内容优化版本,改动内容:

  1. 版本更新,springboot从1.5+升级到2.1+;
  2. 权限缓存使用redis;
  3. 验证码使用redis;
  4. 权限验证完善。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK