Shiro实现会话过期动态跳转(动态跳转到loginUrl)

在这里插入图片描述
需求

项目基于session实现的会话机制,在会话过期后根据当前登录的角色实现不同界面的跳转,管理员角色登录过期后跳转到/manage/login界面,其他角色登录过期后跳转到/login界面


项目结构

springboot + shiro + session + thymeleaf

依赖

		<!--Shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <!--ThymeLeaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
		
		<!-- ThymeLeaf 布局 -->
        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>

解决思路

本来想的是通过直接配置一个过滤器对请求进行拦截,然后判断HTTP请求的header地址来决定跳转界面,但由于项目中配置了过多的放行接口,所以打算使用shiro来进行配置,对shiro中的拦截请求进行验证 ,结合项目中url特点 “/控制器名称/角色名称/接口名称”,对管理员接口例如/version/admin/getPageList进行特殊跳转处理

在这里插入图片描述



代码调试

先看一下解决完自定义跳转之后shiro的核心配置类

  • ShiroConfig

import com.luntek.platform.ic_manufacturing_platform.filter.CustomRolesAuthorizationFilter;
import com.luntek.platform.ic_manufacturing_platform.filter.MyAuthenticationFilter;
import com.luntek.platform.ic_manufacturing_platform.filter.MyRolesAuthorizationFilter;
import com.luntek.platform.ic_manufacturing_platform.filter.UserRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;


/**
 * shiro中自实现的过滤器仅注入到spring中无法执行(@Bean等方式),需要添加到filtersMap然后注入到chains中
 * <p>
 * shiro中核心配置文件,重要过滤器名称与类对应关系
 * Filter Name                 Class
 * anon                 org.apache.shiro.web.filter.authc.AnonymousFilter
 * authc                org.apache.shiro.web.filter.authc.FormAuthenticationFilter
 * authcBasic           org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
 * logout               org.apache.shiro.web.filter.authc.LogoutFilter
 * noSessionCreation    org.apache.shiro.web.filter.session.NoSessionCreationFilter
 * perms                org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
 * port                 org.apache.shiro.web.filter.authz.PortFilter
 * rest                 org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
 * roles                org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
 * ssl                  org.apache.shiro.web.filter.authz.SslFilter
 * user                 org.apache.shiro.web.filter.authc.UserFilter
 */
@Slf4j
@Configuration
public class ShiroConfig {


    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意:初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
     *
     * @param securityManager 安全组
     * @return ShiroFilterFactoryBean
     * Shiro内置过滤器,可以实现权限相关的拦截器
     * 常用的过滤器:
     * anon:无需认证(登录)可以访问
     * authc:必须认证才可以访问
     * user:如果使用rememberMe的功能可以直接访问
     * perms:该资源必须得到资源权限才可以访问
     * role:该资源必须得到角色权限才可以访问
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        log.info("==================ShiroFilter方法==================");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据,被拦截后跳转的地址
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接自行处理。不用shiro进行跳转
        shiroFilterFactoryBean.setSuccessUrl("/course/course");
        //未授权界面跳转的页面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/noauthor");

        /*登出过滤器或者单账号登录过滤器*/
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //限制同一帐号同时在线的个数
        /*filtersMap.put("kickout", kickoutSessionControlFilter());*/
        //将自定义的shiro过滤器放入到
        filtersMap.put("roleOrFilter", new CustomRolesAuthorizationFilter());
        // 将重写的Filter注入到factoryBean的filter中
        filtersMap.put("authc", new MyAuthenticationFilter());
        //将重写的Filter注入到factoryBean的filter中
        filtersMap.put("roles", new MyRolesAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);

        //添加Shiro内置过滤器
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/manage/login", "anon");
        filterChainDefinitionMap.put("/manage/toLogin", "anon");
        filterChainDefinitionMap.put("/toLogin", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        //根据角色拦截指定页面
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
        filterChainDefinitionMap.put("/teacher/**", "roles[teacher]");
        filterChainDefinitionMap.put("/student/**", "roles[student]");
        
        //处理一个用户多个角色时具有一个角色时就放行的问题
      filterChainDefinitionMap.put("/download/vindicator/**","roleOrFilter[teacher,admin]");


        filterChainDefinitionMap.put("/global/*", "anon");  //全局路径(错误或者超时)
        filterChainDefinitionMap.put("/static/**", "anon");

        //放行swagger相关请求
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/webjars/springfox-swagger-ui/**", "anon");


        //拦截其他所以接口
        filterChainDefinitionMap.put("/**", "authc");


        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * 创建DefaultWebSecurityManager
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    /**
     * 创建Realm
     */
    @Bean("userRealm")
    public UserRealm getRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(matcher);
        return userRealm;
    }

    /**
     * 创建自定义的登录证书验证Matcher
     */
    @Bean("credentialMatcher")
    public CredentialMatcher getCredentialMatcher() {
        return new CredentialMatcher();
    }

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

    @Bean("defaultAdvisorAutoProxyCreator")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }


}

  1. 想要通过利用shiro的过滤器解决问题就需要先知道shiro中过滤器的继承关系
    在这里插入图片描述

NameableFilter
NameableFilter给Filter起个名字,如果没有设置默认就是FilterName;还记得之前的如authc吗?当我们组装拦截器链时会根据这个名字找到相应的拦截器实例;

OncePerRequestFilter
OncePerRequestFilter用于防止多次执行Filter的;也就是说一次请求只会走一次拦截器链;另外提供enabled属性,表示是否开启该拦截器实例,默认enabled=true表示开启,如果不想让某个拦截器工作,可以设置为false即可。
在这里插入图片描述
AdviceFilter
AdviceFilter提供了AOP风格的支持,类似于SpringMVC中的Interceptor

1.  boolean preHandle(ServletRequest request, ServletResponse response) throws Exception  
2.  void postHandle(ServletRequest request, ServletResponse response) throws Exception  
3.  void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;   

preHandler:类似于AOP中的前置增强;在拦截器链执行之前执行;如果返回true则继续拦截器链;否则中断后续的拦截器链的执行直接返回;进行预处理(如基于表单的身份验证、授权)
postHandle:类似于AOP中的后置返回增强;在拦截器链执行完成后执行;进行后处理(如记录执行时间之类的);
afterCompletion:类似于AOP中的后置最终增强;即不管有没有异常都会执行;可以进行清理资源(如接触Subject与线程的绑定之类的);

PathMatchingFilter

PathMatchingFilter提供了基于Ant风格的请求路径匹配功能及拦截器参数解析的功能,如“roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径:

1.  boolean pathsMatch(String path, ServletRequest request)  
2.  boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception  

pathsMatch:该方法用于path与请求路径进行匹配的方法;如果匹配返回true
onPreHandle:在preHandle中,当pathsMatch匹配一个路径后,会调用opPreHandler方法并将路径绑定参数配置传给mappedValue;然后可以在这个方法中进行一些验证(如角色授权),如果验证失败可以返回false中断流程;默认返回true;也就是说子类可以只实现onPreHandle即可,无须实现preHandle。如果没有path与请求路径匹配,默认是通过的(即preHandle返回true)。
在这里插入图片描述

AccessControlFilter

AccessControlFilter提供了访问控制的基础功能;比如是否允许访问/当访问拒绝时如何处理等:

1.  abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
2.  boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
3.  abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception; 

以下两个方法特别重要,也是解决此问题的关键:
isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。

onPreHandle会自动调用这两个方法决定是否继续处理:

boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); 
} 

另外AccessControlFilter还提供了如下方法用于处理如登录成功后/重定向到上一个请求:

1.  void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp  
2.  String getLoginUrl()  
3.  Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例  
4.  boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求  
5.  void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面  
6.  void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求  
7.  void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面   

比如基于表单的身份验证就需要使用这些功能。
到此基本的拦截器就完事了,如果我们想进行访问访问的控制就可以继承AccessControlFilter;如果我们要添加一些通用数据我们可以直接继承PathMatchingFilter。

重写重要类AuthorizationFilter、FormAuthenticationFilter、RolesAuthorizationFilter的onAccessDenied()方法,onAccessDenied方法在请求经过当前过滤器isAccessAllowed()方法返回false时被调用,代表当前过滤器如果拦截了URL请求应该如何处理

重写过滤器时一直遇到过滤器代码无法被执行的问题,分析后有两种可能,一是ShiroConfig中没有添加到filtersMap中而是通过@Bean等注解注入到容器中,此方法有可能导致过滤器执行顺序有问题;二是拦截器没有注入到shiro的过滤器链中

shiro配置文件中添加配置
在这里插入图片描述

debug调试

自定义过滤器注入情况
ShiroConfig的shiroFilter的return断点
在这里插入图片描述
ShiroFilterFactoryBean查看过滤器链调用顺序以及类注入情况
在这里插入图片描述
保证顺序没有问题之后在进行后续操作

接下来是重点操作
由于shiro中过滤器链较多,有时无法确定请求是否执行经过了那些过滤器,这时候由于我们清楚AccessControllerFilter的isAccessAllowed是判断授权时请求是否放行的方法,所以可以在这个地方进行调试,然后就可以很快速的进入到URL所进入的过滤器
在这里插入图片描述
在这里插入图片描述

访问接口自动跳转到url所经过的过滤器中,这样的话任何请求都可以知道它所经过的过滤器
在这里插入图片描述

因此,需要对特定的URL进行处理时,只需要接口进入的过滤器中,重写isAccessAllowed方法是代表何时放行请求,重写onAccessDenied()方法代表请求被拒绝后如何处理,重写redirectToLogin方法处理的是请求失败时如何重定向到登录界面(通过Code =》Generate =》Override Methods进行选择)

除此之外可能还需要通过maven下载依赖源码进行全局搜索部分关键字,以及debug class文件(添加两个插件后重启),具体方法可以百度,这里不做细说,有助于快速理解shiro框架原理

剩下的基本就是重写的核心代码了,在这里贴出来做一个参考

**MyAuthenticationFilter **


import com.luntek.platform.ic_manufacturing_platform.utils.AjaxUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 * @author Czw
 * @Description 用户登录过滤器
 * @Date 2019/4/8 0008 上午 9:10
 */
@Slf4j
public class MyAuthenticationFilter extends FormAuthenticationFilter {


    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        log.error("***MyAuthenticationFilter中redirectToLogin***");
        CustomRolesAuthorizationFilter.shiroFilterRedirectToLogin(request, response);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        log.error("***MyAuthenticationFilter中isAccessAllowed***");
        if (isLoginRequest(request, response)) {
            return true;
        } else {
            Subject subject = getSubject(request, response);
            if (subject.getPrincipal() != null) {
                return true;
            } else {
                try {
                    log.info("***登陆过期,重新登录***");
                    AjaxUtil.sendResponse(403, "登陆过期,重新登录");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return false;
            }
        }
    }

    /**
     * 被拒绝访问时调用的方法,例如会话过期等
     *
     * @param request  请求对象
     * @param response 响应对象
     * @return 返回值
     * @throws Exception 异常信息
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        log.info("***MyAuthenticationFilter的onAccessDenied***");
        if (isLoginRequest(request, response)) {
            log.info("***isLoginRequest***");
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                return executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                //allow them to see the login page ;)
                return true;
            }
        } else {
            log.info("onAccessDenied中访问被拒绝,重定向到登录界面");
            this.redirectToLogin(request, response);
            return false;
        }
    }


}

CustomRolesAuthorizationFilter 处理roleOrFilter时具有一种角色就可以访问

import com.luntek.platform.ic_manufacturing_platform.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * shiro中过滤器,使得同一URL配置多个角色的时候变成或的关系,满足一个角色就能访问
 * roleOrFilter[teacher,admin]配置默认为同时具有两个角色
 *
 * @author Czw
 * @Date 2019/5/21 0021 上午 9:16
 */
@Slf4j
public class CustomRolesAuthorizationFilter extends AuthorizationFilter {

    /**
     * 判断用户是否具有访问接口的角色权限
     *
     * @param servletRequest  请求对象
     * @param servletResponse 响应对象
     * @param mappedValue     访问此接口需要的角色权限数组(当前访问者具有一个就行)
     * @return 是否具有访问权限
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
        log.info("***isAccessAllowed***");
        Subject subject = getSubject(servletRequest, servletResponse);
        //获取角色的数组内容
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) { //没有角色限制,有权限访问
            return true;
        }
        for (String s : rolesArray) {
            //若当前用户是rolesArray中的任何一个,则有权限访问
            if (subject.hasRole(s)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 此过滤器中发生充定向到首页的方法
     *
     * @param request  请求对象
     * @param response 响应对象
     */
    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        log.error("***CustomRolesAuthorizationFilter中redirectToLogin***");
        shiroFilterRedirectToLogin(request, response);
    }

    /**
     * 根据请求的URI和referer来决定重定向的地址
     * 普通请求重定向到/login
     * 特殊请求重定向到管理员用户/manage/login
     *
     * @param request  请求对象
     * @param response 响应对象
     */
    protected static void shiroFilterRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String uri = httpServletRequest.getRequestURI();
        log.error("***uri=【{}】***", uri);
        String referer = httpServletRequest.getHeader("Referer");
        log.error("***referer=【{}】***", referer);

        boolean flag = false;
        for (String i : Constants.REDIRECT_ADMIN_URL) {
            if (uri.contains(i) || referer.contains(i)) {
                flag = true;
                break;
            }
        }

        if (flag) {
            log.error("***redirectToLogin-ADMIN***");
            WebUtils.issueRedirect(request, response, Constants.ADMIN_LOGIN_URL);
        } else {
            log.error("***redirectToLogin-USER***");
            WebUtils.issueRedirect(request, response, Constants.USR_LOGIN_URL);
        }
    }


    /**
     * 经过当前过滤器授权授权失败时执行此方法
     *
     * @param request  请求对象
     * @param response 响应对象
     * @return 是否成功
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        log.info("***CustomRolesAuthorizationFilter中onAccessDenied***");
        Subject subject = getSubject(request, response);
        // If the subject isn't identified, redirect to login URL
        if (subject.getPrincipal() == null) {
            saveRequestAndRedirectToLogin(request, response);
        } else {
            // If subject is known but not authorized, redirect to the unauthorized URL if there is one
            // If no unauthorized URL is specified, just return an unauthorized HTTP status code
            String unauthorizedUrl = getUnauthorizedUrl();
            //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:
            if (StringUtils.hasText(unauthorizedUrl)) {
                WebUtils.issueRedirect(request, response, unauthorizedUrl);
            } else {
                WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
            }
        }
        return false;
    }
}

**MyRolesAuthorizationFilter **


import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 * 用于处理shiro中经过roles的过滤器请求
 *
 * @author: Czw
 * @create: 2021-03-17 19:33
 **/
@Slf4j
public class MyRolesAuthorizationFilter extends RolesAuthorizationFilter {

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        log.error("***MyRolesAuthorizationFilter的onAccessDenied***");
        CustomRolesAuthorizationFilter.shiroFilterRedirectToLogin(request, response);
        return false;
    }
}

Constants

    //普通用户跳转登录界面
    public static final String USR_LOGIN_URL = "/login";
    //管理员跳转登录界面
    public static final String ADMIN_LOGIN_URL = "/manage/login";
    //url中包含其中的部分时需要跳转到管理员登录界面
    public static final String[] REDIRECT_ADMIN_URL = {"/admin", "/manage"};



效果

为了方便看效果可以降低session的有效时间,降低session的有效时间有两种方法,一种是在登录之后设置session有效时长,另一种是通过shiro获取到subject后获取session对象设置过期时间,虽然项目中使用的是第一种

//HttpSession session
session.setMaxInactiveInterval(10); //单位S
@Bean
    public DefaultWebSessionManager getDefaultWebSessionManager() {
	DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
	//会话过期时间,单位:毫秒。如果小于1000ms会被容器系统转化成0S
	defaultWebSessionManager.setGlobalSessionTimeout(1000 * 60);
	defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
	defaultWebSessionManager.setSessionIdCookieEnabled(true);
	return defaultWebSessionManager;
    }

为什么建议使用第二种方式呢,因为shiro是一个自带会话机制的框架,虽然它也是基于spring的session来的,但它还对spring的session进行了修饰,包括获取、停用session也不一样。例如项目中有个功能是限制系统中单账号登录的功能,在另一台电脑在登录同一个账号后,实际原来的session是已经失效了的,如果使用spring的session方法失效,那么在shiro中还可以获取到session(subject.getSession()),这样可能导致系统出错,故不建议第一种方式
在这里插入图片描述


余生还长,切勿惆怅。创作不易,随手点赞

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一米阳光zw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值