需求
项目基于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;
}
}
- 想要通过利用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()),这样可能导致系统出错,故不建议第一种方式
余生还长,切勿惆怅。创作不易,随手点赞