shiro_实现分布式会话SessionManager、限制密码重试次数和并发登录控制

shiro_实现分布式会话SessionManager、限制密码重试次数和并发登录控制

要解决的问题

解决方案

所有服务器的Session都存储到redis服务器中,通过redis实现Session共享。

QL(B6.pngQL(B6.png))

首先,继承AbstractSessionDAO并实现

java 复制代码
package com.itheima.shiro.core.impl;

import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.utils.ShiroRedissionSerialize;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @Description:自定义统一sessiondao实现
 */
public class RedisSessionDao extends AbstractSessionDAO {

    @Resource(name = "redissonClientForShiro")
    RedissonClient redissonClient;

    private long globalSessionTimeout;

    /**
     * @Description 创建session
     * @param session 会话对象
     * @return
     */
    @Override
    protected Serializable doCreate(Session session) {
        //创建唯一标识的sessionId
        Serializable sessionId = generateSessionId(session);
        //为session会话指定唯一的sessionId
        assignSessionId(session, sessionId);
        //放入缓存中
        String key = CacheConstant.GROUP_CAS+sessionId.toString();
        RBucket<String> bucket = redissonClient.getBucket(key);
        bucket.trySet(ShiroRedissionSerialize.serialize(session), globalSessionTimeout/1000, TimeUnit.SECONDS);
        return sessionId;
    }

    /**
     * @Description 读取sessio
     * @param sessionId 唯一标识
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        String key = CacheConstant.GROUP_CAS+sessionId.toString();
        RBucket<String> bucket = redissonClient.getBucket(key);
        return (Session) ShiroRedissionSerialize.deserialize(bucket.get());
    }

    /**
     * @Description 更新session
     * @param session 对象
     * @return
     */
    @Override
    public void update(Session session) throws UnknownSessionException {
        String key = CacheConstant.GROUP_CAS+session.getId().toString();
        RBucket<String> bucket = redissonClient.getBucket(key);
        bucket.set(ShiroRedissionSerialize.serialize(session), globalSessionTimeout/1000, TimeUnit.SECONDS);
    }

    /**
     * @Description 删除session
     * @param
     * @return
     */
    @Override
    public void delete(Session session) {
        String key = CacheConstant.GROUP_CAS+session.getId().toString();
        RBucket<String> bucket = redissonClient.getBucket(key);
        bucket.delete();
    }

    /**
     * @Description 统计当前活跃用户数(暂不实现)
     * @param
     * @return
     */
    @Override
    public Collection<Session> getActiveSessions() {
        return Collections.emptySortedSet();
    }

    public void setGlobalSessionTimeout(long globalSessionTimeout) {
        this.globalSessionTimeout = globalSessionTimeout;
    }
}

在shiroConfig中配置会话管理器

java 复制代码
/**
 * @Description 会话管理器
 */
@Bean(name="sessionManager")
public DefaultWebSessionManager shiroSessionManager(){
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    // 指定自定义的SessionDao
    sessionManager.setSessionDAO(redisSessionDao());
    sessionManager.setSessionValidationSchedulerEnabled(false);
    sessionManager.setSessionIdCookieEnabled(true);
    sessionManager.setSessionIdCookie(simpleCookie());
    sessionManager.setGlobalSessionTimeout(shiroRedisProperties.getGlobalSessionTimeout());
    return sessionManager;
}

  /**
     *  配置Session的超时时间
     * @return
     */
    @Bean("redisSessionDao")
    public RedisSessionDao redisSessionDao(){
        RedisSessionDao redisSessionDao = new RedisSessionDao();
        redisSessionDao.setGlobalSessionTimeout(shiroRedisProperties.getGlobalSessionTimeout());
        return redisSessionDao;
    }

限制密码重试次数

  1. 获取系统中是否已有登录次数缓存,缓存对象结构预期为:用户名---登录次数
  2. 如果没有登录缓存,则创建一个登录次数缓存
  3. 如果缓存次数已超过限制,则驳回本次登录请求
  4. 将缓存记录的登录次数加1,设定指定时间内有效
  5. 验证用户本次输入的账号密码,如果登录成功,则清除登录次数缓存

自定义密码比较器

java 复制代码
package com.itheima.shiro.core.impl;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;

/**
 * @Description:自定义密码比较器
 */
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {

    private RedissonClient redissonClient;

    // 限制登录5次
    private static Long RETRY_LIMIT_NUM = 4L;

    public RetryLimitCredentialsMatcher(String hashAlgorithmName,RedissonClient redissonClient) {
        super(hashAlgorithmName);
        this.redissonClient = redissonClient;
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String loginName = (String) token.getPrincipal();
        // 1、获取系统中是否已有登录次数缓存,缓存对象结构预期为:"用户名--登录次数"。
        RAtomicLong atomicLong = redissonClient.getAtomicLong(loginName);
        //2、获取登录次数,如果之前没有登录缓存,默认为0。
        long retryFlat = atomicLong.get();
        //判断是否超过次数
        if (retryFlat>RETRY_LIMIT_NUM){
            //3、如果缓存次数已经超过限制,则驳回本次登录请求。
            // 10分钟后过期
            atomicLong.expire(10, TimeUnit.MINUTES);
            // 这里抛出异常需要前端进行处理
            throw new ExcessiveAttemptsException("密码次数错误5次,请10分钟后重试");
        }
        //4、将缓存记录的登录次数加1,设置指定时间内有效
        atomicLong.incrementAndGet();
        atomicLong.expire(10, TimeUnit.MINUTES);
        //5、验证用户本次输入的帐号密码,如果登录登录成功,则清除掉登录次数的缓存
        boolean flag = super.doCredentialsMatch(token, info);
        if (flag){
            atomicLong.delete();
        }
        return flag;
    }
}

使用自定义密码比较器

修改ShiroDbRealmImpl类中的initCredentialsMatcher方法

java 复制代码
@Override
public void initCredentialsMatcher() {
    //指定密码算法
    RetryLimitCredentialsMatcher hashedCredentialsMatcher = new RetryLimitCredentialsMatcher(SuperConstant.HASH_ALGORITHM,redissonClient);
    //指定迭代次数
    hashedCredentialsMatcher.setHashIterations(SuperConstant.HASH_INTERATIONS);
    //生效密码比较器
    setCredentialsMatcher(hashedCredentialsMatcher);
}

在LoginAction中处理异常

java 复制代码
/**
 * @Description 登录操作
 * @param loginVo 登录对象
 * @return
 */
@RequestMapping(value="/usersLongin",method=RequestMethod.POST)
public ModelAndView usersLongin(LoginVo loginVo){
    ModelAndView modelAndView = new ModelAndView("/account/login");
    String shiroLoginFailure=null;
    Map<String, String> map = new HashMap<String, String>();
    try {
       loginVo.setSystemCode(ShiroConstant.PLATFORM_MGT);
       loginService.route(loginVo);
    } catch (UnknownAccountException ex) {
       log.error("登陆异常:{}",ex);
       shiroLoginFailure="登录失败 - 账号不存在!";
       map.put("loginName", loginVo.getLoginName());
       map.put("shiroLoginFailure", shiroLoginFailure);
       modelAndView.addAllObjects(map);
    } catch (IncorrectCredentialsException ex) {
       log.error("登陆异常:{}",ex);
       shiroLoginFailure="登录失败 - 密码不正确!";
       map.put("shiroLoginFailure", shiroLoginFailure);
       map.put("loginName", loginVo.getLoginName());
       modelAndView.addAllObjects(map);
    } catch (ExcessiveAttemptsException ex) {
        // 登录次数异常
       log.error("登陆异常:{}",ex);
       shiroLoginFailure=ex.getMessage();
       map.put("shiroLoginFailure", shiroLoginFailure);
       map.put("loginName", loginVo.getLoginName());
       modelAndView.addAllObjects(map);
    }catch (Exception ex) {
       log.error("登陆异常:{}",ex);
       shiroLoginFailure="登录失败 - 请联系平台管理员!";
       map.put("shiroLoginFailure", shiroLoginFailure);
       map.put("loginName", loginVo.getLoginName());
       modelAndView.addAllObjects(map);
    }
    if (shiroLoginFailure!=null) {
       return modelAndView;
    }
    modelAndView.setViewName("redirect:/menus/system");
    return modelAndView;
}

在线账户控制

一个账号只允许同时一个在线,当账号在其他地方登录时,会踢出前面登录的账号。

实现思路:

  1. 先判断用户是否登录
  2. 使用RedissionClient创建队列
  3. 判断当前SessionID是否在此队列
  4. 不存在则放入队列尾端
  5. 存在则判断队列大小是否超过同时在线人数
  6. 如果超过:
    • 从队列头部拿到用户SessionID
    • 从SessionManager根据SessionID获得Session
    • 从SessionDao中移除Session
  7. 未超过不执行操作

自定义过滤器

java 复制代码
package com.itheima.shiro.core.filter;

import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.core.impl.RedisSessionDao;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.utils.ShiroUserUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.redisson.api.RDeque;
import org.redisson.api.RedissonClient;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * @Description:自定义踢出过滤器
 */
@Log4j2
public class KickedOutAuthorizationFilter extends AccessControlFilter {

    private RedissonClient redissonClient;

    private RedisSessionDao redisSessionDao;

    private DefaultWebSessionManager defaultWebSessionManager;

    public KickedOutAuthorizationFilter(RedissonClient redissonClient, RedisSessionDao redisSessionDao, DefaultWebSessionManager defaultWebSessionManager) {
        this.redissonClient = redissonClient;
        this.redisSessionDao = redisSessionDao;
        this.defaultWebSessionManager = defaultWebSessionManager;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //1、只针对登录用户处理,首先判断是否登录
        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated()){
            return true;
        }
        //2、使用RedissionClien创建队列
        String sessionId = ShiroUserUtil.getShiroSessionId();
        String logiName = ShiroUserUtil.getShiroUser().getLoginName();
        //2.1当前用户的队列
        RDeque<String> deque = redissonClient.getDeque(CacheConstant.GROUP_CAS + "kickedOutAuthorizationFilter:" + logiName);
        //3、判断当前sessionId是否存在于此用户的队列=key:登录名 value:多个sessionId
        boolean flag = deque.contains(sessionId);
        //4、不存在则放入队列尾端==>存入sessionId
        if (!flag){
            deque.addLast(sessionId);
        }
        //5、判断当前队列大小是否超过限定此账号的可在线人数
        if (deque.size()>1){
            //6、超过:
            //*从队列头部拿到用户sessionId
            //*从sessionManger根据sessionId拿到session
            //*从sessionDao中移除session会话
            sessionId = deque.getFirst();
            deque.removeFirst();
            Session session = null;
            try {
                session = defaultWebSessionManager.getSession(new DefaultSessionKey(sessionId));
            }catch (UnknownSessionException ex){
                log.info("session已经失效");
            }catch (ExpiredSessionException expiredSessionException){
                log.info("session已经过期");
            }
            if (!EmptyUtil.isNullOrEmpty(session)){
                // 删掉Session
                redisSessionDao.delete(session);
            }
        }

        //7、未超过:放过操作
        return true;
    }
}

在ShiroConfig配置过滤器

java 复制代码
/**
 * @Description 自定义拦截器定义
 */
private Map<String, Filter> filters() {
    Map<String, Filter> map = new HashMap<String, Filter>();
    map.put("role-or", new RolesOrAuthorizationFilter());
    // 装载过滤器
    map.put("kicked-out", new KickedOutAuthorizationFilter(redissonClient(), redisSessionDao(), shiroSessionManager()));
    return map;
}

在authentication.properties中配置

properties 复制代码
/static/**=anon
#登录链接不拦截
/login/**=anon
#访问/resource/**需要有admin的角色
/resource/**=role-or[MangerRole,SuperAdmin]
#其他链接是需要登录的
/**=kicked-out,authc
相关推荐
Seven972 小时前
剑指offer-63、数据流中的中位数
java
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Spring Boot的社区养老服务管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
mjhcsp2 小时前
C++ Manacher 算法:原理、实现与应用全解析
java·c++·算法·manacher 算法
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-企业级软件研发工程应用规范案例
java·运维·spring boot·软件工程·devops
indexsunny2 小时前
互联网大厂Java面试实战:微服务、Spring Boot与Kafka在电商场景中的应用
java·spring boot·微服务·面试·kafka·电商
SUDO-12 小时前
Spring Boot + Vue 2 的企业级 SaaS 多租户招聘管理系统
java·spring boot·求职招聘·sass
sheji34162 小时前
【开题答辩全过程】以 基于spring boot的停车管理系统为例,包含答辩的问题和答案
java·spring boot·后端
重生之后端学习3 小时前
21. 合并两个有序链表
java·算法·leetcode·链表·职场和发展
南屿欣风3 小时前
Sentinel 熔断规则 - 异常比例(order & product 示例)笔记
java·开发语言