springboot+shiro控制同一用户在线的并发数,同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录
具体实现类粘贴到了下方,有需要的直接复制使用即可
ShiroConfig 配置类
package com.XXX.web.config.auth;
import com.XXX.web.login.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpStatus;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
* 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
* 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* HashedCredentialsMatcher,这个类是为了对密码进行编码的,
* 防止密码在数据库里明码保存,当然在登陆认证的时候,
* 这个类也负责对form里输入的密码进行编码。
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* UserRealm,这是个自定义的认证类,继承自AuthorizingRealm,
* 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
*/
@Bean(name = "userRealm")
@DependsOn("lifecycleBeanPostProcessor")
public UserRealm userRealm() {
UserRealm realm = new UserRealm();
realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
/**
* 设置session失效时间
*
* @return
*/
@Bean(name = "sessionManager")
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间3600s
sessionManager.setGlobalSessionTimeout(3600000L);
return sessionManager;
}
/**
* 自定义缓存管理器
*
* @return
*/
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
return cacheManager;
}
/**
* SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(ehCacheManager());
ThreadContext.bind(securityManager);
return securityManager;
}
/**
* ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
* 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager());
//Shiro登出后转发到登录页面
Map<String, Filter> filtersDefinition = new LinkedHashMap<>();
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/login");
filtersDefinition.put("logout", logoutFilter);
filtersDefinition.put("authc", new ShiroLoginFilter(HttpStatus.UNAUTHORIZED.value())); //自定义状态码
filtersDefinition.put("kickout", kickoutSessionControlFilter()); // 限制同一帐号同时在线的个数
shiroFilterFactoryBean.setFilters(filtersDefinition);
/**
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* roles: 该资源必须得到角色权限才可以访问
*/
Map<String, String> filterChainDefinitionManager = new LinkedHashMap<>();
filterChainDefinitionManager.put("/logout", "logout");
filterChainDefinitionManager.put("/XXX/api/**", "kickout,authc");
filterChainDefinitionManager.put("/XXX/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
return shiroFilterFactoryBean;
}
/**
* 并发登录控制
*
* @return
*/
@Bean(name = "kickoutSessionControlFilter")
public KickoutSessionFilter kickoutSessionControlFilter() {
KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
// 用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionFilter.setSessionManager(sessionManager());
// 使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionFilter.setCacheManager(ehCacheManager());
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionFilter.setKickoutAfter(false);
// 同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionFilter.setMaxSession(1);
// 被踢出后重定向到的地址;
kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
return kickoutSessionFilter;
}
/**
* DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/**
* AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
* 内部使用Aop AllianceAnnotationsAuthorizing MethodInterceptor来拦截用以下注解的方法。
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
aASA.setSecurityManager(securityManager());
return aASA;
}
}
UserRealm 类
package com.XXX.web.login;
import com.XXX.web.entity.user.UserEntity;
import com.XXX.web.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import javax.annotation.Resource;
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Resource
private UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("执行了=>授权doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("执行了=>认证doGetAuthenticationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
UserEntity user = userService.queryUserByName(userToken.getUsername()); //认证用户名和密码 数据库中取
//此时return null就会抛出异常 UnknownAccountException
if (user == null) {return null;}
/**
* 验证密码,我们可以使用一个AuthenticationInfo实现类 SimpleAuthenticationInfo
* 密码认证,shiro帮你做,只需要像下面把正确的密码丢进去
* shiro会自动帮我们验证!重点是第二个参数就是要验证的密码!
*/
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
}
}
ShiroLoginFilter 类,登录拦截器
package com.XXX.web.config.auth;
import com.alibaba.fastjson.JSON;
import com.XXX.web.common.Response;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.http.MediaType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ShiroLoginFilter extends FormAuthenticationFilter {
private Integer code;
public ShiroLoginFilter(Integer code) {
this.code = code;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setStatus(code);
httpServletResponse.getWriter().write(JSON.toJSONString(Response.of("User not logged in", code, "用户未登录,请先进行身份验证!")));
return false;
}
}
踢出用户类
import com.alibaba.fastjson.JSON;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
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.util.WebUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
@Configuration
public class KickoutSessionFilter extends AccessControlFilter {
/*前端需要加的代码
<script>
var href=location.href;
if(href.indexOf("kickout")>0){
layer.msg("您的账号在另一台设备上登录,如非本人操作,请立即修改密码!",{offset: '100px'});
layer.open({
type: 0,
title :"下线提示",
closeBtn: 1,
anim: 6,
content: '您的账号在另一台设备上登录,如非本人操作,请立即修改密码!'
});
}
</script>
*/
/**
* 踢出后到的地址
*/
private String kickoutUrl;
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickoutAfter = false;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession = 1;
/**
* session管理器
*/
private SessionManager sessionManager;
/**
* 缓存管理器
*/
private Cache<String, Deque<Serializable>> cache;
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 setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-activeSessionCache");
}
/**
* 是否允许访问
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated() && !subject.isRemembered()) {
// 如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
String username = (String) SecurityUtils.getSubject().getPrincipal();
Serializable sessionId = session.getId();
// 同步控制
Deque<Serializable> deque = cache.get(username);
if (deque == null) {
deque = new LinkedList<>();
cache.put(username, deque);
}
// 如果队列里没有此sessionId,且用户没有被踢出;放入队列
if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
// 如果队列里的sessionId数超出最大会话数,开始踢人
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 (kickoutSession != null) {
// 设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
// 会话被踢出了
try {
subject.logout();
} catch (Exception e) {
e.printStackTrace();
}
saveRequest(request);
HttpServletRequest httpRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 如果是ajax请求
if (isAjax(httpRequest)) {
// 使得http会话过期
httpServletResponse.sendError(0);
} else {
// httpServletResponse.setCharacterEncoding("UTF-8");
// httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
// httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
// httpServletResponse.getWriter().write(JSON.toJSONString(Response.of("The user is already logged in on another device", HttpStatus.UNAUTHORIZED.value(), "您的账号在另一台设备上登录,如非本人操作,请立即修改密码!")));
WebUtils.issueRedirect(request, response, kickoutUrl);
}
return false;
}
return true;
}
/**
* 判断是否为ajax请求
*
* @param request
* @return boolean对象
*/
public static boolean isAjax(ServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"));
}
}
文章介绍了如何在SpringBoot项目中使用Shiro框架进行用户认证和权限管理,并特别关注了如何配置和实现限制同一用户并发登录的数量,包括创建ShiroFilterFactoryBean、自定义Realm、配置SessionManager、设置踢出策略等步骤。
7011

被折叠的 条评论
为什么被折叠?



