SpringBoot+Shiro+Redis整合以及实现记住我(RememberMe)功能

前言:

Shiro中本身就提供了sessionManager和sessionDAO,我们可以把shiro和redis集成起来,把session持久化到Redis中,需要使用的时候从Redis中可以获取对应的session。

本章介绍如下几个功能:

1.当用户没有登陆时只能访问登陆界面

2.当用户登陆成功后,只能访问该用户下仅有的权限

3.记住登录用户(rememberMe)

4.一个账号可以多人同时登录

说明:本章案例做了简化,仅作为springboot+shiro+redis项目整合为参考,适合入门使用,亲测有效。

 

一、数据库设计

表设计思路:用户对应角色,角色包含拥有的菜单和其他权限,菜单也对应着某个权限,说明有这个菜单就有对应的权限(权限表包含菜单ID),权限表里不设置菜单ID就是其他权限。

 1.SQL

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50712
 Source Host           : localhost:3306
 Source Schema         : boot_shiro_redis

 Target Server Type    : MySQL
 Target Server Version : 50712
 File Encoding         : 65001

 Date: 10/03/2020 17:09:41
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for manage_menu
-- ----------------------------
DROP TABLE IF EXISTS `manage_menu`;
CREATE TABLE `manage_menu`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '路径名称',
  `ICON` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图标class(el)',
  `URL` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路径地址',
  `PARENT_ID` int(11) NULL DEFAULT NULL COMMENT '父节点ID,父节点同样在本目录下',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单管理表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of manage_menu
-- ----------------------------
INSERT INTO `manage_menu` VALUES (1, '首页', 'el-icon-s-home', '/index', NULL);
INSERT INTO `manage_menu` VALUES (2, '权限管理', 'fa fa-book', '/managePermission/getPermissionAll', NULL);
INSERT INTO `manage_menu` VALUES (3, '人员管理', 'fa fa-book', '/manageUser/getUserAll', 2);

-- ----------------------------
-- Table structure for manage_permission
-- ----------------------------
DROP TABLE IF EXISTS `manage_permission`;
CREATE TABLE `manage_permission`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限名称',
  `RESOURCE` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '资源地址',
  `SN` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '描述',
  `MENU_ID` int(11) NULL DEFAULT NULL COMMENT '菜单表中的ID',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '路径权限' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of manage_permission
-- ----------------------------
INSERT INTO `manage_permission` VALUES (1, '查看所有权限', '/managePermission/getPermissionAll', 'managePermission:list', 3);
INSERT INTO `manage_permission` VALUES (2, '查看所有人员', '/manageUser/getUserAll', 'manageUser:list', NULL);

-- ----------------------------
-- Table structure for manage_roles
-- ----------------------------
DROP TABLE IF EXISTS `manage_roles`;
CREATE TABLE `manage_roles`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名',
  `MENUS_ID` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单的ID(多个菜单由逗号分隔)',
  `PERMISSIONS_ID` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '其余权限的ID(多个权限由逗号分隔)',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色权限' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of manage_roles
-- ----------------------------
INSERT INTO `manage_roles` VALUES (1, '管理员', '1,2,3', '3');
INSERT INTO `manage_roles` VALUES (2, '普通用户', '1', NULL);

-- ----------------------------
-- Table structure for manage_user
-- ----------------------------
DROP TABLE IF EXISTS `manage_user`;
CREATE TABLE `manage_user`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '管理员ID',
  `USERNAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '登陆名',
  `PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `ROLE_ID` int(11) NOT NULL DEFAULT 0 COMMENT '对应的角色Id',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '登录用户' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of manage_user
-- ----------------------------
INSERT INTO `manage_user` VALUES (1, 'admin', '4ec847db9bc2bad60e4279cce1fad5db', 1);
INSERT INTO `manage_user` VALUES (4, 'user', '4e0374eaa5fd58d90a549cac95a657ab', 2);

SET FOREIGN_KEY_CHECKS = 1;
View Code

 

二、pom.xml添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--mysql数据库连接驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>

        <!--lombok代码简化工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.6</version>
        </dependency>

        <!-- shiro spring. -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--使用的是shiro-redis开源插件-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.2.1-RELEASE</version>
            <exclusions>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>jedis</artifactId>
                    <groupId>redis.clients</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--thymeleaf 页面模板依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
View Code

 

三、application.yml配置redis

spring
    redis:
      database: 1
      host: 127.0.0.1
      port: 6379
      password:       # 密码(默认为空)
      timeout: 6000  # 连接超时时长(毫秒)
      jedis:
        pool:
          max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
          max-wait: -1     # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-idle: 10      # 连接池中的最大空闲连接
          min-idle: 5       # 连接池中的最小空闲连接
View Code

 

四、添加配置类

1.shiro配置类

package com.example.demo.config;

import com.example.demo.shiro.MySessionManager;
import com.example.demo.shiro.realms.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
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.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author:
 * @since:
 * @description:
 */
@Configuration
public class ShiroConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.timeout}")
    private Integer timeout;
    @Value("${spring.redis.password}")
    private String password;

    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean() {
        
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager());// 必须设置 SecurityManager安全管理器

        bean.setSuccessUrl("/index");
        bean.setUnauthorizedUrl("/unauthorizedurl");

        Map<String, String> map = new LinkedHashMap<>();

        //匿名使用
        map.put("/login", "anon");
        map.put("/loginPage","anon");
        map.put("/logout", "logout");// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了


        map.put("/**","authc");

        //配置记住我或认证通过可以访问的地址
        map.put("/**", "user");
        bean.setLoginUrl("/loginPage");
        bean.setFilterChainDefinitionMap(map);
        return bean;

    }
    /**
     * 开启shiro aop注解支持. 使用代理方式;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * DefaultAdvisorAutoProxyCreator实现了BeanProcessor接口,
     * 当ApplicationContext读如所有的Bean配置信息后,这个类将扫描上下文,
     * 找出所有的Advistor(一个切入点和一个通知的组成),将这些Advisor应用到所有符合切入点的Bean中
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }


    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        //自定义rememberMe //把cookie管理器交给SecurityManager
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }
    @Bean
    public UserRealm myRealm() {
        UserRealm userRealm = new UserRealm();
        // 配置 加密 (在加密后,不配置的话会导致登陆密码失败)
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return userRealm;
    }
    /**
     * 密码校验规则HashedCredentialsMatcher
     * 这个类是为了对密码进行编码的 ,
     * 防止密码在数据库里明码保存 , 当然在登陆认证的时候 ,
     * 这个类也负责对form里输入的密码进行编码
     * 处理认证匹配处理器:如果自定义需要实现继承HashedCredentialsMatcher
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次数
        credentialsMatcher.setHashIterations(1024);
        //此时用的是密码加密用的是 Hex 编码; false 时用 Base64 编码
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    //自定义sessionManager
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionIdUrlRewritingEnabled(false);//url 不显示sessionID
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }
    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    //@Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setExpire(1800);// 配置缓存过期时间
        redisManager.setTimeout(timeout);
        redisManager.setPort(port);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * cookie管理对象;
     * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
     * @return
     */
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }
    public SimpleCookie rememberMeCookie(){
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("boot-shiro-rememberMe");
        simpleCookie.setMaxAge(200000);//设置cookie的生效时间
        simpleCookie.setHttpOnly(true);
        return simpleCookie;
    }
}
View Code

 1.1自定义MySessionManager 获取sessionId

public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有 Authorization 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}
View Code

2.redis配置类

package com.example.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置类
 * @program: springbootdemo
 * @Description:
 */
@Configuration
@EnableCaching //开启注解
public class RedisConfig extends CachingConfigurerSupport {

    @Autowired
    private RedisTemplate redisTemplate;

    @Bean
    public RedisTemplate<String, Object> stringSerializerRedisTemplate(){
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
        return redisTemplate;
    }

    /**
     * retemplate相关配置
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);

        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());

        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();

        return template;
    }

    /**
     * 对hash类型的数据操作
     *
     * @param redisTemplate
     * @return
     */
    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    /**
     * 对redis字符串类型数据操作
     *
     * @param redisTemplate
     * @return
     */
    @Bean
    public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    /**
     * 对链表类型的数据操作
     *
     * @param redisTemplate
     * @return
     */
    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    /**
     * 对无序集合类型的数据操作
     *
     * @param redisTemplate
     * @return
     */
    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }
    /**
     * 对有序集合类型的数据操作
     *
     * @param redisTemplate
     * @return
     */
    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }

}
View Code

 

 五、自定义Realm

package com.example.demo.shiro.realms;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.example.demo.entity.ManagePermission;
import com.example.demo.entity.ManageRoles;
import com.example.demo.entity.ManageUser;
import com.example.demo.service.ManagePermissionService;
import com.example.demo.service.ManageRolesService;
import com.example.demo.service.ManageUserService;
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.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author:
 * @since:
 * @description:
 */
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private ManageUserService userService;

    @Autowired
    private ManageRolesService manageRolesService;

    @Autowired
    private ManagePermissionService managePermissionService;
    //执行授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        // TODO Auto-generated method stub
        System.out.println("授权");
        //获取当前登录用户
        Subject subject = SecurityUtils.getSubject();
        ManageUser user = (ManageUser) subject.getPrincipal();

        //查询用户对应的角色
        Integer roleId = user.getRoleId();
        ManageRoles manageRoles = manageRolesService.selectById(roleId);

        if(manageRoles!=null){//角色不为空
            //获取角色对应的菜单
            String menus = manageRoles.getMenusId();
            String[] menuId = menus.split(",");

            //菜单对应的权限
            List<ManagePermission> menuPermission = managePermissionService.selectList(new EntityWrapper<ManagePermission>().in("MENU_ID", Arrays.asList(menuId)));

            //其他相关权限
            List<ManagePermission> managePermissions = new ArrayList<>();
            String permissions = manageRoles.getPermissionsId();
            if(StringUtils.isNotBlank(permissions)){
                String[] permissionsId = permissions.split(",");
                 managePermissions = managePermissionService.selectBatchIds(Arrays.asList(permissionsId));
            }

            //当前登录用户的所有权限
            managePermissions.addAll(menuPermission);
            List<String> sn = managePermissions.stream().map(ManagePermission::getSn).collect(Collectors.toList());

            System.out.println(String.valueOf(sn));
            //给资源授权
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addStringPermissions(sn);
            simpleAuthorizationInfo.addRole(manageRoles.getName());
            return simpleAuthorizationInfo;
        }
        return null;
    }
    //执行认证逻辑
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // TODO Auto-generated method stub
        System.out.println("认证");

        //shiro判断逻辑
        UsernamePasswordToken user = (UsernamePasswordToken) token;
        ManageUser realUser = new ManageUser();
        realUser.setUsername(user.getUsername());
        realUser.setPassword(String.copyValueOf(user.getPassword()));

        ManageUser newUser = userService.selectOne(new EntityWrapper<ManageUser>().eq("USERNAME",realUser.getUsername()));
        if(newUser == null){
            //用户名错误
            //shiro会抛出UnknownAccountException异常
            return null;
        }
        System.out.println("认证用户:"+realUser);
        ByteSource credentialsSalt = ByteSource.Util.bytes(newUser.getUsername());
        return new SimpleAuthenticationInfo(newUser,newUser.getPassword(),credentialsSalt,getName());
    }
    /**
     * 在使用登出功能的时候,会触发的回调
     * @param principals
     */
    @Override
    public void onLogout(PrincipalCollection principals) {
        super.onLogout(principals);
    }

    public static void main(String[] args) {
        String hashAlgorithName = "MD5";
        String password = "root";
        int hashIterations = 1024;//加密次数
        ByteSource credentialsSalt = ByteSource.Util.bytes("admin");//盐值
        Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations);
        System.out.println(obj);
    }
}
View Code

 

六、VO层

1.菜单树

@Data
@Builder
@NoArgsConstructor
@EqualsAndHashCode
@AllArgsConstructor
@Accessors(chain = true)
public class ManageMenuVO implements Serializable {

    private Integer id;
    /**
     * 路径名称
     */
    private String name;
    /**
     * 图标class(elementui)
     */
    private String icon;
    /**
     * 路径地址
     */
    private String url;
    /**
     * 父节点ID,父节点同样在本目录下
     */
    private Integer parentId;


    private List<ManageMenuVO> childrens;

}
View Code

 

七、Controller层

1.登录

package com.example.demo.controller;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @author:
 * @since:
 * @description:
 */
@Slf4j
@Controller
public class LoginController {

    @PostMapping(value = "/login")
    public String createUser(String username, String password, String rememberMe, Model model) {
        Map restMap = new HashMap<String,Object>();
        Integer code = 200;
        String msg = "登录成功";
        Object data = null;

        Session session = SecurityUtils.getSubject().getSession();
        log.info("登录的sessionId  "+session.getId());

        //获取Subject
        Subject subject = SecurityUtils.getSubject();
        //封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //执行登录方法
        try {
            if(rememberMe!=null){token.setRememberMe(Boolean.parseBoolean(rememberMe));}
            subject.login(token);
            //登录成功
            return "redirect:index";
        } catch (UnknownAccountException e) {
            code = 500;
            msg = "用户名错误";
            data = e;
        } catch (IncorrectCredentialsException e) {
            code = 500;
            msg = "密码错误";
            data = e;
        }
        restMap.put("code",code);
        restMap.put("msg",msg);
        restMap.put("data",data);
        model.addAllAttributes(restMap);
        return "err";
    }

     @RequestMapping(value = "/logout")
     public void logout() {
         System.err.println("退出");
         Subject subject = SecurityUtils.getSubject();
         if (subject.isAuthenticated()) {
             subject.logout(); // session 会销毁,在sessionlistener监听session销毁,清理权限缓存
         }
     }
}
View Code

2.页面跳转

@Controller
public class RouterController {

    @Autowired
    private ManageMenuService manageMenuService;

    @GetMapping("/loginPage")
    public String loginPage(){
        return "/loginPage";
    }

    @GetMapping("/index")
    public String indexPage(Model model){
        //获取Subject
        Subject subject = SecurityUtils.getSubject();
        Map restMap = new HashMap<String,Object>();
        restMap.put("menuList",manageMenuService.selectMenuTree());
        restMap.put("data",String.valueOf(subject.getSession().getId()));
        System.out.println(restMap);
        model.addAllAttributes(restMap);
        return "/index";
    }
}
View Code

3.查看所有权限

@RestController
@RequestMapping("/managePermission")
public class ManagePermissionController {

    @Autowired
    private ManagePermissionService managePermissionService;

    @GetMapping("/getPermissionAll")
    @RequiresPermissions("managePermission:list")
    public List<ManagePermission>  RequiresPermissions(){return managePermissionService.selectList(null);}
}
View Code

4.查看所有人员

@RestController
@RequestMapping("/manageUser")
public class ManageUserController {

    @Autowired
    private ManageUserService manageUserService;

    @GetMapping("/getUserAll")
    @RequiresPermissions("manageUser:list")
    public List<ManageUser> getUserAll(){return manageUserService.selectList(null);}
}
View Code

 

八、权限不足全局异常

@ResponseBody
@ControllerAdvice
public class GlobalException {
    //授权异常
    @ExceptionHandler({AuthorizationException.class})
    public Object unauthorizedException() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("msg", "权限不足!");
        System.err.println("权限不足");
        return map;
    }
}
View Code

 

九、Html

1.登录

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>login</title>
</head>
<body>
<form action="/login" method="POST">
    <input type="text" name="username" id=""></br>
    <input type="password" name="password" id=""></br>
    <input type ="checkbox" name ="rememberMe" value="true" />记住我
    <input type="submit" value="登录">
</form>
</body>
</html>
loginPage.html

2.登录成功

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>首页</title>
</head>
<body>
    <h1>菜单</h1>
    <div th:each="menu:${menuList}">
        <a th:href="${menu.url}" ><h4 th:text="${menu.name}"></h4></a>
        <div th:each="child:${menu.childrens}" style="margin-left: 50px">
          <a th:href="${child.url}" > <h4 th:text="${child.name}"></h4></a>
        </div>
    </div>
    </br>
    <a href="/logout">退出登录</a>
</body>
</html>
index.html

3.错误页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>错误页面</title>
</head>
<body>
<h2 th:text="${code}" style="color: red"></h2>
<h1 th:text="${msg}" ></h1>
<h3 th:text="${data}"></h3>
</body>
</html>
err.html

 

项目结构

 

 

 

本章没有过多的赘述 entity、mapper、service层,可参考【SpringBoot整合Mybatis-Plus

 

posted @ 2020-03-11 22:25  一月1  阅读(3935)  评论(3编辑  收藏  举报