SpringBoot集成Shiro前后端分离使用redis做缓存

文章目录

  • 一 、shiro介绍
    • 1、基础介绍
    • 2、基本功能点
    • 3、基本流程图
  • 二、 常用的权限管理表关系
    • 2.1. 表组成
    • 2.2. 表结构
  • 三、实战案例
    • 3.1. 案例介绍
    • 3.2. 依赖
    • 3.3. Shiro全局配置
    • 3.4. 自定义ShiroRealm
    • 3.5. ShiroUtils
    • 3.6. 自定义SessionManager
    • 3.7. 登录/出主方法
    • 3.8. 测试主方法
  • 四、 前后端分离需要注意的点
  • 五、测试验链接
  • 六、测试验证
    • 6.1. 登录测试
      • 6.1. 1. 链接
      • 6.1. 2. 参数
      • 6.1. 3. 登陆后的token
    • 6.2. 获取用户列表
      • 6.2. 1. 链接
      • 6.2. 2. 参数
    • 6.3. 获取用户详情
      • 6.3. 1. 链接
      • 6.3. 2. 参数
    • 6.4. 添加用户
      • 6.4. 1. 链接
      • 6.4. 2. 参数
    • 6.5. 删除用户
      • 6.5. 1. 链接
      • 6.5. 2. 参数
    • 6.6. 注销登录
      • 6.6. 1. 链接
      • 6.6. 2. 参数

技术选型

框架 说明 版本
环境 JDK 1.8
后台 SpringBoot 2.1.7.RELEASE
权限控制 Shiro 1.4.0
数据库 Mysql 8.0.17
数据源 druid 1.1.10
持久层 mybatis 1.3.2
缓存 shiro-redis 3.1.0

一 、shiro介绍

1、基础介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。作为一款安全框架Shiro的设计相当巧妙。Shiro的应用不依赖任何容器,它不仅可以在JavaEE下使用,还可以应用在JavaSE环境中。

2、基本功能点

SpringBoot集成Shiro前后端分离使用redis做缓存_第1张图片

1、Authentication:身份认证/登录,验证用户是不是拥有相应的身份。

2、Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。

3、Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的。

4、Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。

5、Web Support:Web支持,可以非常容易的集成到Web环境。

6、Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。

7、Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去。

8、Testing:提供测试支持。

9、Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。

10、Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

3、基本流程图

SpringBoot集成Shiro前后端分离使用redis做缓存_第2张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第3张图片

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器。

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

    流程如下:
    
    步骤一:Shiro把用户的数据封装成标识token,token一般封装着用户名,密码等信息。

    步骤二:使用Subject门面获取到封装着用户的数据的标识token

    步骤三:Subject把标识token交给SecurityManager,在SecurityManager安全中心中,SecurityManager把标识token委托给认证器Authenticator进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些Realm。

   步骤四:认证器Authenticator将传入的标识token,与数据源Realm对比,验证token是否合法。

二、 常用的权限管理表关系

2.1. 表组成

  • 5张表,也就是现在流行的权限设计模型RBAC
    分别是:用户表 ,角色表,菜单(权限)表 , 用户和角色关联表,角色和菜单关联表

SpringBoot集成Shiro前后端分离使用redis做缓存_第4张图片

2.2. 表结构

-- 表结构总览--
-- 权限表--
DROP TABLE IF EXISTS `menu`;

CREATE TABLE `menu` (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  `name` varchar(50) DEFAULT NULL COMMENT '菜单名称',
  `url` varchar(200) DEFAULT NULL COMMENT '菜单URL',
  `perms` varchar(500) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `type` int(11) DEFAULT NULL COMMENT '类型   0:目录   1:菜单   2:按钮',
  `icon` varchar(50) DEFAULT NULL COMMENT '菜单图标',
  `order_num` int(11) DEFAULT NULL COMMENT '排序',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='菜单管理';

LOCK TABLES `menu` WRITE;

INSERT INTO `menu` (`menu_id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `create_time`, `update_time`)
VALUES
	(1,0,'权限菜单','menu/list','menu:list',0,'system',0,'2019-08-30 03:06:59','2019-08-30 07:38:01'),
	(2,0,'用户列表','user/list','user:list',0,'user',0,'2019-08-30 07:38:46','2019-08-30 07:38:56'),
	(3,0,'用户详情','user/detail','user:detail',0,'user',0,'2019-08-30 07:38:52','2019-08-30 07:39:43'),
	(4,0,'添加用户','user/add','user:add',0,'user',0,'2019-08-30 07:38:52','2019-08-30 07:42:54');

UNLOCK TABLES;


-- 角色表表--
DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `role_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) DEFAULT NULL COMMENT '角色名称',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  `create_user_id` bigint(20) DEFAULT NULL COMMENT '创建者ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色';

LOCK TABLES `role` WRITE;

INSERT INTO `role` (`role_id`, `role_name`, `remark`, `create_user_id`, `create_time`, `update_time`)
VALUES
(1,'admin','超级管理员',1,'2019-08-30 07:41:08','2019-08-30 07:41:08');

UNLOCK TABLES;


-- 角色和权限的关系表--
DROP TABLE IF EXISTS `role_menu`;

CREATE TABLE `role_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
  `menu_id` bigint(20) DEFAULT NULL COMMENT '菜单ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色与菜单对应关系';

LOCK TABLES `role_menu` WRITE;

INSERT INTO `role_menu` (`id`, `role_id`, `menu_id`, `create_time`, `update_time`)
VALUES
	(1,1,1,'2019-08-30 07:41:16','2019-08-30 07:41:16'),
	(3,1,3,'2019-08-30 07:41:28','2019-08-30 07:41:28'),
	(4,1,4,'2019-08-30 07:46:14','2019-08-30 07:46:14');

UNLOCK TABLES;

-- 用户表--
DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `salt` varchar(20) DEFAULT NULL COMMENT '盐',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
  `name` varchar(100) DEFAULT NULL COMMENT '姓名',
  `status` tinyint(4) DEFAULT NULL COMMENT '状态  0:禁用   1:正常',
  `create_user_id` bigint(20) DEFAULT NULL COMMENT '创建者ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系统用户';

LOCK TABLES `user` WRITE;

INSERT INTO `user` (`user_id`, `username`, `password`, `salt`, `email`, `mobile`, `name`, `status`, `create_user_id`, `create_time`, `update_time`)
VALUES
	(1,'admin','3743a4c09a17e6f2829febd09ca54e627810001cf255ddcae9dabd288a949c4a','123','[email protected]','18967835678',NULL,1,1,'2019-01-18 11:11:11','2019-01-18 11:11:11');

UNLOCK TABLES;

-- 用户和角色关系表
DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户与角色对应关系';

LOCK TABLES `user_role` WRITE;

INSERT INTO `user_role` (`id`, `user_id`, `role_id`, `create_time`, `update_time`)
VALUES
	(1,1,1,'2019-08-30 07:40:51','2019-08-30 07:40:51');

UNLOCK TABLES;

注:建表语句在项目中

三、实战案例

3.1. 案例介绍

3.2. 依赖

 
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            ${mybatis.version}
        
        
        
            com.alibaba
            druid-spring-boot-starter
            ${druid.version}
        
        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            org.projectlombok
            lombok
            true
        
    
        
            org.apache.shiro
            shiro-spring
            ${shiro-spring.version}
        

        
        
            org.crazycake
            shiro-redis
            ${shiro-redis.version}
        

3.3. Shiro全局配置

package com.gblfy.shiro.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
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.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.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author: http://gblfy.com
 * @Version 1.0.0
 */
@Configuration
@Slf4j
@Data
@ConfigurationProperties(prefix = "spring.redis")
public class ShiroConfig {

    private String host = "localhost";
    private int port = 6379;
    private Duration timeout;


    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     *
     * @return ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 过滤器链定义映射
        Map filterChainDefinitionMap = new LinkedHashMap<>();

        /*
         * anon:所有url都都可以匿名访问,authc:所有url都必须认证通过才可以访问;
         * 过滤链定义,从上向下顺序执行,authc 应放在 anon 下面
         * */
        filterChainDefinitionMap.put("/login", "anon");
        // 配置不会被拦截的链接 顺序判断,如果前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/html/**", "anon");
        // 所有url都必须认证通过才可以访问
        filterChainDefinitionMap.put("/**", "authc");

        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了, 位置放在 anon、authc下面
        filterChainDefinitionMap.put("/logout", "logout");

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        // 配器shirot认登录累面地址,前后端分离中登录累面跳转应由前端路由控制,后台仅返回json数据, 对应LoginController中unauth请求
        shiroFilterFactoryBean.setLoginUrl("/un_auth");

        // 登录成功后要跳转的链接, 此项目是前后端分离,故此行注释掉,登录成功之后返回用户基本信息及token给前端
        // shiroFilterFactoryBean.setSuccessUrl("/index");

        // 未授权界面, 对应LoginController中 unauthorized 请求
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis, 使用的是shiro-redis开源插件
     *
     * @return RedisSessionDAO
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setExpire(1800);
        return redisSessionDAO;
    }

    /**
     * Session ID 生成器
     *
     * @return JavaUuidSessionIdGenerator
     */
    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }

    /**
     * 自定义sessionManager
     *
     * @return SessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

    /**
     * 配置shiro redisManager, 使用的是shiro-redis开源插件
     *
     * @return RedisManager
     */
    private RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout((int) timeout.toMillis());
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现, 使用的是shiro-redis开源插件
     *
     * @return RedisCacheManager
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        // 必须要设置主键名称,shiro-redis 插件用过这个缓存用户信息
        redisCacheManager.setPrincipalIdFieldName("userId");
        return redisCacheManager;
    }


    /**
     * 权限管理,配置主要是Realm的管理认证
     *
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager(ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }


    /*
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    @Bean
    public SimpleCookie cookie() {
        // cookie的name,对应的默认是 JSESSIONID
        SimpleCookie cookie = new SimpleCookie("SHARE_JSESSIONID");
        cookie.setHttpOnly(true);
        //  path为 / 用于多个系统共享 JSESSIONID
        cookie.setPath("/");
        return cookie;
    }

    /* 此项目使用 shiro 场景为前后端分离项目,这里先注释掉,统一异常处理已在 GlobalExceptionHand.java 中实现 */

}

3.4. 自定义ShiroRealm

package com.gblfy.shiro.config;

import com.gblfy.shiro.entity.Role;
import com.gblfy.shiro.util.ShiroUtils;
import com.gblfy.shiro.entity.Menu;
import com.gblfy.shiro.entity.User;
import com.gblfy.shiro.mapper.MenuMapper;
import com.gblfy.shiro.mapper.RoleMapper;
import com.gblfy.shiro.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Objects;

/**
 * @Author: http://gblfy.com
 * @Version 1.0.0
 */
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {

    private UserMapper userMapper;

    private RoleMapper roleMapper;

    private MenuMapper menuMapper;

    @Autowired
    @SuppressWarnings("all")
    public ShiroRealm(UserMapper userMapper, RoleMapper roleMapper, MenuMapper menuMapper) {
        this.userMapper = userMapper;
        this.roleMapper = roleMapper;
        this.menuMapper = menuMapper;
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        log.info("开始执行授权操作.......");

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        /**
         * 查询用户角色
         * 如果身份认证的时候没有传入User对象,这里只能取到userName
         * 也就是SimpleAuthenticationInfo构造的时候第一个参数传递需要User对象
         */
        User user = (User) principalCollection.getPrimaryPrincipal();

        if (user == null) {
            log.error("用户不存在");
            throw new UnknownAccountException("用户不存在");
        }

        //TODO 是否为超级管理员   是  全部菜单权限


        /**
         * 查询用户角色
         */

        List roles = this.roleMapper.listRoleByUserId(user.getUserId());

        if(CollectionUtils.isNotEmpty(roles)){
            for (Role role : roles) {
                authorizationInfo.addRole(role.getRoleName());
                // 根据角色查询权限
                List menus = this.menuMapper.listMenuByRoleId(role.getRoleId());
                for (Menu m : menus) {
                    authorizationInfo.addStringPermission(m.getPerms());
                }
            }
        }

        return authorizationInfo;
    }


    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        log.info("开始进行身份认证......");

        //获取用户的输入的账号.
        String username = (String) authenticationToken.getPrincipal();

        //通过username从数据库中查找 User对象.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userMapper.findByUsername(username);
        if (Objects.isNull(user)) {
            return null;
        }

        return new SimpleAuthenticationInfo(
                // 这里传入的是user对象,比对的是用户名,直接传入用户名也没错,但是在授权部分就需要自己重新从数据库里取权限
                user,
                // 密码
                user.getPassword(),
                // salt = username + salt
                ByteSource.Util.bytes(user.getSalt()),
                // realm name
                getName()
        );
    }


    /**
     * 将自己的验证方式加入容器
     *
     * 凭证匹配器(由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
     *
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

        /**
         * 散列算法:这里可以使用MD5算法 也可以使用SHA-256
         */
        hashedCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
        // 散列的次数,比如散列16次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
       super.setCredentialsMatcher(hashedCredentialsMatcher);
    }

}

3.5. ShiroUtils

package com.gblfy.shiro.util;

import com.gblfy.shiro.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;

/**
 * Shiro工具类
 */
public class ShiroUtils {
    /**  加密算法 */
    public final static String hashAlgorithmName = "SHA-256";
    /**  循环次数 */
    public final static int hashIterations = 16;

    public static String sha256(String password, String salt) {
        return new SimpleHash(hashAlgorithmName, password, salt, hashIterations).toString();
    }

    // 获取一个测试账号 admin
    public static void main(String[] args) {
        // 3743a4c09a17e6f2829febd09ca54e627810001cf255ddcae9dabd288a949c4a
        System.out.println(sha256("admin","123")) ;
    }

    /**
     * 获取会话
     */
    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }
    
    /**
     * Subject:主体,代表了当前“用户”
     */
    public static Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    public static User getUserEntity() {
        return (User)SecurityUtils.getSubject().getPrincipal();
    }

    public static Long getUserId() {
        return getUserEntity().getUserId();
    }

    public static void setSessionAttribute(Object key, Object value) {
        getSession().setAttribute(key, value);
    }

    public static Object getSessionAttribute(Object key) {
        return getSession().getAttribute(key);
    }

    public static boolean isLogin() {
        return SecurityUtils.getSubject().getPrincipal() != null;
    }

    public static void logout() {
        SecurityUtils.getSubject().logout();
    }
}

3.6. 自定义SessionManager

package com.gblfy.shiro.config;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 *
 * @Author: http://gblfy.com
 * @Version 1.0.0
 *
 * 自定义session管理
 * 
* 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用), * 我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。 * 自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法 */ public class MySessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; @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); } } }

3.7. 登录/出主方法

package com.gblfy.shiro.controller;

import com.gblfy.shiro.util.CacheUser;
import com.gblfy.shiro.util.Response;
import com.gblfy.shiro.entity.User;
import com.gblfy.shiro.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author http://gblfy.com
 * @Description 登录
 * @Date 2019/9/14 15:34
 * @version1.0
 */
@Slf4j
@RestController
public class LoginController {


    private Response response;

    private UserService userService;

    @Autowired
    @SuppressWarnings("all")
    public LoginController(Response response, UserService userService) {
        this.response = response;
        this.userService = userService;
    }

    /**
     * description: 登录
     *
     * @return 登录结果
     */
    @PostMapping("/login")
    public Response login(User user) {
        log.warn("进入登录.....");

        String username = user.getUsername();
        String password = user.getPassword();

        if (StringUtils.isBlank(username)) {
            return response.failure("用户名为空!");
        }

        if (StringUtils.isBlank(password)) {
            return response.failure("密码为空!");
        }

        CacheUser loginUser = userService.login(username, password);
        // 登录成功返回用户信息
        return response.success("登录成功!", loginUser);
    }

    /**
     * description: 登出
     */
    @RequestMapping("/logout")
    public Response logOut() {
        userService.logout();
        return response.success("登出成功!");
    }

    /**
     * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
     * @return
     */
    @RequestMapping("/un_auth")
    public Response unAuth() {
        return response.failure(HttpStatus.UNAUTHORIZED, "用户未登录!", null);
    }

    /**
     * 未授权,无权限,此处返回未授权状态信息由前端控制跳转页面
     * @return
     */
    @RequestMapping("/unauthorized")
    public Response unauthorized() {
        return response.failure(HttpStatus.FORBIDDEN, "用户无权限!", null);
    }

}

3.8. 测试主方法

package com.gblfy.shiro.controller;

import com.gblfy.shiro.util.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author http://gblfy.com
 * @Description 测试主方法
 * @Date 2019/9/14 15:34
 * @version1.0
 */
@RestController
@Slf4j
@RequestMapping("user")
public class UserController {


    @Autowired
    private Response response;


    @GetMapping("list")
    @RequiresPermissions("user:list")
    public Response listUser() {
        return response.success("用户列表");
    }


    @GetMapping("{userId}")
    @RequiresPermissions("user:detail")
    public Response detailUser(@PathVariable("userId") Long userId) {
        return response.success("用户详情");
    }


    @PostMapping("add")
    @RequiresRoles("admin")
    @RequiresPermissions("user:add")
    public Response addUser() {
        return response.success("添加用户成功");
    }

    @DeleteMapping("del")
    @RequiresRoles("role")
    public Response delUser() {
        return response.success("删除用户");
    }
}

四、 前后端分离需要注意的点

传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法
登入失败,登入地址,前后端分离,不应该直接跳转页面,而是返回响应结果

五、测试验链接

说明 请求方式 url 参数
登录 POST localhost:80/login?username=admin&password=admin
获取用户列表 GET localhost:80/user/list Authorization
获取用户详情 GET localhost:80/user/1 Authorization
添加用户 POST localhost:80/user/add Authorization
删除用户 DELETE localhost:80/user/del Authorization
退出登入 查询redis中数据是否清除缓存信息 GET localhost:80/logout Authorization

六、测试验证

6.1. 登录测试

6.1. 1. 链接

localhost:80/login?username=admin&password=admin

6.1. 2. 参数

6.1. 3. 登陆后的token

025e0f13-ba78-47ff-bd2a-3fb6f459f102

SpringBoot集成Shiro前后端分离使用redis做缓存_第5张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第6张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第7张图片

6.2. 获取用户列表

6.2. 1. 链接

localhost:80/user/list

6.2. 2. 参数

key:Authorization 
#登录后的token
values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102

SpringBoot集成Shiro前后端分离使用redis做缓存_第8张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第9张图片

6.3. 获取用户详情

6.3. 1. 链接

localhost:80/user/1

6.3. 2. 参数

key:Authorization 
#登录后的token
values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102

SpringBoot集成Shiro前后端分离使用redis做缓存_第10张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第11张图片

6.4. 添加用户

6.4. 1. 链接

localhost:80/user/add

6.4. 2. 参数

key:Authorization 
#登录后的token
values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102

SpringBoot集成Shiro前后端分离使用redis做缓存_第12张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第13张图片

6.5. 删除用户

6.5. 1. 链接

localhost:80/user/del

6.5. 2. 参数

key:Authorization 
#登录后的token
values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102

SpringBoot集成Shiro前后端分离使用redis做缓存_第14张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第15张图片

SpringBoot集成Shiro前后端分离使用redis做缓存_第16张图片

6.6. 注销登录

6.6. 1. 链接

localhost:80/logout

6.6. 2. 参数

key:Authorization 
#登录后的token
values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102

SpringBoot集成Shiro前后端分离使用redis做缓存_第17张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第18张图片
SpringBoot集成Shiro前后端分离使用redis做缓存_第19张图片
从redis中,再次查看,token消失了,简言之,用户登录信息,销毁了

注:不填写登录的token或者填写token不正确,会提示用户未登录
SpringBoot集成Shiro前后端分离使用redis做缓存_第20张图片
再次查询详情


gitlab项目链接:
https://gitlab.com/gb-heima/springoot-shiro-redis
git下载方式:

git clone [email protected]:gb-heima/springoot-shiro-redis.git

zip下载方式:
https://gitlab.com/gb-heima/springoot-shiro-redis/-/archive/master/springoot-shiro-redis-master.zip

你可能感兴趣的:(SpringBoot)