背景
二开项目,登录/权限管理用的是shiro,SessionDAO底层依赖的是redis,历史遗留问题是没有做同一个用户登录限制,也就是同一个账号可以同时在N个地方进行登录,留下了很大的安全隐患。
现在的需求就是要实现同一个账户同一时只能一处在线,暂定的方案是后登录的可以强制先登录的下线。
思路
在登录认证,即调用doGetAuthenticationInfo方法的时候,从sessionDAO中获取全部的session,判断一下与当前用户是否为同一账号,如果是,将历史session清除,只保留当前session
实现
ShiroConfig
package com.abc.sv.conf;
import com.abc.sv.shrio.CustomRealm;
import com.abc.sv.shrio.CustomSessionManager;
import com.abc.sv.shrio.CustomWebSecurityManager;
import com.abc.sv.shrio.RedisSessionDao;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.session.mgt.SessionManager;
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.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
@Configuration
public class ShiroConfig {
@Value("${session.redis.expireTime}")
private long expireTime;
@Bean(name = "customRealm")
public CustomRealm customRealm() {
return new CustomRealm();
}
@Bean(name = "redisSessionDao")
public RedisSessionDao redisSessionDao() {
return new RedisSessionDao();
}
@Bean(name = "sessionManager")
public SessionManager sessionManager() {
CustomSessionManager customSessionManager = new CustomSessionManager();
customSessionManager.setSessionIdCookieEnabled(false);
customSessionManager.setSessionDAO(redisSessionDao());
customSessionManager.setGlobalSessionTimeout(expireTime * 1000);
customSessionManager.setSessionValidationSchedulerEnabled(true);
customSessionManager.setDeleteInvalidSessions(true);
return customSessionManager;
}
@Bean(name = "securityManager")
public CustomWebSecurityManager customWebSecurityManager() {
CustomWebSecurityManager securityManager = new CustomWebSecurityManager();
securityManager.setRememberMeManager(null);
securityManager.setRealm(customRealm());
securityManager.setSessionManager(sessionManager());
//暂不开启缓存
//securityManager.setCacheManager(new MemoryConstrainedCacheManager());
return securityManager;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
CustomRealm
package com.abc.sv.shrio;
import cn.hutool.core.collection.CollUtil;
import com.abc.framework.utils.Md5Util;
import com.abc.framework.utils.redis.RedisKeyEnum;
import com.abc.framework.utils.redis.RedisUtils;
import com.abc.sv.constant.UserTypeEnum;
import com.abc.sv.entity.User;
import com.abc.sv.service.management.UserService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
public class CustomRealm extends AuthorizingRealm {
public static final Integer FAILED_LOGIN_TIME = 5;
@Autowired
private RedisUtils redisUtils;
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Long userId = (Long) super.getAvailablePrincipal(principalCollection);
User user = userService.selectByPrimaryKey(userId);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 鉴权逻辑
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
/**
* 认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User user = userService.selectByUserName(username);
if (user == null) {
throw new AccountException("账号或密码有误");
}
if (user.getStatus() != 1) {
throw new AccountException("您的账户已被停用,请向系统管理员咨询");
}
String passwordInToken = new String(token.getPassword());
String passwordInDb = user.getPassword();
String encryptedPwd = Md5Util.encryptPassword(passwordInToken);
String lockTimeValue = redisUtils.get(RedisKeyEnum.USER_LOGIN_FAILED_LOCK.getFormatKey(username));
if (!passwordInDb.equals(encryptedPwd)) {
int lockTime = 1;
if (StringUtils.isNotEmpty(lockTimeValue)) {
lockTime = Integer.parseInt(lockTimeValue) + 1;
}
redisUtils.set(RedisKeyEnum.USER_LOGIN_FAILED_LOCK, lockTime, username);
throw new AccountException("账号或密码有误");
} else {
if (StringUtils.isEmpty(lockTimeValue)) {
redisUtils.delete(RedisKeyEnum.USER_LOGIN_FAILED_LOCK.getFormatKey(username));
}
}
//处理多处登录问题
DefaultSecurityManager securityManager = (DefaultSecurityManager) SecurityUtils.getSecurityManager();
DefaultSessionManager sessionManager = (DefaultSessionManager)securityManager.getSessionManager();
SessionDAO sessionDAO = sessionManager.getSessionDAO();
//获取当前已登录的用户session列表
Collection sessions = sessionDAO.getActiveSessions();
if (CollUtil.isNotEmpty(sessions)) {
for (Session session : sessions) {
//清除该用户以前登录时保存的session
Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
SimplePrincipalCollection coll = (SimplePrincipalCollection) obj;
if (coll != null) {
Long historyUserId = (Long) coll.getPrimaryPrincipal();
if (user.getId().equals(historyUserId)) {
// 强制删除
sessionDAO.delete(session);
}
}
}
}
return new SimpleAuthenticationInfo(user.getId(), passwordInToken, ByteSource.Util.bytes(salt), getName());
}
}
RedisSessionDao
package com.abc.sv.shrio;
import cn.hutool.core.util.ObjectUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class RedisSessionDao extends AbstractSessionDAO {
@Value("${session.redis.expireTime}")
private long expireTime;
private final String SESSION_PREFIX = "session:";
@Autowired
private RedisTemplate redisTemplate;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
String key = SESSION_PREFIX + session.getId();
redisTemplate.opsForValue().set(key, session, expireTime, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = null;
if (ObjectUtil.isNotNull(sessionId)){
String key = SESSION_PREFIX + sessionId;
Object o = redisTemplate.opsForValue().get(key);
if (ObjectUtil.isNotNull(o)){
session = (Session)o;
}
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
if (session != null && session.getId() != null) {
session.setTimeout(expireTime * 1000);
String key = SESSION_PREFIX + session.getId();
redisTemplate.opsForValue().set(key, session, expireTime, TimeUnit.SECONDS);
}
}
@Override
public void delete(Session session) {
if (session != null && session.getId() != null) {
String key = SESSION_PREFIX + session.getId();
redisTemplate.opsForValue().getOperations().delete(key);
}
}
@Override
public Collection getActiveSessions() {
Set keys = redisTemplate.keys(SESSION_PREFIX + "*");
if (keys != null) {
return keys.stream().map(o -> (Session) redisTemplate.opsForValue().get(o)).collect(Collectors.toSet());
} else {
return null;
}
}
}
RedisConfig
package com.abc.sv.service.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.yhdja.framework.utils.redis.RedisUtils;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
@Configuration
public class RedisConfig {
@Bean
@Primary
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//!!!默认JdkSerializationRedisSerializer ,increment操作不会进行序列化,get会出错!!!需要设置反序列出来的类型
redisTemplate.setValueSerializer(new CollectionSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
public RedisUtils redisUtils(StringRedisTemplate stringRedisTemplate) {
return new RedisUtils(stringRedisTemplate);
}
}
CollectionSerializer
package com.abc.sv.service.config;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
public class CollectionSerializer implements RedisSerializer{
CollectionSerializer(){}
public static volatile CollectionSerializer collectionSerializer=null;
public static CollectionSerializer getInstance(){
if(collectionSerializer==null){
synchronized (CollectionSerializer.class) {
if(collectionSerializer==null){
collectionSerializer=new CollectionSerializer();
}
}
}
return collectionSerializer;
}
@Override
public byte[] serialize(T t) throws SerializationException {
return SerializationUtils.serialize(t);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (ArrayUtils.isNotEmpty(bytes)){
return SerializationUtils.deserialize(bytes);
}
return null;
}
}
遇到的坑
redis序列化方式没设置好,导致sessionDAO.getActiveSessions()取不到数据
测试
正常登录
换个浏览器,再次登录
回到首次登录的页面,刷新