Shiro+Redis控制用户并发登录

前言

最近些项目时需要控制用户同一时间在线登陆的人数,所以就从网上找了一些相关代码学习学习。不料各种方法使我学的头昏眼花,最后索性自己造一个简易版的算球。以下是这个过滤器的具体写法。

思路

用户登录时在redis中存入一个值(这个值可以是sessionid,也可以是IP地址转成的密文,目的是验证用户是否是在同一设备或者同一环境登录。或者直接使用redis管理session,然后用session验证也是可以的),跟session的更新思路相同,在用户操作时更新该值的过期时间。设置一个过滤器,判断当前用户的环境是否与redis中存储的值相同,如果不同,则使当前用户登出。
以下使具体实现:

过滤器设置

@Slf4j
public class KickoutSessionFilter extends AccessControlFilter {

    // 使用RedisCacheManager 存储的cache前缀名
    public static String ONLINE_USER = "online_user";
    private RedisTemplate<String , Object> redisTemplate;

    public void setStringRedisTemplate(RedisTemplate<String , Object> redisTemplate){
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        return false;
    }

    
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = SecurityUtils.getSubject();
        //如果用户未登录,跳过此过程
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            return true;
        }

        String username = (String) subject.getPrincipal();
        Serializable id = subject.getSession().getId();

        String s = (String) redisTemplate.opsForValue().get("online_user:" + username);
        log.info(username);

        if(s == null){
            return true;
        }
        if(!s.equals(id.toString())){
            subject.logout();
            response.setContentType("application/json; charset=utf-8");

            Result result = new Result(Code.FORBIDDEN, "您已在别处登录,请重新登录!");
            String json = JSONUtil.toJsonStr(result);

            response.getWriter().write(json);
            return false;
        }else{
            return true;
        }
    }

}

ShiroConfig(配置过滤器)

@Slf4j
@Configuration
public class ShiroConfiguration {

    @Autowired
    private DefinitionRealm realm;
    @Autowired
    private RedisTemplate<String , Object> redisTemplate;


    @Bean(name = {"shiroFilterFactoryBean"})
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(securityManager);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("authc", new ShiroLoginFilter());
        filters.put("roles", new ShiroRoleFilter());
        filters.put("kickout" , kickoutSessionFilter());
        bean.setFilters(filters);

        //设置内置过滤器
        //拦截
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/sign/signout", "logout");
        filterChainDefinitionMap.put("/sign/signin", "anon");
        filterChainDefinitionMap.put("/pwd/updatePwd", "anon");
        filterChainDefinitionMap.put("/code/sendEmailCode", "anon");
        filterChainDefinitionMap.put("/code/sendSmsCode", "anon");
        filterChainDefinitionMap.put("/teacher/autoMatching","anon");
        filterChainDefinitionMap.put("/code/*", "kickout ,authc");
        filterChainDefinitionMap.put("/search/**","kickout ,authc");
        filterChainDefinitionMap.put("/student/**", "kickout ,authc,roles[student]");
        filterChainDefinitionMap.put("/teacher/**", "kickout ,authc,roles[teacher]");
        filterChainDefinitionMap.put("/admin/**", "kickout ,authc,roles[admin]");

        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return bean;
    }

    //Coolie设置
    public SimpleCookie rememberMeCookie() {
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        //设置跨域
        //cookie.setDomain(domain);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30 * 24 * 60 * 60);
        return cookie;
    }

    @Bean
    //创建Shiro的cookie管理对象
    //设置rememberMe管理器
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        cookieRememberMeManager.setCipherKey("123456".getBytes(StandardCharsets.UTF_8));
        return cookieRememberMeManager;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //设置加密编码器
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        //设置加密方法
        matcher.setHashAlgorithmName(HASH_ALGORITHM_NAME);
        //设置加密次数
        matcher.setHashIterations(HASH_ITERATORS);

        //开启全局缓存
        realm.setCachingEnabled(true);
        //开启认证缓存,指定缓存名称
        realm.setAuthenticationCachingEnabled(true);
        realm.setAuthenticationCacheName("authenticationCache");
        //开启授权缓存,指定缓存名称
        realm.setAuthorizationCachingEnabled(true);
        realm.setAuthorizationCacheName("authorizationCache");
        //将加密对象存到realm中
        realm.setCredentialsMatcher(matcher);
        //给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    @Bean
    public KickoutSessionFilter kickoutSessionFilter(){
        KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
        kickoutSessionFilter.setStringRedisTemplate(redisTemplate);
        return kickoutSessionFilter;
    }

}

登录(重点看更新redis的那一行)

@Service
public class SignServiceImpl implements SignService {

    @Autowired
    private StudentMapper studentMapper;
    @Autowired
    private TeacherMapper teacherMapper;
    @Autowired
    private RedisTemplate<String ,Object> redisTemplate;


    @Override
    public Result signIn(String uId, String pwd, boolean rememberMe ,HttpSession session) {
        //生成token
        AuthenticationToken token = new UsernamePasswordToken(uId , pwd , rememberMe);
        //获得subject
        Subject subject = SecurityUtils.getSubject();
        //执行登陆操作
        subject.login(token);
        //将登录信息传入redis
        redisTemplate.opsForValue().set("online_user:"+uId , session.getId());
        return new Result();
        }
    }

    @Override
    public Result signOut() {
        //获得subject
        Subject subject = SecurityUtils.getSubject();
        //登出
        subject.logout();
        return new Result(
                Code.OK ,
                "sign out success!" ,
                null
        );
    }
}

Realm(这里是为了更新过期时间)

@Component
public class DefinitionRealm extends AuthorizingRealm {
    @Autowired
    private StudentService studentService;
    @Autowired
    private TeacherService teacherService;
    @Autowired
    private RedisTemplate<String , Object> redisTemplate;

    /**
     * 授权方法
     * */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("鉴权发生");
        //获得主身份信息
        //根据用户名获取当前用户的角色信息、权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //获取角色
        //如果角色为学生,给角色赋予学生权限
        Student studentByStuId = studentService.getStudentByStuId((String) principalCollection.getPrimaryPrincipal());
        if(studentByStuId!=null){
            simpleAuthorizationInfo.addRole(STUDENT);
        }
        //如果角色为教师,给角色赋予教师权限
        Teacher teacherByJobId = teacherService.getTeacherByJobId((String) principalCollection.getPrimaryPrincipal());
        if(teacherByJobId!=null){
            if(teacherByJobId.getStatus()==1){
                simpleAuthorizationInfo.addRole(ADMIN);
            }else {
                simpleAuthorizationInfo.addRole(TEACHER);
            }
        }
		
		redisTemplate.expire("online_user:"+principalCollection.getPrimaryPrincipal() , Duration.ofSeconds(60 * 30));//更新过期时间
        //返回角色权限
        return simpleAuthorizationInfo;
    }

    /**
     * 认证方法
     * */
    @Override
    protected  AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken){
        //获得用户名
        String principal = (String) authToken.getPrincipal();
        //获取学生身份信息
        Student student = studentService.getStudentByStuId(principal);
        if(student!=null){
            return new SimpleAuthenticationInfo(
                    principal,
                    student.getPassword(),
                    ByteSourceUtil.bytes(student.getSalt()),
                    principal
            );
        }
        //获取教师身份信息
        Teacher teacher = teacherService.getTeacherByJobId(principal);
        if(teacher!=null){
            return new SimpleAuthenticationInfo(
                    principal,
                    teacher.getPassword(),
                    ByteSourceUtil.bytes(teacher.getSalt()),
                    principal
            );
        }
        redisTemplate.expire("online_user:"+principal , Duration.ofSeconds(60 * 30));
        return null;
    }

    @Override
    protected void doClearCache(PrincipalCollection principals) {
        super.doClearCache(principals);
    }
}

测试

在Edge中使用swagger登录:
在这里插入图片描述
在这里插入图片描述
登陆成功,redis中出现值
使用ie登录:
在这里插入图片描述

在这里插入图片描述
登陆成功,redis中的值更新,此时再返回edge进行操作
在这里插入图片描述
返回值与预期结果相同!过滤器成功执行。

该过滤器的进阶版

学习了其他大佬的思想,其实redis中可以存储一个队列,通过控制队列内容的数量来控制同时在线的人数。但这个项目比较简单,所以写成了同时在线的用户只能有一个,感兴趣的可以去学习学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值