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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM