SpringSecurity(2)用户状态设置、记住我、用户授权、权限异常处理

1、设置用户状态

在我们的数据库表sys_user中,有一个status字段,用来表示用户的状态,表示:有效或无效等。比如有些用户恶意操作,我们需要对它封号,就需要设置一个状态,这样的话,该用户就无法登录了。根据业务需要,可以设置多个状态字段,Spring Security的User对象也为我们封装一个有关用户状态的构造方法,接下来就来了解下该怎么使用吧

1.1 源码分析

用户认证业务里,我们封装Spring Security的org.springframework.security.core.userdetails.User对象时,选择了三个构造参数的构造方法,其实还有另一个构造方法:

public class User implements UserDetails, CredentialsContainer {
    //......
	//三个参数的构造方法
    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }
	//多个参数的构造方法,第3个到第6个参数都是状态相关的,必须全部都是true,该对象才能是可用的
    public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        if (username != null && !"".equals(username) && password != null) {
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.accountNonExpired = accountNonExpired;
            this.credentialsNonExpired = credentialsNonExpired;
            this.accountNonLocked = accountNonLocked;
            this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }
}

可以看到,这个构造方法里多了四个布尔类型的构造参数,其实我们使用的三个构造参数的构造方法里这四个布尔

值默认都被赋值为了true,那么这四个布尔值到底是何意思呢?

  • boolean enabled 是否可用

  • boolean accountNonExpired 账户是否失效

  • boolean credentialsNonExpired 密码是否失效

  • boolean accountNonLocked 账户是否锁定

1.2 认证方法修改,添加状态判断

修改UserServiceImpl中的认证方法loadUserByUsername,在返回用户对象给Spring Security的时候,调用7个参数的构造方法。这四个参数必须同时为true认证才可以,为了节省时间,我只用第一个布尔值做个测试,修改认证业务代码:

 /**
     * Spring Security进行用户认证的方法
     * @param username                      页面传过来的用户名
     * @return  UserDetails                 UserDetails是Spring Security自己的用户对象,如果返回null就表示认证失败
     * @throws UsernameNotFoundException
     */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    try {
        //根据用户名从数据库中查询到用户信息
        SysUser sysUser = userDao.findByName(username);
        if(sysUser == null) {
            //若用户名不对,直接返回null,表示认证失败。
            return null;
        }
        //把sysUser封装成Spring Security可以认识的UserDetails对象
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        //得到当前用户的所有角色的集合
        List<SysRole> roles = sysUser.getRoles();
        //遍历角色(权限),存入到authorities中
        for (SysRole role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        }

        /**
             * 最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
             * 浏览器输入的密码已经被Spring Security保存了,这里封装之后,会进行比对,如果是相同的就是认证通过,否则认证失败
             */
        //UserDetails userDetails = new User(username,sysUser.getPassword(),authorities);
        //使用用户状态的认证对象
        UserDetails userDetails = new User(username,sysUser.getPassword(),
                                           sysUser.getStatus()==1, //用户状态为1表示可用,0表示不可用,数据库表中定义
                                           true,
                                           true,
                                           true,
                                           authorities);
        return userDetails;
    }catch (Exception e){
        e.printStackTrace();
        return null;
    }

}

此刻,只有用户状态为1的用户才能成功通过认证!

1.3 测试

可以新增一个用户:

在这里插入图片描述

然后使用这个新用户登录系统,因为设置的状态是关闭,status的值是0,那么登录是失败的

在这里插入图片描述

2、remember me

2.1 记住我功能原理分析

还记得前面咱们分析认证流程时,提到的记住我功能吗?

2.1.1 AbstractAuthenticationProcessingFilter

用户名密码登录的过滤器是UsernamePasswordAuthenticationFilter,登录成功的逻辑在它的父类中实现:AbstractAuthenticationProcessingFilter的doFilter有登录成功的逻辑:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
			//登录成功
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }
    //登录成功执行的方法
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);
        //这里就是处理记住我的功能,点击进去查看
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }
 }

2.1.2 AbstractRememberMeServices

从上面分析的这句代码:

//这里就是处理记住我的功能,点击进去查看
 this.rememberMeServices.loginSuccess(request, response, authResult);

点击进去发现是下面的这个接口:

public interface RememberMeServices {
    Authentication autoLogin(HttpServletRequest var1, HttpServletResponse var2);

    void loginFail(HttpServletRequest var1, HttpServletResponse var2);

    void loginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);
}

查看它的实现类:AbstractRememberMeServices的loginSuccess方法:

public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
    public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
    private String parameter = "remember-me";

    public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        // 判断是否勾选记住我 ,rememberMeRequested方法就在下面
        // 注意:这里this.parameter点进去是上面的private String parameter = "remember-me";
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
            //若勾选就调用onLoginSuccess方法
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }
	//这个是抽象方法,所以应该看子类的实现
    protected abstract void onLoginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);

    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        if (this.alwaysRemember) {
            return true;
        } else {
            // 从上面的字parameter的值为"remember-me" 
            // 也就是说,此功能提交的属性名必须为"remember-me"
            String paramValue = request.getParameter(parameter);
            // 这里我们看到属性值可以为:true,on,yes,1。
            if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
                
                //满足上面条件才能返回true
                return true;
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
                }

                return false;
            }
        }
    }
}

如果上面方法返回true,就表示页面勾选了记住我选项了。

继续顺着调用的方法找到PersistentTokenBasedRememberMeServices的onLoginSuccess方法:

2.1.3 PersistentTokenBasedRememberMeServices

选中了记住我之后,就会走onLoginSuccess方法,我们看PersistentTokenBasedRememberMeServices的onLoginSuccess方法:

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        // 获取用户名
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        //创建记住我的token
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            //将token持久化到数据库
            this.tokenRepository.createNewToken(persistentToken);
            
            //将token写入到浏览器的Cookie中
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }
}

2.2 记住我功能实现

2.2.1 记住我功能页面代码

注意name和value属性的值不要写错哦!

<div class="col-xs-8"> 
    <div class="checkbox icheck"> 
        <!-- 注意name和value属性的值不要写错哦 --> 
        <label>
            <input type="checkbox" name="remember-me" value="true"> 记住 下次自动登录 	             </label> 
    </div> 
</div>

先测试一下,认证通过后,关掉浏览器,再次打开页面,发现还要认证!为什么没有起作用呢?

这是因为remember me功能使用的过滤器RememberMeAuthenticationFilter默认是不开启的!

2.2.2 开启remember me过滤器

在spring-security.xml中添加下面的配置:

<security:http>
	<!--省略其余配置-->
    <!--开启remember me过滤器,设置token存储时间为60秒-->
    <security:remember-me token-validity-seconds="60"/>
</security:http>

说明:RememberMeAuthenticationFilter中功能非常简单,会在打开浏览器时,自动判断是否认证,如果没有则调用autoLogin进行自动认证。

2.3 持久化remember me信息

2.3.1 remember me安全性分析

记住我功能方便是大家看得见的,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。

此外,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在db中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到db中验证,如果通过,自动登录才算通过。

2.3.2 持久化remember me信息到数据库

1、创建数据库表

创建一张表,注意这张表的名称和字段都是固定的,不要修改:

CREATE TABLE `persistent_logins` ( 
    `username` varchar(64) NOT NULL, 
    `series` varchar(64) NOT NULL, 
    `token` varchar(64) NOT NULL, 
    `last_used` timestamp NOT NULL, 
    PRIMARY KEY (`series`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8

然后将spring-security.xml中 改为:

<security:http>
	<!--省略其余配置-->
    <!-- 开启remember me过滤器,
                data-source-ref="dataSource" 指定数据库连接池
                token-validity-seconds="60" 设置token存储时间为60秒 可省略
                remember-me-parameter="remember-me" 指定记住的参数名 可省略 -->
    <security:remember-me data-source-ref="dataSource"
                          token-validity-seconds="60"
                          remember-me-parameter="remember-me"/>
</security:http>

最后测试发现数据库中自动多了一条记录:

在这里插入图片描述

3、显示登录用户名

3.1 前端jsp获取

在header.jsp中找到页面头部最右侧图片处添加如下信息:

<!-- 用户名的显示 -->
<span class="hidden-xs">
    <security:authentication property="name" />

    <%--或者用下面的--%>
    <%--<security:authentication property="principal.username" />--%>
</span>

3.2 后端获取

使用了Spring Security之后,就可以从SecurityContextHolder获取

//获取当前登录的用户名
String name = SecurityContextHolder.getContext().getAuthentication().getName();

//或者通过下面的方式获取
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = (User) principal;
String username = user.getUsername();

4、用户授权

4.1 授权准备工作

为了模拟授权操作,咱们临时编写两个业务功能:

处理器代码:

package com.itheima.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/order")
public class OrderController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "order-list";
    }
}
package com.itheima.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/product")
public class ProductController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}

aside.jsp页面:

<ul class="treeview-menu">
    <li id="system-setting">
        <a href="${pageContext.request.contextPath}/product/findAll">
        <i class="fa fa-circle-o"></i> 产品管理
        </a>
    </li>
    <li id="system-setting">
        <a href="${pageContext.request.contextPath}/order/findAll">
        <i class="fa fa-circle-o"></i> 订单管理
        </a>
    </li>
</ul>

4.2 动态展示菜单

在aside.jsp对每个菜单通过SpringSecurity标签库指定访问所需角色,主要就以产品管理和角色管理菜单做演示

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<aside class="main-sidebar">
    <!-- sidebar: style can be found in sidebar.less -->
    <section class="sidebar">
        <!-- 这里省略了很多... -->

            <li class="treeview"><a href="#"> <i class="fa fa-cube"></i>
                <span>基础数据</span> <span class="pull-right-container"> <i
                        class="fa fa-angle-left pull-right"></i>
				</span>
            </a>
                <ul class="treeview-menu">

                    <!-- 产品模块还需要产品角色,注意这里还需再次指定超级管理员 -->
                    <security:authorize access="hasAnyRole('ROLE_PRODUCT','ROLE_ADMIN')" >
                    <li id="system-setting"><a
                            href="${pageContext.request.contextPath}/product/findAll">
                        <i class="fa fa-circle-o"></i> 产品管理
                    </a></li>
                    </security:authorize>
                    <!-- 订单模块还需要订单角色,注意这里还需再次指定超级管理员 -->
                    <security:authorize access="hasAnyRole('ROLE_ORDER','ROLE_ADMIN')" >
                    <li id="system-setting"><a
                            href="${pageContext.request.contextPath}/order/findAll">
                        <i class="fa fa-circle-o"></i> 订单管理
                    </a></li>
                    </security:authorize>
                </ul>
            </li>
        </ul>
    </section>
    <!-- /.sidebar -->
</aside>

我们做个测试,xiaoming这个用户现在只有普通用户角色ROLE_USER,用xiaoming登录后,果然就看不到产品管理和订单管理菜单:

那么问题来了,是不是现在已经授权成功了呢?答案是否定的!你可以试试直接去访问产品的http请求地址:http://localhost:8080/product/findAll ,这个时候能访问到该页面。

总结一句:页面动态菜单的展示只是为了用户体验,并未真正控制权限!

4.3 授权操作

4.3.1 Spring IOC父子容器说明

说明:SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在controller类中,对应注解支持应该放在mvc配置文件中,因为controller类是由mvc配置文件扫描并创建的,同理,注解放在service类中,对应注解支持应该放在spring配置文件中。由于我们现在是模拟业务操作,并没有service业务代码(正常情况应该把权限注解配置到service方法上,因为service是父容器管理,不会被http请求访问,会根据的安全),所以就把注解放在controller类中了(由此也就决定了我们要在spring-mvc.xml中开启权限的注解支持)。

可以通过下面的图可以知道Spring IOC父子容器的区别:

在这里插入图片描述

1、ServletContext容器是所有的Web容器都有的,是最外层的;

2、当web.xml配置的ContextLoaderListener监听器加载了Spring的主配置文件:applicationContext.xml后,就会在ServletContext中创建一个Spring IOC的父容器,它是注册和管理service,dao相关的Bean,不能被http请求访问,主要是被controller进行调用,并且不能调用controller;

3、当web.xml配置的DispatcherServlet前置处理器加载了SpringMVC的配置文件:spring-mvc.xml,就会创建一个Spring IOC的子容器,它是注册和管理controller,可以被http请求访问,并且能调用service的Bean。

所以:父容器中的对象可以被子容器调用,不能被http请求访问;子容器可以访问父容器的对象,子容器的对象可以被http请求访问。

4.3.2 开启权限注解支持

在spring-mvc.xml中开启权限注解的支持;添加下面的配置

这里给大家演示三类注解,但实际开发中,用一类即可!


    <!-- 开启权限控制注解支持
        jsr250-annotations="enabled"表示支持jsr250-api的注解,需要jsr250-api的jar包
        pre-post-annotations="enabled"表示支持spring表达式注解
        secured-annotations="enabled"这才是SpringSecurity提供的注解 -->
    <security:global-method-security
            jsr250-annotations="enabled"
            pre-post-annotations="enabled"
            secured-annotations="enabled"/>

如果报错,是因为确实security的名称空间,手动引入即可。

4.3.3 在对应类或者方法上添加权限注解

JSR-250注解

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.security.RolesAllowed;

@Controller
@RequestMapping("/product")
//表示当前类中所有方法都需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@RolesAllowed({"ROLE_ADMIN","ROLE_PRODUCT"})//JSR-250注解
public class ProductController {

    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}

spring表达式注解

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/product")
public class ProductController {

    @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')")//spring表达式注解
    @RequestMapping("/findAll")
    public String findAll(){
       return "product-list";
    }
}

SpringSecurity注解

import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/product")
public class ProductController {

    @Secured({"ROLE_ADMIN","ROLE_PRODUCT"})//SpringSecurity注解
    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}

上面的三种注解,任意一种都可以实现。

也可以同样的方式给OrderController添加注解。

4.3.4 测试

现在进去看不到产品的菜单,如果直接浏览器访问:http://localhost:8080/product/findAll 也访问不了了,会出现403的错误。达到了我们的要求

5、权限不足的异常处理

大家也发现了,每次权限不足都出现403页面,着实难堪!体会一下:

在这里插入图片描述

这样的错误直接返回给了客户,这是非常不友好的,现在我们立马消灭它!

可以有如下三种方式来处理403异常。

5.1 spring-security.xml配置403错误页面(不推荐)

 <security:http>
     <!--省略了其他的配置-->
     
	<!--处理403异常-->
    <security:access-denied-handler error-page="/403.jsp"/>
</security:http>

5.2 在web.xml配置403错误页面

所有的web工程,在web容器中可以处理相关的异常,跳转到一个友好的页面。在web.xml中添加如下的配置:

<error-page>
    <error-code>403</error-code>
    <location>/403.jsp</location>
</error-page>
<error-page>
    <error-code>404</error-code>
    <location>/404.jsp</location>
</error-page>
<error-page>
    <error-code>500</error-code>
    <location>/500.jsp</location>
</error-page>

5.3 全局异常处理

上面的异常处理都是在web工程中使用的,因为他们都是基于过滤器来处理异常,而过滤器是依赖于Servlet容器的。而目前,微服务很流行,都会是前后端分离的,所以上面的方式就不太适用了。

我们可以基于拦截器的思想来处理,可以自己编写一个拦截器,然后在Spring的配置文件中进行配置,这样当然也是可以的。但是既然用到了Spring的框架,那么就不需要这么麻烦了。

5.3.1 实现HandlerExceptionResolver

Spring给我们提供了一个接口:HandlerExceptionResolver,它就是基于拦截器机制来处理异常的,我们只需要写一个类,实现它里面的一个方法:resolveException,就能处理对应的异常了

package com.itheima.controller.exception;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class HandlerControllerException implements HandlerExceptionResolver {
    /**
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o  出现异常的对象
     * @param e  出现的异常信息
     * @return   ModelAndView
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        ModelAndView mv = new ModelAndView();
        //将异常信息放入request域中,基本不用
        mv.addObject("errorMsg", e.getMessage());
        //指定不同异常跳转的页面
        if(e instanceof AccessDeniedException){
            //如果直接写403.jsp,那么视图解析器会加上前后缀,那么就找不到该页面了,加上redirect:之后,视图解析器就不会解析了
            mv.setViewName("redirect:/403.jsp");
        }else {
            mv.setViewName("redirect:/500.jsp");
        }
        return mv;
    }
}

5.3.2 注解@ControllerAdvice和@ExceptionHandler

当然其实使用了SpringMVC之后,不用上面这样麻烦了,只需要使用@ControllerAdvice和@ExceptionHandler注解就可以了:

package com.itheima.controller.exception;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class HandlerControllerAdvice{

    @ExceptionHandler(AccessDeniedException.class)
    public String handlerException(){
        return "redirect:/403.jsp";
    }

    @ExceptionHandler(RuntimeException.class)
    public String runtimeHandlerException(){
        return "redirect:/500.jsp";
    }

}

@ControllerAdvice该注解就相当于表明这个类是一个拦截器

r

当然其实使用了SpringMVC之后,不用上面这样麻烦了,只需要使用@ControllerAdvice和@ExceptionHandler注解就可以了:

package com.itheima.controller.exception;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class HandlerControllerAdvice{

    @ExceptionHandler(AccessDeniedException.class)
    public String handlerException(){
        return "redirect:/403.jsp";
    }

    @ExceptionHandler(RuntimeException.class)
    public String runtimeHandlerException(){
        return "redirect:/500.jsp";
    }

}

@ControllerAdvice该注解就相当于表明这个类是一个拦截器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值