【博客项目三】SpringBoot 集成 Shiro

SpringBoot 2.2 + Shiro 1.4.0.
写该文章的用意主要是很多博客使用SpringBoot版本过低有些方法并不适用或没有示例或说的过于简洁或过于深入,不适用与初学者。
自己了解尚浅,文章只是给出主要方法。后附有示例源码可参考。密码加密及登录次数验证需看博客代码。
参考的博客后附有相关文章。

简单登陆跳转

maven依赖

<!--web-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--日志-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Shiro 核心依赖 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
</dependency>
<!--shiro freemarker页面标签-->
<dependency>
	<groupId>net.mingsoft</groupId>
	<artifactId>shiro-freemarker-tags</artifactId>
</dependency>
<!--shiro redis-->
<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
</dependency>
<!-- Redis -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- mysql -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>
<!--druid-->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
</dependency>
<!-- mybatisPlus 核心库 -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--分页-->
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<!-- mybatisPlus 代码生成器插件-->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!-- 代码生成器模板-->
<dependency>
	<groupId>org.freemarker</groupId>
	<artifactId>freemarker</artifactId>
</dependency>
<!--java持久化API-->
<dependency>
	<groupId>javax.persistence</groupId>
	<artifactId>persistence-api</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

表Sql

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL COMMENT '权限名称',
  `type`  enum('menu','button') DEFAULT 'menu' COMMENT '权限类型(菜单,按钮)',
  `url` varchar(200) DEFAULT NULL COMMENT '资源路径',
  `permission` varchar(100) DEFAULT NULL COMMENT '权限标识(多个用逗号分隔,如:user:list,user:create)',
  `parent_id` bigint(20) unsigned DEFAULT '0' COMMENT '父节点',
  `sort` int(10) unsigned DEFAULT NULL COMMENT '排序',
  `external` tinyint(1) unsigned DEFAULT NULL COMMENT '是否外部链接',
  `available` tinyint(1) unsigned DEFAULT '0' COMMENT '是否可用',
  `icon` varchar(100) DEFAULT NULL COMMENT '菜单图标',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_sys_resource_parent_id` (`parent_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8 COMMENT = '权限表' ROW_FORMAT=COMPACT;

INSERT INTO `sys_menu` VALUES ('1', '用户管理', 'menu', null, null, '0', '1', '0', '1', 'fa fa-users', '2018-05-16 17:02:54', '2018-05-16 17:02:54');
INSERT INTO `sys_menu` VALUES ('2', '用户列表', 'menu', '/user/index', 'users', '1', '1', '0', '1', null, '2017-12-22 13:56:15', '2018-05-16 14:44:20');
INSERT INTO `sys_menu` VALUES ('3', '新增用户', 'button', null, 'user:add', '2', '2', '0', '1', null, '2018-05-16 14:07:43', '2018-05-16 14:16:23');
INSERT INTO `sys_menu` VALUES ('4', '批量删除用户', 'button', null, 'user:batchDelete', '2', '3', '0', '1', null, '2018-05-16 14:12:23', '2018-05-16 14:16:35');
INSERT INTO `sys_menu` VALUES ('5', '编辑用户', 'button', null, 'user:edit', '2', '4', '0', '1', null, '2018-05-16 14:12:50', '2018-05-16 14:16:43');
INSERT INTO `sys_menu` VALUES ('6', '删除用户', 'button', null, 'user:delete', '2', '5', '0', '1', null, '2018-05-16 14:13:09', '2018-05-16 14:51:50');
INSERT INTO `sys_menu` VALUES ('7', '分配用户角色', 'button', null, 'user:allotRole', '2', '6', '0', '1', null, '2018-05-16 14:15:28', '2018-05-16 14:16:54');
INSERT INTO `sys_menu` VALUES ('8', '系统配置', 'menu', null, null, '0', '2', '0', '1', 'fa fa-cogs', '2017-12-20 16:40:06', '2017-12-20 16:40:08');
INSERT INTO `sys_menu` VALUES ('9', '资源管理', 'menu', '/menu/index', 'resources', '8', '1', '0', '1', null, '2017-12-22 15:31:05', '2017-12-22 15:31:05');
INSERT INTO `sys_menu` VALUES ('10', '新增资源', 'button', null, 'resource:add', '9', '2', '0', '1', null, '2018-05-16 14:07:43', '2018-05-16 14:16:23');
INSERT INTO `sys_menu` VALUES ('11', '批量删除资源', 'button', null, 'resource:batchDelete', '9', '3', '0', '1', null, '2018-05-16 14:12:23', '2018-05-16 14:16:35');
INSERT INTO `sys_menu` VALUES ('12', '编辑资源', 'button', null, 'resource:edit', '9', '4', '0', '1', null, '2018-05-16 14:12:50', '2018-05-16 14:16:43');
INSERT INTO `sys_menu` VALUES ('13', '删除资源', 'button', null, 'resource:delete', '9', '5', '0', '1', null, '2018-05-16 14:13:09', '2018-05-16 14:51:50');
INSERT INTO `sys_menu` VALUES ('14', '角色管理', 'menu', '/role/index', 'roles', '8', '2', '0', '1', '', '2017-12-22 15:31:27', '2018-05-17 12:51:06');
INSERT INTO `sys_menu` VALUES ('15', '新增角色', 'button', null, 'role:add', '14', '2', '0', '1', null, '2018-05-16 14:07:43', '2018-05-16 14:16:23');
INSERT INTO `sys_menu` VALUES ('16', '批量删除角色', 'button', null, 'role:batchDelete', '14', '3', '0', '1', null, '2018-05-16 14:12:23', '2018-05-16 14:16:35');
INSERT INTO `sys_menu` VALUES ('17', '编辑角色', 'button', null, 'role:edit', '14', '4', '0', '1', null, '2018-05-16 14:12:50', '2018-05-16 14:16:43');
INSERT INTO `sys_menu` VALUES ('18', '删除角色', 'button', null, 'role:delete', '14', '5', '0', '1', null, '2018-05-16 14:13:09', '2018-05-16 14:51:50');
INSERT INTO `sys_menu` VALUES ('19', '分配角色资源', 'button', null, 'role:allotResource', '14', '6', '0', '1', null, '2018-05-17 10:04:21', '2018-05-17 10:04:21');
INSERT INTO `sys_menu` VALUES ('20', '数据监控', 'menu', '', '', null, '3', '0', '1', 'fa fa-heartbeat', '2018-05-17 12:38:20', '2018-05-17 12:53:06');
INSERT INTO `sys_menu` VALUES ('21', 'Druid监控', 'menu', '/druid/index.html', 'druid', '20', '1', '1', '1', '', '2018-05-17 12:46:37', '2018-05-17 12:52:33');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL COMMENT '角色名',
  `description` varchar(100) DEFAULT NULL COMMENT '描述',
  `available` tinyint(1) DEFAULT '0' COMMENT '是否可用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT = '角色表' ROW_FORMAT=COMPACT;

INSERT INTO `sys_role` VALUES ('1', 'role:root', '超级管理员', '1', '2017-12-20 16:40:24', '2017-12-20 16:40:26');
INSERT INTO `sys_role` VALUES ('2', 'role:admin', '管理员', '1', '2017-12-22 13:56:39', '2017-12-22 13:56:39');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) unsigned NOT NULL COMMENT '权限ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=57 DEFAULT CHARSET=utf8 COMMENT = '角色与权限关系表' ROW_FORMAT=COMPACT;

INSERT INTO `sys_role_menu` VALUES ('27', '1', '20', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('28', '1', '21', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('29', '1', '1', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('30', '1', '2', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('31', '1', '3', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('32', '1', '4', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('33', '1', '5', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('34', '1', '6', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('35', '1', '7', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('36', '1', '8', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('37', '1', '9', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('38', '1', '10', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('39', '1', '11', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('40', '1', '12', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('41', '1', '13', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('42', '1', '14', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('43', '1', '15', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('44', '1', '16', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('45', '1', '17', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('46', '1', '18', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('47', '1', '19', '2018-05-17 12:52:41', '2018-05-17 12:52:41');
INSERT INTO `sys_role_menu` VALUES ('48', '2', '20', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('49', '2', '21', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('50', '2', '2', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('51', '2', '3', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('52', '2', '8', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('53', '2', '9', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('54', '2', '10', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('55', '2', '14', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('56', '2', '15', '2018-05-17 12:52:51', '2018-05-17 12:52:51');
INSERT INTO `sys_role_menu` VALUES ('57', '2', '1', '2018-05-17 12:52:51', '2018-05-17 12:52:51');

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL COMMENT '用户名称',
  `password` varchar(100) DEFAULT NULL COMMENT '登录密码',
  `nickname` varchar(30) DEFAULT '' COMMENT '昵称',
  `mobile` varchar(30) DEFAULT NULL COMMENT '手机号',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱地址',
  `birthday` date DEFAULT NULL COMMENT '生日',
  `gender` tinyint(2) unsigned DEFAULT NULL COMMENT '性别',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址',
  `user_type` enum('ROOT','ADMIN','USER') DEFAULT 'ADMIN' COMMENT 'ROOT超级管理员、ADMIN管理员、USER普通用户',
  `reg_ip` varchar(30) DEFAULT NULL COMMENT '注册IP',
  `last_login_ip` varchar(30) DEFAULT NULL COMMENT '最近登录IP',
  `last_login_time` datetime DEFAULT NULL COMMENT '最近登录时间',
  `login_count` int(10) unsigned DEFAULT '0' COMMENT '登录次数',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  `status` int(1) unsigned DEFAULT NULL COMMENT '状态  0:禁用 1:正常',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT = '系统用户表' ROW_FORMAT=DYNAMIC;

INSERT INTO `sys_user` VALUES ('1', 'root', '123456', '超级管理员', '15151551516', '123456789@qq.com', null, null, 'https://static.zhyd.me/static/img/favicon.ico', 'ROOT', null, '127.0.0.1', '2018-05-17 13:09:35', '228', null, '1', '2018-01-02 09:32:15', '2018-05-17 13:09:35');
INSERT INTO `sys_user` VALUES ('2', 'admin', '123456', '管理员', '15151551516', '123456789@qq.com', null, null, null, 'ADMIN', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1', '2018-05-17 13:08:30', '13', null, '1', '2018-01-02 15:56:34', '2018-05-17 13:08:30');

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT = '用户与角色关系表' ROW_FORMAT=COMPACT;

INSERT INTO `sys_user_role` VALUES ('1', '1', '1', '2018-01-02 10:47:27', '2018-01-02 10:47:27');
INSERT INTO `sys_user_role` VALUES ('2', '2', '2', '2018-01-05 18:21:02', '2018-01-05 18:21:02');

entity,mapper,service,serviceImpl

这些类直接根据mybatis-plus-generator生成即可。mybatis-plus-generator也有对应的文章源码可直接使用。
不想用代码生成器我的源码也有提供。这里附录一下主要方法。
没有在这里写出的,说明只是用mybatis-plus自带的查询就可以。mybatis-plus QueryWrapper不能使用外联。
用到的手写SQL:
menu

List<SysMenu> listByUserId(Integer userId);

MenuMapper.xml
<!--查询用户权限-->
<select id="listByUserId" parameterType="Integer" resultMap="BaseResultMap">
	SELECT
		re.id,
		re.`name`,
		re.parent_id,
		re.url,
		re.permission,
		re.icon,
		re.sort
	FROM
		sys_menu re
	INNER JOIN sys_role_menu rr ON re.id = rr.menu_id
	INNER JOIN sys_user_role ur ON rr.role_id = ur.role_id
	WHERE
		ur.user_id = #{userId}
	AND re.available = 1
	ORDER BY
		re.parent_id ASC,
		re.sort ASC
</select>

role

List<SysRole> listRolesByUserId(Integer userId);

<!--查询用户角色-->
<select id="listRolesByUserId" parameterType="Integer" resultMap="BaseResultMap">
	SELECT
		r.id,
		r.name,
		r.description
	FROM
		sys_role r
	INNER JOIN sys_user_role ur ON ur.role_id = r.id
	WHERE
		ur.user_id = #{userId}
	AND r.available = 1
</select>

LoginController

package com.blackcat.shiro.controller;

import com.blackcat.shiro.entity.SysUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpSession;

/**
 * <p> 描述 :
 *
 * @author : blackcat
 * @date : 2020/2/3 11:14
 */
@Controller
public class LoginController {

    /**
     * 跳转到login页面
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String login(Model model) {
        Subject subject = SecurityUtils.getSubject();
        SysUser user=(SysUser) subject.getPrincipal();
        if (user == null){
            return "login";
        }else{
            return "redirect:index";
        }
    }

    /**
     * <p> 描述 : 用户登录
     * @author : blackcat
     * @date  : 2020/2/3 17:04
     */
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @ResponseBody
    public String loginUser(String username, String password, boolean rememberMe) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
        //获取当前的Subject
        Subject currentUser = SecurityUtils.getSubject();
        try {
            // 在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
            // 每个Realm都能在必要时对提交的AuthenticationTokens作出反应
            // 所以这一步在调用login(token)方法时,它会走到xxRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
            currentUser.login(token);
            return "登录成功!";
        } catch (Exception e) {
            token.clear();
            return "登录失败";
        }
    }

    /**
     * <p> 描述 : 访问项目根路径
     * @author : blackcat
     * @date  : 2020/2/3 17:02
     */
    @RequiresAuthentication
    @GetMapping(value = {"", "/index"})
    public String home() {
        Subject subject = SecurityUtils.getSubject();
        SysUser user=(SysUser) subject.getPrincipal();
        if (user == null){
            return "login";
        }else{
            return "index";
        }
    }


    /**
     * 登出  这个方法没用到,用的是shiro默认的logout
     * @param session
     * @param model
     * @return
     */
    @RequestMapping("/logout")
    public String logout(HttpSession session, Model model) {
        model.addAttribute("msg","安全退出!");
        return "login";
    }

    /**
     * 跳转到无权限页面
     * @param session
     * @param model
     * @return
     */
    @RequestMapping("/unauthorized")
    public String unauthorized(HttpSession session, Model model) {
        return "unauthorized";
    }
}

ShiroConfig

package com.blackcat.shiro.config;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.LinkedHashMap;
import java.util.Properties;

/**
 * <p> 描述 :Shiro配置
 * @author : blackcat
 * @date : 2020/2/3 10:53
 */
@Configuration
public class ShiroConfig {

    /**
     * 解决: 无权限页面不跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 无效
     * shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须满足filter instanceof AuthorizationFilter,
     * 只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,
     * 所以unauthorizedUrl设置后页面不跳转 Shiro注解模式下,登录失败与没有权限都是通过抛出异常。
     * 并且默认并没有去处理或者捕获这些异常。在SpringMVC下需要配置捕获相应异常来通知用户信息
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
        Properties properties=new Properties();
        //这里的 /unauthorized 是页面,不是访问的路径
        properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized");
        properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/unauthorized");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }

    @Bean
    public MethodInvokingFactoryBean methodInvokingFactoryBean(SecurityManager securityManager){
        MethodInvokingFactoryBean bean = new MethodInvokingFactoryBean();
        bean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
        bean.setArguments(securityManager);
        return bean;
    }

    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意:初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
     * @param securityManager
     * @return
     */
    @Bean(name = "shirFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //必须设置 SecurityManager,Shiro的核心安全接口
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //这里的/login是后台的接口名,非页面,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //这里的/index是后台的接口名,非页面,登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权界面,该配置无效,并不会进行页面跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        //自定义拦截器限制并发人数,参考博客:
        //LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //限制同一帐号同时在线的个数
        //filtersMap.put("kickout", kickoutSessionControlFilter());
        //shiroFilterFactoryBean.setFilters(filtersMap);

        // 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
        // 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 一定要注意顺序,否则就不好使了
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //配置不登录可以访问的资源,anon 表示资源都可以匿名访问
        filterChainDefinitionMap.put("/login", "anon");
        //filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/bootstrap/**", "anon");
        filterChainDefinitionMap.put("/ztree/**", "anon");
        filterChainDefinitionMap.put("/jquery/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        //logout是shiro提供的过滤器
        filterChainDefinitionMap.put("/logout", "logout");
        //此时访问/userInfo/del需要del权限,在自定义Realm中为用户授权。
        //filterChainDefinitionMap.put("/userInfo/del", "perms[\"userInfo:del\"]");

        //其他资源都需要认证  authc 表示需要认证才能进行访问
        filterChainDefinitionMap.put("/**", "user");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * 配置核心安全事务管理器
     * @param shiroRealm
     * @return
     */
    @Bean(name="securityManager")
    public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        //设置自定义realm.
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }

    /**
     *  身份认证realm; (这个需要自己写,账号密码校验;权限等)
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm(){
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }

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

    /**
     * <p> 描述 : 开启shiro aop注解支持.
     * 可以在controller中的方法前加上注解 如 @RequiresPermissions("user:add")
     * @author : blackcat
     * @date  : 2020/2/2 10:21
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

ShiroRealm

package com.blackcat.shiro.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blackcat.shiro.entity.SysMenu;
import com.blackcat.shiro.entity.SysRole;
import com.blackcat.shiro.entity.SysUser;
import com.blackcat.shiro.service.SysMenuService;
import com.blackcat.shiro.service.SysRoleService;
import com.blackcat.shiro.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import javax.annotation.Resource;
import java.util.List;

/**
 * <p> 描述 :身份认证
 * @author : blackcat
 * @date : 2020/2/3 10:54
 */
public class ShiroRealm extends AuthorizingRealm {

    @Resource
    private SysUserService sysUserService;
    @Resource
    private SysRoleService sysRoleService;
    @Resource
    private SysMenuService sysMenuService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 用户信息
        SysUser sysUser = (SysUser) principalCollection.getPrimaryPrincipal();
        // 赋予角色
        List<SysRole> roleList = sysRoleService.listRolesByUserId(sysUser.getId());
        roleList.forEach(role -> info.addRole(role.getName()));
        // 赋予权限
        List<SysMenu> menusList = sysMenuService.listByUserId(sysUser.getId());
        menusList.forEach(menu -> {
            if (StringUtils.isNotBlank(menu.getPermission())) {
                info.addStringPermission(menu.getPermission());
            }
        });
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        String password = new String((char[]) authenticationToken.getCredentials());

        SysUser user = sysUserService.getOne(new QueryWrapper<SysUser>().eq("username", username));
        //可以在这里直接对用户名校验,或者调用 CredentialsMatcher 校验
        if (user == null) {
            throw new UnknownAccountException("用户名或密码错误!");
        }
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("用户名或密码错误!");
        }
        if ("1".equals(user.getStatus())) {
            throw new LockedAccountException("账号已被锁定,请联系管理员!");
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(), getName());
        return info;
    }
}

application.yml

# Server settings
server:
    port: 8083
    # HTTP请求和响应头的最大量,以字节为单位,默认值为4096字节,超过此长度的部分不予处理,一般8K。解决java.io.EOFException: null问题
    max-http-header-size: 8192
    # use-forward-headers: true Spring Boot 2.2中的弃用 使用下列参数
    forward-headers-strategy: native
    compression:
        enabled: true
        min-response-size: 1024
        mime-types: text/plain,text/css,text/xml,text/javascript,application/json,application/javascript,application/xml,application/xml+rss,application/x-javascript,application/x-httpd-php,image/jpeg,image/gif,image/png
    tomcat:
        remote-ip-header: X-Forwarded-for
        protocol-header: X-Forwarded-Proto
        port-header: X-Forwarded-Port
        uri-encoding: UTF-8
        basedir: /var/tmp/website-app
    #servlet:
        #context-path: /blackcat
# SPRING PROFILES
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=UTC
        username: root
        password: 111111
    application:
        name: blog
    freemarker:
        allow-request-override: false
        allow-session-override: false
        cache: false #缓存配置
        charset: UTF-8 #编码格式
        request-context-attribute: request # 访问request名称定义
        check-template-location: true
        content-type: text/html # 设置Content-Type
        enabled: true
        expose-request-attributes: false # 设定所有request的属性在merge到模板的时候,是否要都添加到model中
        expose-session-attributes: false # 是否在merge模板的时候,将HttpSession属性都添加到model中
        expose-spring-macro-helpers: true # 设定是否以springMacroRequestContext的形式暴露RequestContext给Spring’s macro library使用
        prefer-file-system-access: true
        suffix: .ftl
        template-loader-path: classpath:/templates/ #模板加载路径 按需配置
        settings:
            template_update_delay: 0
            default_encoding: UTF-8
            classic_compatible: true #解决前台使用${}赋值值为空的情况
    # HTTP ENCODING
    http:
        multipart:
            #max-file-size: 2MB
            #max-request-size: 10MB
        encoding:
            enabled: true
            charset: UTF-8
            force: true
    messages:
        encoding: UTF-8
    jmx:
        enabled: true
        default-domain: agentservice
    resources:
        static-locations: classpath:/static/
        chain:
            strategy:
                content:
                    enabled: true
                    paths: /**
    # redis缓存服务配置
    session:
        store-type: redis
    # Redis数据库索引(默认为0)
    redis:
        database: 1
        # Redis服务器地址
        host: 127.0.0.1
        # Redis服务器连接端口
        port: 6379
        # Redis服务器连接密码(默认为空)
        password: 111111
        # 连接池最大连接数(使用负值表示没有限制)
        jedis:
            pool:
                max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
                max-wait: -1      # 连接池最大阻塞等待时间(使用负值表示没有限制)
                max-idle: 10      # 连接池中的最大空闲连接
                min-idle: 5       # 连接池中的最小空闲连接
        # 连接超时时间(毫秒)
        timeout: 0
        # 默认的数据过期时间,主要用于shiro权限管理
        expire: 2592000

# MyBatis plus
mybatis-plus:
    mapper-locations: classpath:/mappers/*.xml
    #实体扫描,多个package用逗号或者分号分隔
    typeAliasesPackage: com.blackcat.shiro.entity
    global-config:
        #刷新mapper 调试神器
        db-config:
            #主键类型  0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
            #idtype: 0
            #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
            field-strategy: not_empty
            #驼峰下划线转换
            column-underline: true
            #数据库大写下划线转换
            capitalmode: true
            #逻辑删除配置
            logic-delete-value: 1
            logic-not-delete-value: 0
            db-type: mysql
        refresh: true
        #自定义填充策略接口实现
        #metaobjecthandler: com.baomidou.springboot.xxx
        #自定义SQL注入器
        #sqlinjector: com.baomidou.mybatisplus.extension.injector.LogicSqlInjector
        configuration:
            # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
            log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
            map-underscore-to-camel-case: true
            cache-enabled: false

banner:
    #charset: UTF-8

index.ftl

<!DOCTYPE html>
<html lang="en">
<head>

</head>
<body>
<shiro:hasPermission name="user:add"><a href="/user/add">点击添加固定用户信息(后台写死,方便测试)</a></shiro:hasPermission><br/>
<shiro:hasPermission name="user:del"><a href="/user/del">点击删除固定用户信息(后台写死,方便测试)</a></shiro:hasPermission><br/>
<shiro:hasPermission name="user:view"><a href="/user/view">显示此内容表示拥有查看用户列表的权限</a></shiro:hasPermission><br/>



<!-- 用户没有身份验证时显示相应信息,即游客访问信息 -->
<shiro:guest>游客显示的信息</shiro:guest><br/>
<!-- 用户已经身份验证/记住我登录后显示相应的信息 -->
<shiro:user>用户已经登录过了</shiro:user><br/>
<!-- 用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的 -->
<shiro:authenticated>不是记住我登录</shiro:authenticated><br/>
<!-- 显示用户身份信息,通常为登录帐号信息,默认调用Subject.getPrincipal()获取,即Primary Principal -->
<shiro:principal></shiro:principal><br/>
<!--用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证,与guest标签的区别是,该标签包含已记住用户 -->
<shiro:notAuthenticated>已记住用户</shiro:notAuthenticated><br/>
<!-- 相当于Subject.getPrincipals().oneByType(String.class) -->
<shiro:principal type="java.lang.String"/><br/>
<!-- 相当于((User)Subject.getPrincipals()).getUsername() -->
<shiro:principal property="username"/><br/>
<!-- 如果当前Subject有角色将显示body体内容 name="角色名" -->
<shiro:hasRole name="admin">这是admin角色</shiro:hasRole><br/>
<!-- 如果当前Subject有任意一个角色(或的关系)将显示body体内容。 name="角色名1,角色名2..." -->
<shiro:hasAnyRoles name="admin,vip">用户拥有admin角色 或者 vip角色</shiro:hasAnyRoles><br/>
<!-- 如果当前Subject没有角色将显示body体内容 -->
<shiro:lacksRole name="admin">如果不是admin角色,显示内容</shiro:lacksRole><br/>
<!-- 如果当前Subject有权限将显示body体内容 name="权限名" -->
<shiro:hasPermission name="user:add">用户拥有添加权限</shiro:hasPermission><br/>
<!-- 用户同时拥有以下两种权限,显示内容 -->
<shiro:hasAllPermissions name="user:add,user:view">用户同时拥有列表权限和添加权限</shiro:hasAllPermissions><br/>
<!-- 用户拥有以下权限任意一种 -->
<shiro:hasAnyPermissions name="user:view,user:delete">用户拥有列表权限或者删除权限</shiro:hasAnyPermissions><br/>
<!-- 如果当前Subject没有权限将显示body体内容 name="权限名" -->
<shiro:lacksPermission name="user:add">如果用户没有添加权限,显示的内容</shiro:lacksPermission><br/>



<a href="/logout">点我注销</a>
</body>
</html>

login.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <#assign basePath=request.contextPath />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>后台管理</title>
    <link href="${basePath}/images/favicon.ico" rel="icon">
    <link href="${basePath}/bootstrap/css/bootstrap.css" rel="stylesheet">
    <link href="${basePath}/bootstrap/css/font-awesome.css" rel="stylesheet">
    <link href="${basePath}/css/jquery-confirm.min.css" rel="stylesheet">
    <link href="${basePath}/css/nprogress2.min.css" rel="stylesheet">
    <link href="${basePath}/css/zhyd.core.css" rel="stylesheet">
</head>

<body class="login">
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static"
     data-keyboard="false">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-body">
                <div class="login_wrapper">
                    <div class="animate form login_form" style="position: relative;">
                        <section class="login_content">
                            <form action="/login" method="POST" id="login-form">
                                <h1>登录管理系统</h1>
                                <div>
                                    <input type="text" class="form-control" placeholder="请输入用户名" name="username" required=""/>
                                </div>
                                <div>
                                    <input type="password" class="form-control" placeholder="请输入密码" name="password" required=""/>
                                </div>
                                <div class="form-group" style="text-align : left">
                                    <label><input type="checkbox" id="rememberMe" name="rememberMe" style="width: 12px; height: 12px;margin-right: 5px;">记住我</label>
                                </div>
                                <div>
                                    <button type="button" class="btn btn-success btn-login" style="width: 100%;">登录</button>
                                </div>

                                <div class="clearfix"></div>

                                <div class="separator">
                                    <div class="clearfix"></div>
                                    <div>
                                        <h1><i class="fa fa-coffee"></i> BlackCat 博客系统</h1>
                                        <p>Copyright © 2019 <a href="https://www.kylin-blackcat.com">blackcat</a>. All Rights Reserved. </p>
                                    </div>
                                </div>
                            </form>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>

<script src="${basePath}/jquery/jquery-2.1.4.min.js" type="text/javascript"></script>
<script src="${basePath}/js/jquery-confirm.min.js" type="text/javascript"></script>
<script src="${basePath}/bootstrap/js/bootstrap.js" type="text/javascript"></script>
<script src="${basePath}/js/blog-table-tool.js"></script>

<script>
    $("#modal").modal('show');
    $(".btn-login").click(function () {
        $.ajax({
            type: "POST",
            url: "/login",
            data: $("#login-form").serialize(),
            //dataType: "json",
            success: function (json) {
                window.location.href = "/index";
            }
        });
    });
    document.onkeydown = function (event) {
        var e = event || window.event || arguments.callee.caller.arguments[0];
        if (e && e.keyCode === 13) {
            $(".btn-login").click();
        }
    };
</script>
</html>

unauthorized.ftl

<!DOCTYPE html>
<html lang="en">
<head>

</head>
<body>
    <h1>对不起,您没有权限</h1>
</body>
</html>

记住我

ShiroConfig 添加


/**
 * <p> 描述 : 配置核心安全事务管理器
 * @author : blackcat
 * @date  : 2020/2/4 16:37
*/
@Bean(name="securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
	DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
	//设置自定义realm.
	securityManager.setRealm(shiroRealm);
	//配置记住我 (加入此行代码)
	securityManager.setRememberMeManager(rememberMeManager());
	return securityManager;
}

加入下方两个方法

/**
 * <p> 描述 : cookie对象;会话Cookie模板 ,默认为: JSESSIONID 
 * 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe,自定义
 * @author : blackcat
 * @date  : 2020/2/6 13:25   
*/
@Bean
public SimpleCookie rememberMeCookie(){
	//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
	SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
	//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:

	//setcookie()的第七个参数
	//设为true后,只能通过http访问,javascript无法访问
	//防止xss读取cookie
	simpleCookie.setHttpOnly(true);
	simpleCookie.setPath("/");
	//<!-- 记住我cookie生效时间30天 ,单位秒;-->
	simpleCookie.setMaxAge(2592000);
	return simpleCookie;
}

/**
 * <p> 描述 : cookie管理对象;记住我功能,rememberMe管理器
 * @author : blackcat
 * @date  : 2020/2/6 13:25   
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
	CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
	cookieRememberMeManager.setCookie(rememberMeCookie());
	//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
	cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
	return cookieRememberMeManager;
}

修改访问首页访问权限

如果使用@RequiresAuthentication该注释,记住我登陆关闭浏览器,重新访问项目,主页会显示无权限。
@RequiresUser:验证用户是否被记忆。

/**
 * <p> 描述 : 访问项目根路径
 * @author : blackcat
 * @date  : 2020/2/3 17:02
 */
@RequiresUser
@GetMapping(value = {"", "/index"})
public String home() {
	Subject subject = SecurityUtils.getSubject();
	SysUser user=(SysUser) subject.getPrincipal();
	if (user == null){
		return "login";
	}else{
		return "index";
	}
}

配置redis缓存管理器

Redis配置文件

package com.blackcat.blog.common.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.time.Duration;


/**
 * <p> 描述 : Redis配置文件
 * @author : blackcat
 * @date  : 2020/2/8 15:58
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 缓存数据时Key的生成器,可以依据业务和技术场景自行定制
     *
     * @return
     */
    @Bean
    @Override
    @Deprecated
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    /**
     * <p> 描述 : 管理缓存
     * @author : blackcat
     * @date  : 2020/2/8 16:00
     * 注:在springboot2.x中,RedisCacheManager已经没有了单参数的构造方法
    */
    @SuppressWarnings("rawtypes")
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)); // 设置缓存有效期一小时
        return RedisCacheManager
                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheConfiguration).build();
    }

    /**
     * <p> 描述 : RedisTemplate配置
     * @author : blackcat
     * @date  : 2020/2/8 16:00
    */
    @Bean
    @SuppressWarnings({"rawtypes", "unchecked"})
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<Object,Object> redisTemplate=new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer替换默认的序列化规则
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        //设置value的序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

redis属性配置文件

package com.blackcat.blog.common.property;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * <p> 描述 : redis属性配置文件
 * @author : blackcat
 * @date  : 2020/2/2 11:36
*/
@Component
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "spring.redis")
@Data
@EqualsAndHashCode(callSuper = false)
@Order(-1)
public class RedisProperties {
    private Integer database;
    private String host;
    private Integer port;
    private String password;
    private Integer timeout;
    /**
     * 默认30天 = 2592000s
     */
    private Integer expire = 2592000;

}

ShiroConfig 配置

添加以下方法

/**
 * cacheManager 缓存 redis实现
 * 使用的是shiro-redis开源插件
 *
 * @return
 */
@Bean
public RedisCacheManager redisCacheManager() {
	RedisCacheManager redisCacheManager = new RedisCacheManager();
	redisCacheManager.setRedisManager(redisManager());
	return redisCacheManager;
}

/**
 * 配置shiro redisManager
 * 使用的是shiro-redis开源插件
 *
 * @return
 */
public RedisManager redisManager() {
	RedisManager redisManager = new RedisManager();
	redisManager.setHost(redisProperties.getHost());
	redisManager.setPort(redisProperties.getPort());
	redisManager.setDatabase(redisProperties.getDatabase());
	redisManager.setTimeout(redisProperties.getTimeout());
	redisManager.setPassword(redisProperties.getPassword());
	return redisManager;
}

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

/**
 * shiro session的管理
 */
@Bean
public DefaultWebSessionManager sessionManager() {
	DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
	sessionManager.setGlobalSessionTimeout(redisProperties.getExpire() * 1000L);
	sessionManager.setSessionDAO(redisSessionDAO());
	return sessionManager;
}

/**
 * cookie对象;
 *
 * @return
 */
public SimpleCookie rememberMeCookie() {
	// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
	SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
	// 记住我cookie生效时间30天 ,单位秒。 注释掉,默认永久不过期
	simpleCookie.setMaxAge(redisProperties.getExpire());
	//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
	//设为true后,只能通过http访问,javascript无法访问
	//防止xss读取cookie
	simpleCookie.setHttpOnly(true);
	simpleCookie.setPath("/");
	return simpleCookie;
}

Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after no timeout

错误原因:连接超时时间设置的过于短暂 我在配置文件中设置了0
解决:将spring.redis.timeout值修改成几秒左右差不多 改值单位为毫秒

密码加密及登录次数验证

Shiro-密码凭证匹配器

package com.blackcat.blog.common.shiro.credentials;

import com.blackcat.blog.util.PasswordUtil;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

/**
 * <p> 描述 : Shiro-密码凭证匹配器
 * @author : blackcat
 * @date  : 2020/2/14 11:20
*/
public class CredentialsMatcher extends SimpleCredentialsMatcher {

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken utoken = (UsernamePasswordToken) token;
        //获得用户输入的密码:(可以采用加盐(salt)的方式去检验)
        String inPassword = new String(utoken.getPassword());
        //获得数据库中的密码
        String dbPassword = (String) info.getCredentials();
        try {
            dbPassword = PasswordUtil.decrypt(dbPassword, utoken.getUsername());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        //进行密码的比对
        return this.equals(inPassword, dbPassword);
    }
}

Shiro-密码输入错误的状态下重试次数的匹配管理

package com.blackcat.blog.common.shiro.credentials;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.blackcat.blog.common.holder.RequestHolder;
import com.blackcat.blog.core.entity.SysUser;
import com.blackcat.blog.core.service.SysUserService;
import com.blackcat.blog.util.IpUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * <p> 描述 : Shiro-密码输入错误的状态下重试次数的匹配管理
 * @author : blackcat
 * @date  : 2020/2/14 11:26
*/
public class RetryLimitCredentialsMatcher extends CredentialsMatcher {

    public static final String USER_SESSION_KEY = "user";

    /**
     * 用户登录次数计数  redisKey 前缀
     */
    private static final String SHIRO_LOGIN_COUNT = "shiro_login_count_";
    /**
     * 用户登录是否被锁定    一小时 redisKey 前缀
     */
    private static final String SHIRO_IS_LOCK = "shiro_is_lock_";
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SysUserService userService;

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        SysUser shiroUser = (SysUser) info.getPrincipals().getPrimaryPrincipal();
        SysUser user = userService.getOne(new QueryWrapper<SysUser>().eq("id",shiroUser.getId()));
        String username = user.getUsername();
        // 访问一次,计数一次
        ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
        String loginCountKey = SHIRO_LOGIN_COUNT + username;
        String isLockKey = SHIRO_IS_LOCK + username;
        opsForValue.increment(loginCountKey, 1);

        if (redisTemplate.hasKey(isLockKey)) {
            throw new ExcessiveAttemptsException("帐号[" + username + "]已被禁止登录!");
        }

        // 计数大于5时,设置用户被锁定一小时
        String loginCount = String.valueOf(opsForValue.get(loginCountKey));
        int retryCount = (5 - Integer.parseInt(loginCount));
        if (retryCount <= 0) {
            opsForValue.set(isLockKey, "LOCK");
            redisTemplate.expire(isLockKey, 1, TimeUnit.HOURS);
            redisTemplate.expire(loginCountKey, 1, TimeUnit.HOURS);
            throw new ExcessiveAttemptsException("由于密码输入错误次数过多,帐号[" + username + "]已被禁止登录!");
        }

        boolean matches = super.doCredentialsMatch(token, info);
        if (!matches) {
            String msg = retryCount <= 0 ? "您的账号一小时内禁止登录!" : "您还剩" + retryCount + "次重试的机会";
            throw new AccountException("帐号或密码不正确!" + msg);
        }

        //清空登录计数
        redisTemplate.delete(loginCountKey);
        try {
            SysUser sysUser= new SysUser();
            sysUser.setLastLoginIp(IpUtil.getRealIp(RequestHolder.getRequest()));
            sysUser.setLastLoginTime(LocalDateTime.now());
            sysUser.setLoginCount(user.getLoginCount() + 1);
            userService.update(user, new UpdateWrapper<SysUser>().eq("id", user.getId()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 当验证都通过后,把用户信息放在session里
        // 注:User必须实现序列化
        SecurityUtils.getSubject().getSession().setAttribute(USER_SESSION_KEY, user);
        return true;
    }
}

ShiroRealm 身份认证修改

/**
 * <p> 描述 : 身份认证
 * @author : blackcat
 * @date  : 2020/2/1 12:32
 * @return org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
	String username = (String) authenticationToken.getPrincipal();
	String userPwd = new String((char[]) authenticationToken.getCredentials());
	// 可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
	SysUser user = sysUserService.getOne(new QueryWrapper<SysUser>().eq("username", username));
	if (user == null) {
		throw new UnknownAccountException("账号不存在!");
	}
	if (user.getStatus() != null && 0==user.getStatus()) {
		throw new LockedAccountException("帐号已被锁定,禁止登录!");
	}
	// principal参数使用用户Id,方便动态刷新用户权限
	return new SimpleAuthenticationInfo(
			user,
			user.getPassword(),
			ByteSource.Util.bytes(username),
			getName()
	);
}

Shiro 配置修改

/**
 * <p> 描述 : 身份认证realm
 * @author : blackcat
 * @date  : 2020/2/4 16:37
*/
@Bean
public ShiroRealm shiroRealm(){
	ShiroRealm shiroRealm = new ShiroRealm();
	// 使用加密凭证
	shiroRealm.setCredentialsMatcher(credentialsMatcher());
	return shiroRealm;
}

修改数据库密码

只是修改代码没有修改数据库的密码会报:Input length must be multiple of 16 when decrypting with padded cipher
root:CGUx1FN++xS+4wNDFeN6DA==
admin:gXp2EbyZ+sB/A6QUMhiUJQ==

使用说明

用户:root 密码:123456
用户:admin 密码:123456
访问:http://localhost:8083/login

示例代码

简单demo 无相关实体类数据库 :https://gitee.com/kylin_lawliet/springboot-demos/tree/master/springboot-shiro
实现登陆跳转主页 附数据库:https://gitee.com/kylin_lawliet/springboot-demos/tree/master/springboot-shiro2

相关注解

@RequiresAuthentication

验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。

@RequiresUser

验证用户是否被记忆,user有两种含义:
  一种是成功登录的(subject.isAuthenticated() 结果为true);
  另外一种是被记忆的(subject.isRemembered()结果为true)。

@RequiresGuest

验证是否是一个guest的请求,与@RequiresUser完全相反。
  换言之,RequiresUser == !RequiresGuest。
  此时subject.getPrincipal() 结果为null.

@RequiresRoles

例如:@RequiresRoles(“aRoleName”);
  void someMethod();
  如果subject中有aRoleName角色才可以访问方法someMethod。如果没有这个权限则会抛出异常AuthorizationException。

@RequiresPermissions

例如: @RequiresPermissions({“file:read”, “write:aFile.txt”} )
  void someMethod();
  要求subject中必须同时含有file:read和write:aFile.txt的权限才能执行方法someMethod()。否则抛出异常AuthorizationException。

相关文章

Shiro官方文档
Shiro 学习博客系列 SpringBoot
Shiro 学习博客系列 spring
SpringBoot2.0 集成Shiro 基础版
SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录 有源码
Springboot+shiro权限管理系统 有源码
Spring Boot + Mybatis-Plus + Apache Shiro + FreeMarker 制作的通用权限管理
springboot2.0、mybatis-plus、shiro技术栈开发的vblog博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值