spring boot + shiro + redis 整合(完整)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: spring boot + shiro + redis 整合(完整)

什么是shiro

shiro是一个Java平台的开源权限框架,用于认证和访问授权。具体来说,满足对如下元素的支持:

  • 用户,角色,权限(仅仅是操作权限,数据权限必须与业务需求紧密结合),资源(url)
  • 用户分配角色,角色定义权限
  • 访问授权时支持角色或者权限,并且支持多级的权限定义

1.数据库设计

数据库中有分别有6个表,分别是:权限表(permissions)、用户表(users)、用户权限关系表(user_role)、角色表(roles)、角色权限关系表(role_permission)、shiro过滤器表( resource)

数据库下载地址: https://pan.baidu.com/s/1AMKwNfRimPPzqWHY30S1HQ 提取码: r9oh

2.创建springboot项目并在pom加入依赖

  <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 导入web支持:SpringMVC开发支持,Servlet相关的程序 -->
        <!-- web支持,SpringMVC, Servlet支持等  web 启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--导入thymeleaf依赖-->
        <!--thymeleaf启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- jstl -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <!-- jasper -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- fastjson阿里巴巴jSON处理器 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- reids -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- shiro+redis缓存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.6</version>
        </dependency>
        <!-- mysql 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- druid 数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
            <version>2.8.0</version>
        </dependency>
        <!-- thymeleaf整合shiro标签 -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

3. 编辑application.yml

#springboot
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  redis:
    host: localhost
    port: 6379
    jedis:
      pool:
        max-idle: 8
        min-idle: 0
        max-active: 8
        max-wait: -1
    timeout: 0
mybatis:
  mapper-locations: classpath:mapper/*.xml    #*/
  type-aliases-package: com.jbl.springboot_redis_shiro.entity

4.建包搭架子(先把包建完)

4.1创建实体类

/**
 * @Author: yfj
 * @Description: 资源
 * Serializable 要缓存的JavaBean必须实现Serializable接口,因为Spring会将对象先序列化再存入 Redis
 */
@Data
public class Resources implements Serializable {
    public Integer id;
    public String key;
    public  String val;
    public Integer order;
}
/**
 * @Author: yfj
 * @Description: 用户信息
 * Serializable 要缓存的JavaBean必须实现Serializable接口,因为Spring会将对象先序列化再存入 Redis
 */
@Data
public class User implements Serializable {
    public Integer u_id;
    public String u_name;
    public String u_pwd;
}

我用的lombok插件,简化get、set、ToString等方法想学习的可以点击下面链接:

lombok基本使用和安装

4.2springboot启动类

@SpringBootApplication
@MapperScan("com.jbl.springboot_redis_shiro.dao") //这里填的是自己dao层接口的路径
public class SpringbootRedisShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootRedisShiroApplication.class, args);
    }
}

4.3启动springboot自动跳转到登陆页面

/**
 * 启动项目自动跳转到登陆页面
 */
@SpringBootConfiguration
public class AutoStartProjectInDefaultBrowser implements CommandLineRunner {
    @Value("${server.port}")
    private String port;
    @Override
    public void run(String ... args) throws Exception {
        try {
            Runtime.getRuntime().exec("cmd /c start http://localhost:" + port + "/shiro/login");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

4.4全局异常类

/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/16 下午 03:11
 * @description: 全局异常类
 */
@ControllerAdvice
public class CtrlExceptionHandler {
    private static Logger logger = LoggerFactory.getLogger(CtrlExceptionHandler.class);
    //拦截未授权页面
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ExceptionHandler(UnauthorizedException.class)
    public String handleException(UnauthorizedException e) {
        logger.debug(e.getMessage());
        return "403";
    }
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ExceptionHandler(AuthorizationException.class)
    public String handleException2(AuthorizationException e) {
        logger.debug(e.getMessage());
        return "403";
    }
}

5.后台代码

5.1 Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jbl.springboot_redis_shiro.dao.ShiroMapper">
    <select id="login" resultType="User" parameterType="String">
        SELECT `u_id`,`u_name`,`u_pwd` FROM `users`
        WHERE `u_name`= #{name} and `u_pwd` = #{u_pwd}
    </select>
    <select id="seRoles" resultType="String" parameterType="String">
         SELECT r.`r_name` FROM `user_role` ur
        LEFT JOIN `users` u ON(ur.`u_id`=u.`u_id`)
        LEFT JOIN `roles` r ON(ur.`r_id`=r.`r_id`)
        WHERE u.`u_name`=#{userName}
    </select>
    <select id="orders" resultType="Resources">
        SELECT * FROM `resource` ORDER BY `orders`
    </select>
    <select id="quanxian" resultType="String" parameterType="String">
         SELECT  p.`p_name` FROM  `role_permission` rp
        LEFT JOIN `permissions` p ON(rp.`p_id`=p.`p_id`)
        LEFT JOIN `roles` r ON(rp.`r_id`=r.`r_id`)
        WHERE r.`r_name`IN(SELECT r.`r_name` FROM `user_role` ur
        LEFT JOIN `users` u ON(ur.`u_id`=u.`u_id`)
        LEFT JOIN `roles` r ON(ur.`r_id`=r.`r_id`)
        WHERE u.`u_name`=#{userName})
    </select>
</mapper>

5.2 Mapper接口

import com.jbl.springboot_redis_shiro.entity.Resources;
import com.jbl.springboot_redis_shiro.entity.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Set;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 02:40
 * @description:
 */
@Repository
public interface ShiroMapper {
    /**
     * 登陆
     * @param name 用户名
     * @param u_pwd 密码
     * @return
     */
    User login(@Param("name")String name,@Param("u_pwd")String u_pwd);
    /**
     * 查询资源
     * @return List
     */
    List<Resources> orders();
    /**
     * 根据用户查询角色
     * @param userName
     * @return
     */
    Set<String> seRoles(@Param("userName")String userName);
    /**
     * 根据名字查询权限
     * @param userName
     * @return
     */
    Set<String> quanxian(@Param("userName")String userName);
}

5.3 server接口

import com.jbl.springboot_redis_shiro.entity.Resources;
import com.jbl.springboot_redis_shiro.entity.User;
import java.util.List;
import java.util.Set;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 02:42
 * @description:
 */
public interface ShiroServer {
    /**
     * 登陆
     * @return
     */
    User login(String name,String u_Pwd);
    /**
     * 查询资源
     * @return List
     */
    List<Resources> orders();
    /**
     * 根据用户查询角色
     * @param userName
     * @return
     */
    Set<String> seRoles(String userName);
    /**
     * 根据名字查询权限
     * @param userName
     * @return
     */
    Set<String> quanxian(String userName);
}

server实现类

import com.jbl.springboot_redis_shiro.dao.ShiroMapper;
import com.jbl.springboot_redis_shiro.entity.Resources;
import com.jbl.springboot_redis_shiro.entity.User;
import com.jbl.springboot_redis_shiro.server.ShiroServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 02:43
 * @description:
 */
@Service("ShiroServerImpl")
public class ShiroServerImpl implements ShiroServer{
    @Autowired
    private ShiroMapper shiroMapper;
    @Override
    public User login(String name, String u_Pwd) {
        return shiroMapper.login(name,u_Pwd);
    }
    @Override
    public List<Resources> orders() {
        return shiroMapper.orders();
    }
    @Override
    public Set<String> seRoles(String userName) {
        return shiroMapper.seRoles(userName);
    }
    @Override
    public Set<String> quanxian(String userName) {
        return shiroMapper.quanxian(userName);
    }
}

6.创建ShiroConfig类

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.jbl.springboot_redis_shiro.server.ShiroServer;
import org.apache.shiro.mgt.SecurityManager;
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.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 07:06
 * @description: shiro配置
 */
@Configuration
public class ShiroConfig {
    @Autowired
    private ShiroServer shiroServer;
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 没有登陆的用户只能访问登陆页面
        shiroFilterFactoryBean.setLoginUrl("/shiro/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/shiro/index");
        // 未授权界面; ----这个配置了没卵用,具体原因想深入了解的可以自行百度
        shiroFilterFactoryBean.setUnauthorizedUrl("/shiro/wuquan");
        //自定义拦截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        //限制同一帐号同时在线的个数。
        filtersMap.put("kickout", kickoutSessionControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        // 权限控制map.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
/*
        我加了下面这段代码启动就报错,如果有人知道是为什么请联系我,谢谢
        List<Resources> list=shiroServer.orders();
        for(Resources itms:list){
            filterChainDefinitionMap.put(itms.getKey(),itms.getVal());
        }*/
        //添加Shiro内置过滤器
        /**
         * Shiro内置过滤器,可以实现权限相关的拦截器
         *    常用的过滤器:
         *       anon: 无需认证(登录)可以访问
         *       authc: 必须认证才可以访问
         *       user: 如果使用rememberMe的功能可以直接访问
         *       perms: 该资源必须得到资源权限才可以访问
         *       role: 该资源必须得到角色权限才可以访问
         */
        filterChainDefinitionMap.put("/shiro/kickout", "anon");
        filterChainDefinitionMap.put("/shiro/login", "anon");
        filterChainDefinitionMap.put("/shiro/submitLogin", "anon");
        filterChainDefinitionMap.put("/shiro/index", "authc");
        filterChainDefinitionMap.put("/shiro/tianjia", "perms[insert]");
        filterChainDefinitionMap.put("/shiro/update", "perms[update]");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(myShiroRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    /**
     * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }
    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("localhost");
        redisManager.setPort(6379);
        redisManager.setExpire(1800);// 配置缓存过期时间
        redisManager.setTimeout(0);
        // redisManager.setPassword(password);
        return redisManager;
    }
    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }
    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
    /**
     * 限制同一账号登录同时登录人数控制
     *
     * @return
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter() {
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        kickoutSessionControlFilter.setCacheManager(cacheManager());
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        kickoutSessionControlFilter.setKickoutAfter(false);
        kickoutSessionControlFilter.setMaxSession(1);
        kickoutSessionControlFilter.setKickoutUrl("/shiro/kickout");
        return kickoutSessionControlFilter;
    }
    /***
     * 授权所用配置
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    /***
     * 使授权注解起作用不如不想配置可以在pom文件中加入
     * <dependency>
     *<groupId>org.springframework.boot</groupId>
     *<artifactId>spring-boot-starter-aop</artifactId>
     *</dependency>
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * Shiro生命周期处理器
     *
     */
    @Bean
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    /**
     * 配置ShiroDialect,用于thymeleaf和shiro标签配合使用
     */
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }

7.Realm(凭证认证、权限获取)

package com.jbl.springboot_redis_shiro.shiroconfig;
import com.jbl.springboot_redis_shiro.entity.User;
import com.jbl.springboot_redis_shiro.server.ShiroServer;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 07:16
 * @description:
 */
public class MyShiroRealm extends AuthorizingRealm {
    private static org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
    @Autowired
    private ShiroServer shiroServer;
    /**
     * 认证信息.(身份验证) : Authentication 是用来验证用户身份
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        logger.info("---------------- 执行 Shiro 凭证认证 ----------------------");
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        String name = token.getUsername();
        String password = String.valueOf(token.getPassword());
        // 从数据库获取对应用户名密码的用户
        User userList = shiroServer.login(name,password);
        if (userList == null) { //用户名不存在
            return null; //  shiro底层会抛出UnKnoAccountException 异常
        }
        logger.info("---------------- Shiro 凭证认证成功 ----------------------");
        //判断密码
        /**
         * 第一个参数是否返回给controller里的login数据
         * 第二个参数是数据库的密码
         * 第三个参数是名字
         */
        return new SimpleAuthenticationInfo(userList,userList.getU_pwd(),"");
    }
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("---------------- 执行 Shiro 权限获取 ---------------------");
        Object principal = principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        if (principal instanceof User) {
            User userLogin = (User) principal;
            //根据名字查询角色
            Set<String> roles = shiroServer.seRoles(userLogin.getU_name());
            authorizationInfo.addRoles(roles);
            //根据名字查询权限
            Set<String> permissions = shiroServer.quanxian(userLogin.getU_name());
            authorizationInfo.addStringPermissions(permissions);
        }
        logger.info("---- 获取到以下权限 ----");
        logger.info(authorizationInfo.getStringPermissions().toString());
        logger.info("---------------- Shiro 权限获取成功 ----------------------");
        return authorizationInfo;
    }
}

8.限制并发人数登陆

package com.jbl.springboot_redis_shiro.shiroconfig;
import com.alibaba.fastjson.JSON;
import com.jbl.springboot_redis_shiro.entity.User;
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 javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 07:17
 * @description: 限制并发人数登陆
 */
public class KickoutSessionControlFilter extends AccessControlFilter {
    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1
    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;
    }
    //设置Cache的key的前缀
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        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();
        User user = (User) subject.getPrincipal();
        String username = user.getU_name();
        Serializable sessionId = session.getId();
        //读取缓存   没有就存入
        Deque<Serializable> deque = cache.get(username);
        //如果此用户没有session队列,也就是还没有登录过,缓存中没有
        //就new一个空队列,不然deque对象为空,会报空指针
        if(deque==null){
            deque = new LinkedList<Serializable>();
        }
        //如果队列里没有此sessionId,且用户没有被踢出;放入队列
        if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            //将sessionId存入队列
            deque.push(sessionId);
            //将用户的sessionId队列缓存
            cache.put(username, deque);
        }
        //如果队列里的sessionId数超出最大会话数,开始踢人
        while(deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if(kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            }
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if(kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
            Map<String, String> resultMap = new HashMap<String, String>();
            //判断是不是Ajax请求
            if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                resultMap.put("user_status", "300");
                resultMap.put("message", "您已经在其他地方登录,请重新登录!");
                //输出json串
                out(response, resultMap);
            }else{
                //重定向
                WebUtils.issueRedirect(request, response, kickoutUrl);
            }
            return false;
        }
        return true;
    }
    private void out(ServletResponse hresponse, Map<String, String> resultMap)
            throws IOException {
        try {
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.println(JSON.toJSONString(resultMap));
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
        }
    }
}

9.Controller层

import com.jbl.springboot_redis_shiro.server.ShiroServer;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
 * @author yfj
 * @version 1.0
 * @date 2020/04/15 下午 02:37
 * @description: 控制层
 */
@RequestMapping("/shiro")
@Controller
public class ControllerDemo {
    @Autowired
    private ShiroServer shiroServer;
    @RequestMapping("/login")
    public String login(){
        return "login";
    }
    //被踢出后跳转的页面
    @RequestMapping("/kickout")
    public String kickout(){
        return "kickout";
    }
    @RequestMapping("/submitLogin")
    public String submitLogin(String username, String password, Model model) {
        //加密格式
        String hashAlgorithmName="MD5";
        //用户输入的密码
        String credentials =password;
        //盐值
        Object salt = ByteSource.Util.bytes (username) ;
        //加密次数
        int hashIterations = 1024;
        //加密
        Object result =new SimpleHash(hashAlgorithmName,credentials, salt, hashIterations);
        System.out.println(result);
        try {
            //使用shiro 编写认证操作
            UsernamePasswordToken token = new UsernamePasswordToken(username, String.valueOf(result));
            //拿到subject
            Subject subject = SecurityUtils.getSubject();
            // 执行登陆方法
            subject.login(token);
            // 执行到这里说明用户已登录成功
            return "redirect:/shiro/index";
        }catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误");
            return "login";
        }catch (UnknownAccountException e){  //如果有登陆失败就会跳入这里
            model.addAttribute("msg","用户名不存在");   //UnknownAccountException(用户不存在)
            return "login";
        }
    }
    @RequestMapping("/tianjia")
    public String tianjia(){
        return "user/tianjai";
    }
    @RequestMapping("/update")
    public String update(){
        return "user/update";
    }
    @RequestMapping("/index")
    public String index(){
        return "user/index";
    }
    @RequestMapping("/wuquan")
    public String wuquan(){
        return "yichang";
    }
}

10.页面

1.index.html (主页面)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div shiro:hasPermission="insert">
    <a href="/shiro/tianjia">进入用户添加功能</a><br/>
</div>
<div shiro:hasPermission="update">
    <a href="/shiro/update">进入用户修改功能</a><br/>
</div>
<div shiro:hasPermission="select">
    <a>显示</a>
</div>
</body>
</html>

2.tianjii.html(添加页面)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>添加</h1>
</body>
</html>

3.update (修改页面)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
        <h1>修改</h1>
</body>
</html>

4.异常页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>异常页面</h1>
</body>
</html>

5.多台设备登陆此账号提醒页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>多台设备登陆此账号提醒页面</h1>
</body>
</html>

6.登陆页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆页面</title>
</head>
<body>
<font style="color:red"  th:text="${msg}"></font>
<form action="/shiro/submitLogin" method="post">
    用户名:<input type="text" name="username"/><br/>
    密码:<input type="text" name="password"/><br/>
    <input type="submit" value="登陆"/>
</form>
</body>
</html>

7.无权限访问提醒页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>你无权访问这个页面</h1>
</body>
</html>


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
23天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
70 5
|
1月前
|
NoSQL Java API
springboot项目Redis统计在线用户
通过本文的介绍,您可以在Spring Boot项目中使用Redis实现在线用户统计。通过合理配置Redis和实现用户登录、注销及统计逻辑,您可以高效地管理在线用户。希望本文的详细解释和代码示例能帮助您在实际项目中成功应用这一技术。
41 4
|
1月前
|
消息中间件 NoSQL Java
Spring Boot整合Redis
通过Spring Boot整合Redis,可以显著提升应用的性能和响应速度。在本文中,我们详细介绍了如何配置和使用Redis,包括基本的CRUD操作和具有过期时间的值设置方法。希望本文能帮助你在实际项目中高效地整合和使用Redis。
58 2
|
2月前
|
缓存 NoSQL Java
Spring Boot与Redis:整合与实战
【10月更文挑战第15天】本文介绍了如何在Spring Boot项目中整合Redis,通过一个电商商品推荐系统的案例,详细展示了从添加依赖、配置连接信息到创建配置类的具体步骤。实战部分演示了如何利用Redis缓存提高系统响应速度,减少数据库访问压力,从而提升用户体验。
149 2
|
1月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
49 0
|
缓存 负载均衡 NoSQL
shiro 与 redis | 学习笔记
快速学习 shiro 与 redis
124 0
shiro 与 redis | 学习笔记
|
7天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
128 85
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
84 6
|
4天前
|
缓存 监控 NoSQL
Redis经典问题:缓存穿透
本文详细探讨了分布式系统和缓存应用中的经典问题——缓存穿透。缓存穿透是指用户请求的数据在缓存和数据库中都不存在,导致大量请求直接落到数据库上,可能引发数据库崩溃或性能下降。文章介绍了几种有效的解决方案,包括接口层增加校验、缓存空值、使用布隆过滤器、优化数据库查询以及加强监控报警机制。通过这些方法,可以有效缓解缓存穿透对系统的影响,提升系统的稳定性和性能。
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题