shiro自定义拦截器继承AccessControllerFilter,在ShiroConfig中加入到拦截器链中。
package com.lwp.website.shiro;
import com.alibaba.fastjson.JSON;
import com.lwp.website.entity.Vo.UserVo;
import com.lwp.website.res.ResourceManage;
import com.lwp.website.utils.RedisUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.LinkedList;
/**
* Created with IntelliJ IDEA.
*
* @Auther: liweipeng
* @Date: 2021/02/01/10:59
* @Description:
*/
public class KickoutSessionControlFilter extends AccessControlFilter{
//踢出后的地址
private String kickoutUrl;
//踢出之前登陆的/之后登陆的用户 默认踢出之前登陆的用户
private boolean kickoutAfter = false;
//同一个账号最大会话数 默认 1
private int maxSession = 1;
public static final String DEFAULT_KICKOUT_CACHE_KEY_PREFIX = "shiro:cache:kickout:";
private String keyPrefix = DEFAULT_KICKOUT_CACHE_KEY_PREFIX;
public void setKickoutUrl(String kickoutUrl){
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter){
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession){
this.maxSession = maxSession;
}
public void setKeyPrefix(String keyPrefix){
this.keyPrefix = keyPrefix;
}
public String getKeyPrefix(){
return this.keyPrefix;
}
private String getRedisKickoutKey(String username){
return this.keyPrefix+username;
}
/**
* 是否允许访问 返回true表示允许访问
* @param servletRequest
* @param servletResponse
* @param o
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o){
return false;
}
/**
* 表示访问拒绝时 是否自己处理 如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理(比如重定向到另一个页面)
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
Subject subject = this.getSubject(servletRequest, servletResponse);
Boolean b = subject.isAuthenticated();
if(!b){
//没有登录,直接进行之后的流程 不进行拦截
return true;
}
//如果有登录,判断是否访问静态资源 如果允许访问静态资源则返回true
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String path = httpServletRequest.getServletPath();
if(isStaticFile(path)){
return true;
}
RedisUtil redisUtil = (RedisUtil) ResourceManage.getBean("redisUtil");
SessionManager sessionManager = (SessionManager) ResourceManage.getBean("sessionManager");
Session session = subject.getSession();
//这里获取的User是实体 因为我在 自定义ShiroRealm中的doGetAuthenticationInfo方法中
//new SimpleAuthenticationInfo(user, password, getName()); 传的是 User实体 所以这里拿到的也是实体,如果传的是userName 这里拿到的就是userName
String username = ((UserVo)subject.getPrincipal()).getUsername();
Serializable sessionId = session.getId();
//初始化用户的队列到缓存
LinkedList<Serializable> deque = null;
String name = getRedisKickoutKey(username);
Object object = redisUtil.get(name);
if(object == null){
deque = new LinkedList<Serializable>();
}else {
deque = JSON.parseObject(JSON.toJSONString(object),LinkedList.class);
}
//如果队列中没有此sessionid,且用户没有被提出 放入队列
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null){
deque.push(sessionId);
}
//如果队列里的session id数量超出最大会话数,开始踢人
while (deque.size() > maxSession){
Serializable kickoutSessionId = null;
if(kickoutAfter){
kickoutSessionId = deque.removeFirst();
}else {
kickoutSessionId = deque.removeLast();
}
try{
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(null != kickoutSession){
kickoutSession.setAttribute("kickout",true);
}
}catch (Exception e){
e.printStackTrace();
}
}
redisUtil.set(getRedisKickoutKey(username),deque);
if(session.getAttribute("kickout") != null){
try {
subject.logout();
}catch (Exception e){
e.printStackTrace();
}
WebUtils.issueRedirect(servletRequest,servletResponse,kickoutUrl);
return false;
}
return true;
}
private boolean isStaticFile(String path){
return false;
}
}
ShiroConfig中添加到链。
/**
* 并发登录控制
* @return
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址
kickoutSessionControlFilter.setKickoutUrl("/a/login");
return kickoutSessionControlFilter;
}
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "securityManager") SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
//访问的是后端url的地址,这里要写base 服务的公用登录接口。 使用toLogin进行拦截,跳转到/a/login //不使用toLogin,整体调转在html页面进行
shiroFilterFactoryBean.setLoginUrl("/a/login");
// 登录成功后要跳转的链接;现在应该没用
//shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权界面;可以写个公用的403页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//自定义拦截器限制并发人数 //限制同一帐号同时在线的个数
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("custom", customUserFilter());
filtersMap.put("kickout",kickoutSessionControlFilter());
//filtersMap.put("toLogin",loginFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
//拦截器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**","anon");
filterChainDefinitionMap.put("/login","anon");
//对 /a/login 不进行拦截
filterChainDefinitionMap.put("/a/login","anon");
//对toLogin进行拦截
//filterChainDefinitionMap.put("/toLogin","toLogin");
//logout是shiro提供的过滤器
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/index","authc");
//filterChainDefinitionMap.put("/a/**","kickout,authc");
filterChainDefinitionMap.put("/a/**","custom,kickout");
filterChainDefinitionMap.put("/**","anon");
//filterChainDefinitionMap.put("/","kickout,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
LOGGER.info("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
在自定义KickoutSessionControlFilter 中使用
Subject subject = this.getSubject(servletRequest, servletResponse);
或者
SecurityUtils.getSubject();获取到的subject都是空的。
同时经过查找ServletRequest里面的 cookie等值也是为空的。
后检查发现在实例化自定义拦截器的时候,在方法上面又加了一个@Bean导致
定义拦截器的时候不需要加@Bean;