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博客