大家好,最近在忙着做权限控制项目,少有发布blog了,领导让我实现一个用户,角色,权限细粒度的权限控制功能集成到项目中,我经过了一定的调研之后,使用shiro,jwt实现了该功能,以此篇blog记之。
shiro介绍
Apache Shiro是Java的一个安全框架。目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
本教程只介绍基本的Shiro使用,不会过多分析源码等,重在使用。
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
接下来我们分别从外部和内部来看看Shiro的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的API,且API契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。
首先,我们从外部来看Shiro吧,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个Shiro应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。
接下来我们来从Shiro内部来看下Shiro的架构,如下图所示:
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
一、开发前准备
(1)由上面的介绍我们可以知道shiro的Realm 可以进行自定义数据源,那是不是就会想到我们能否把用户,角色,权限信息存储到数据库中呢?答案是:肯定的。
在mysql 数据库中 执行 如下sql 脚本:
分别创建sys_menu: 相当于权限表,
sys_role:角色表
sys_role_menu: 角色权限关联表
sys_user: 用户表
sys_user_role: 用户角色关联表
---------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL,
`name` varchar(50) NOT NULL,
`route_name` varchar(255) DEFAULT NULL COMMENT '路由名称,用于前端跳转,唯一',
`url` varchar(100) DEFAULT NULL,
`perms` text,
`icon` varchar(50) DEFAULT NULL,
`type` tinyint(2) NOT NULL,
`order_num` int(11) DEFAULT NULL,
`component` varchar(200) DEFAULT NULL,
`component_name` varchar(500) DEFAULT NULL,
`redirect` varchar(200) DEFAULT NULL,
`iframe_url` varchar(1000) DEFAULT NULL COMMENT '外部链接,嵌入iframe',
`is_route` tinyint(2) DEFAULT '0',
`always_show` tinyint(2) DEFAULT '0',
`is_leaf` tinyint(2) DEFAULT '0',
`hidden` tinyint(2) DEFAULT '0',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`create_by` varchar(50) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`update_by` varchar(50) DEFAULT NULL,
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='菜单权限表';
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', null, '系统管理', 'system', '/system', null, 'setting', '0', '99', 'layouts/TabLayout', null, '/system/user', null, '1', '0', '0', '0', null, null, '2020-05-14 16:11:35', 'caojun');
INSERT INTO `sys_menu` VALUES ('2', '1', '用户管理', 'user', '/system/user', 'user:list', 'user', '1', '1', 'system/UserList', 'UserList', null, null, '1', '0', '1', '0', null, null, '2020-07-03 18:30:30', 'admin');
INSERT INTO `sys_menu` VALUES ('3', '1', '角色管理', 'role', '/system/role', 'role:list', 'team', '1', '2', 'system/RoleList', 'RoleList', null, null, '1', '0', '1', '0', null, null, '2020-05-07 16:21:53', 'admin');
INSERT INTO `sys_menu` VALUES ('4', '1', '菜单管理', 'permission', '/system/permission', 'menu:list', 'security-scan', '1', '3', 'system/PermissionList', 'PermissionList', null, null, '1', '0', '1', '0', null, null, '2020-05-07 16:21:47', 'admin');
INSERT INTO `sys_menu` VALUES ('5', '1', '字典管理', 'dictionary', '/system/dictionary', null, 'read', '1', '4', 'system/DictionaryList', null, null, null, '1', '0', '1', '0', '2020-05-03 13:43:00', 'admin', '2020-05-07 16:22:06', 'admin');
INSERT INTO `sys_menu` VALUES ('7', '1', '日志管理', 'log', '/system/log', null, 'book', '1', '5', 'system/LogList', null, null, null, '1', '0', '1', '0', '2020-05-05 10:00:00', 'admin', '2020-05-07 16:22:12', 'admin');
INSERT INTO `sys_menu` VALUES ('8', null, '个人中心', 'personal', '/personal', null, 'user', '0', '2', 'layouts/TabLayout', null, '/personal/base', null, '1', '0', '0', '1', '2020-05-07 14:30:00', 'admin', '2020-05-14 13:59:55', 'caojun');
INSERT INTO `sys_menu` VALUES ('9', '8', '基本信息', 'personalBase', '/personal/base', null, 'idcard', '1', '3', 'personal/base', null, null, null, '1', '0', '1', '0', '2020-05-07 14:35:00', 'admin', '2020-05-13 15:05:28', 'caojun');
INSERT INTO `sys_menu` VALUES ('11', '8', '我的申请', 'personalApply', '/personal/apply', null, 'profile', '1', '2', 'personal/apply', null, null, null, '1', '0', '1', '0', '2020-05-13 14:55:02', 'admin', null, null);
INSERT INTO `sys_menu` VALUES ('12', '8', '修改密码', 'personalPwd', '/personal/pwd', null, 'key', '1', '4', 'personal/pwd', null, null, null, '1', '0', '1', '0', '2020-05-13 14:58:00', 'admin', '2020-05-13 14:58:22', 'admin');
INSERT INTO `sys_menu` VALUES ('36', null, '地图台', null, '/', null, 'global', '0', '1', 'layouts/MapLayout', null, '/home', null, '1', '0', '0', '0', '2020-05-22 15:17:00', 'admin', '2020-07-03 14:26:34', 'caojun');
INSERT INTO `sys_menu` VALUES ('37', '36', '视频', null, '/home', null, null, '1', '1', 'video/index', null, null, null, '1', '0', '1', '0', '2020-05-22 15:18:00', 'admin', '2020-07-03 14:27:48', 'caojun');
INSERT INTO `sys_menu` VALUES ('39', '36', '基站', null, '/station', null, null, '1', '4', 'station/index', null, null, null, '1', '0', '1', '0', '2020-06-02 13:39:00', 'admin', '2020-08-14 15:39:11', 'caojun');
INSERT INTO `sys_menu` VALUES ('40', '36', '警力', null, '/police', null, null, '1', '5', 'police/index', null, null, null, '1', '0', '1', '0', '2020-06-02 13:41:00', 'admin', '2020-08-14 15:39:21', 'caojun');
INSERT INTO `sys_menu` VALUES ('41', '36', '警车', null, '/car', null, null, '1', '6', 'car/index', null, null, null, '1', '0', '1', '0', '2020-06-02 13:43:00', 'admin', '2020-08-14 15:39:31', 'caojun');
INSERT INTO `sys_menu` VALUES ('42', '36', '警情', null, '/alarm', null, '', '1', '3', 'layouts/RouteView', null, null, null, '1', '0', '0', '0', '2020-06-02 13:46:00', 'admin', '2020-08-14 15:39:02', 'admin');
INSERT INTO `sys_menu` VALUES ('53', '36', '实战', null, '/training', null, null, '1', '7', 'training/index', null, null, null, '1', '0', '1', '0', '2020-07-03 14:25:00', 'caojun', '2020-08-11 11:34:19', null);
INSERT INTO `sys_menu` VALUES ('54', '36', '人房', null, '/subject', null, null, '1', '2', 'subject/index', null, null, null, '1', '0', '1', '0', '2020-07-03 14:26:00', 'caojun', '2020-08-14 15:38:40', null);
INSERT INTO `sys_menu` VALUES ('55', '42', '实时警情', null, '/alarm/policeCase', null, 'pic-center', '1', '1', 'alarm/policeCase/index', null, null, null, '1', '0', '1', '0', '2020-07-15 15:45:00', 'caojun', '2020-08-05 09:59:40', 'admin');
INSERT INTO `sys_menu` VALUES ('56', '42', '警情热力图', null, '/alarm/heatMap', null, 'radar-chart', '1', '2', 'alarm/heatMap/index', null, null, null, '1', '0', '1', '0', '2020-07-17 16:52:00', 'admin', '2020-08-05 09:59:54', 'admin');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`role_key` varchar(100) NOT NULL COMMENT '角色唯一标识',
`role_name` varchar(100) DEFAULT NULL COMMENT '角色名称',
`remark` varchar(1000) DEFAULT NULL COMMENT '描述',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系统角色表';
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'SUPER_ADMIN', '超级管理员', null, null, '2020-07-17 18:01:25');
INSERT INTO `sys_role` VALUES ('2', 'ADMIN', '管理员', '测试', null, '2020-07-22 09:53:37');
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`uid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`role_key` varchar(100) NOT NULL COMMENT '角色标识',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=784 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('741', 'SUPER_ADMIN', '1');
INSERT INTO `sys_role_menu` VALUES ('742', 'SUPER_ADMIN', '2');
INSERT INTO `sys_role_menu` VALUES ('743', 'SUPER_ADMIN', '6');
INSERT INTO `sys_role_menu` VALUES ('744', 'SUPER_ADMIN', '3');
INSERT INTO `sys_role_menu` VALUES ('745', 'SUPER_ADMIN', '4');
INSERT INTO `sys_role_menu` VALUES ('746', 'SUPER_ADMIN', '5');
INSERT INTO `sys_role_menu` VALUES ('747', 'SUPER_ADMIN', '7');
INSERT INTO `sys_role_menu` VALUES ('748', 'SUPER_ADMIN', '8');
INSERT INTO `sys_role_menu` VALUES ('749', 'SUPER_ADMIN', '9');
INSERT INTO `sys_role_menu` VALUES ('750', 'SUPER_ADMIN', '11');
INSERT INTO `sys_role_menu` VALUES ('751', 'SUPER_ADMIN', '12');
INSERT INTO `sys_role_menu` VALUES ('752', 'SUPER_ADMIN', '36');
INSERT INTO `sys_role_menu` VALUES ('753', 'SUPER_ADMIN', '37');
INSERT INTO `sys_role_menu` VALUES ('754', 'SUPER_ADMIN', '39');
INSERT INTO `sys_role_menu` VALUES ('755', 'SUPER_ADMIN', '40');
INSERT INTO `sys_role_menu` VALUES ('756', 'SUPER_ADMIN', '41');
INSERT INTO `sys_role_menu` VALUES ('757', 'SUPER_ADMIN', '42');
INSERT INTO `sys_role_menu` VALUES ('758', 'SUPER_ADMIN', '53');
INSERT INTO `sys_role_menu` VALUES ('759', 'SUPER_ADMIN', '54');
INSERT INTO `sys_role_menu` VALUES ('760', 'SUPER_ADMIN', '55');
INSERT INTO `sys_role_menu` VALUES ('761', 'SUPER_ADMIN', '56');
INSERT INTO `sys_role_menu` VALUES ('762', 'SUPER_ADMIN', '57');
INSERT INTO `sys_role_menu` VALUES ('763', 'ADMIN', '6');
INSERT INTO `sys_role_menu` VALUES ('764', 'ADMIN', '2');
INSERT INTO `sys_role_menu` VALUES ('765', 'ADMIN', '1');
INSERT INTO `sys_role_menu` VALUES ('766', 'ADMIN', '3');
INSERT INTO `sys_role_menu` VALUES ('767', 'ADMIN', '4');
INSERT INTO `sys_role_menu` VALUES ('768', 'ADMIN', '5');
INSERT INTO `sys_role_menu` VALUES ('769', 'ADMIN', '7');
INSERT INTO `sys_role_menu` VALUES ('770', 'ADMIN', '8');
INSERT INTO `sys_role_menu` VALUES ('771', 'ADMIN', '9');
INSERT INTO `sys_role_menu` VALUES ('772', 'ADMIN', '11');
INSERT INTO `sys_role_menu` VALUES ('773', 'ADMIN', '12');
INSERT INTO `sys_role_menu` VALUES ('774', 'ADMIN', '36');
INSERT INTO `sys_role_menu` VALUES ('775', 'ADMIN', '37');
INSERT INTO `sys_role_menu` VALUES ('776', 'ADMIN', '39');
INSERT INTO `sys_role_menu` VALUES ('777', 'ADMIN', '40');
INSERT INTO `sys_role_menu` VALUES ('778', 'ADMIN', '41');
INSERT INTO `sys_role_menu` VALUES ('779', 'ADMIN', '42');
INSERT INTO `sys_role_menu` VALUES ('780', 'ADMIN', '53');
INSERT INTO `sys_role_menu` VALUES ('781', 'ADMIN', '54');
INSERT INTO `sys_role_menu` VALUES ('782', 'ADMIN', '55');
INSERT INTO `sys_role_menu` VALUES ('783', 'ADMIN', '56');
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(128) NOT NULL COMMENT '密码',
`name` varchar(200) DEFAULT NULL COMMENT '正式姓名',
`dept_id` int(11) DEFAULT NULL COMMENT '部门ID',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
`ssex` tinyint(1) DEFAULT NULL COMMENT '性别',
`avatar` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL,
`last_login_time` datetime DEFAULT NULL,
`theme` varchar(10) DEFAULT NULL,
`status` tinyint(1) NOT NULL COMMENT '状态',
`description` varchar(100) DEFAULT NULL,
`adcode` varchar(10) DEFAULT NULL COMMENT '城市编码',
`region_id` varchar(32) DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系统用户表';
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', '超管', '1', '[email protected]', '13455533222', '1', null, '2019-05-06 14:31:37', '2020-06-08 11:09:44', '2019-05-07 10:11:23', 'indigo', '1', '我是管理员', '361100', '3611');
INSERT INTO `sys_user` VALUES ('3', 'caojun', '3d9f549610dc8eaef5ebe305b7a31f0b', '曹军', null, '[email protected]', null, '1', 'default.jpg', '2020-05-02 11:11:00', '2020-05-13 15:04:43', null, 'green', '1', null, null, null);
INSERT INTO `sys_user` VALUES ('5', 'test123', '8e7dc6b8522024ee33db550c7f14d671', 'scott', null, null, null, null, 'default.jpg', '2020-05-02 14:32:52', null, null, 'green', '1', null, null, null);
INSERT INTO `sys_user` VALUES ('6', 'yingang', '23ead6f6bd37eaf3e70d76c171dddd91', '殷刚', null, null, null, '1', 'default.jpg', '2020-05-23 16:13:41', null, null, 'green', '1', null, null, null);
INSERT INTO `sys_user` VALUES ('7', 'chenjing', 'a31804c9ef2aeac31388f2da1235766d', '陈景', null, null, null, '1', 'default.jpg', '2020-06-28 16:31:34', null, null, 'green', '1', null, null, null);
INSERT INTO `sys_user` VALUES ('8', '123yhf', '794b1a4bd91606df7396a0ff7a570ad0', 'yhf', null, null, null, '1', 'default.jpg', '2020-07-31 16:10:52', null, null, 'green', '1', null, null, null);
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`uid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`username` varchar(100) NOT NULL COMMENT '账号',
`role_key` varchar(100) NOT NULL COMMENT '角色标识',
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户角色关系表';
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', 'admin', 'SUPER_ADMIN');
INSERT INTO `sys_user_role` VALUES ('4', 'caojun', 'ADMIN');
INSERT INTO `sys_user_role` VALUES ('6', 'yingang', 'SUPER_ADMIN');
INSERT INTO `sys_user_role` VALUES ('7', 'chenjing', 'ADMIN');
INSERT INTO `sys_user_role` VALUES ('8', '123yhf', 'ADMIN');
二、添加依赖
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
三、代码编写
JwtTokenController : 模拟登陆获取token的接口,大家可以根据实际项目需求,采用真实的登陆接口,如果登陆成功,则使用用户的信息生成token,返回给前端,前端在调用接口的时候需要在请求头header中加入参数
X-Access-Token:生成的token
package com.sf.gis.boot.rcboot.controller;
import com.sf.gis.boot.rcboot.shiro.JWTUtil;
import com.sf.gis.boot.rcboot.util.JsonResponse;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 80004819
* @ClassName:
* @Description:
* @date 2020年09月11日 17:38:09
*/
@RestController
@RequestMapping("/jwt")
@Slf4j
@Api(tags = "JWT权限controller")
public class JwtTokenController {
/**
* 模拟登陆接口获取到token,有效期为30分钟
*
* @return
*/
@GetMapping("/getToken")
public JsonResponse getToken() {
try {
//String token = JWTUtil.createToken(new User("admin", "123456"));
String token = JWTUtil.sign("admin", "123456", false);
return JsonResponse.ok(JsonResponse.STATUS_SUCCESS, token);
} catch (Exception e) {
log.error("error", e);
return JsonResponse.error("获取token失败");
}
}
}
JWTUtil : 创建和校验token工具类
此处可以自定义一个秘钥和用户的信息一起进行生成token,并设置一个过期时间,此处的rememberMe实际上就是过期时间设置的特别长,如果不适用rememberMe ,则默认过期时间是30分钟(半小时)。
注意:在生成token 和 验证token 的时候,放入的claim中的数据体一定要保持一致,否则会校验失败。
package com.sf.gis.boot.rcboot.shiro;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.sf.gis.boot.rcboot.constants.Common;
import com.sf.gis.boot.rcboot.shiro.entity.SysUser;
import com.sf.gis.boot.rcboot.util.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
@Slf4j
public class JWTUtil {
public static final long EXPIRE_TIME = SpringContextUtils.getBean(ShiroProperties.class).getJwtTimeOut() * 1000;
public static final String MY_SECRET = "my_secret";
/**
* 生成 token
*
* @param username 用户名
* @return token
*/
public static String sign(String username,String password,Boolean rememberMe) {
long expireTime = null != rememberMe && rememberMe ? EXPIRE_TIME : 30 * 60 * 1000 ;
try {
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(MY_SECRET+password);
String userinfo = Common.GSON.toJson(new SysUser().setUsername(username).setPassword(password));
return JWT.create()
.withClaim("userinfo", userinfo)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
log.error("error:{}", e);
return null;
}
}
/**
* 校验 token是否正确
*
* @param token 密钥
* @return 是否正确
*/
public static boolean verify(String token,String username,String password) {
try {
Algorithm algorithm = Algorithm.HMAC256(MY_SECRET+password);
String userinfo = Common.GSON.toJson(new SysUser().setUsername(username).setPassword(password));
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("userinfo", userinfo)
.build();
verifier.verify(token);
// log.info("token is valid");
return true;
} catch (Exception e) {
log.info("token is invalid{}", e.getMessage());
return false;
}
}
}
JWTToken : 实现AuthenticationToken 接口,shiro 在获取subject之后的login操作需要自定义一个token对象传入
package com.sf.gis.boot.rcboot.shiro;
import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;
/**
* JSON Web Token
*/
@Data
public class JWTToken implements AuthenticationToken {
private static final long serialVersionUID = 1282057025599826155L;
private String token;
private String exipreAt;
public JWTToken(String token) {
this.token = token;
}
public JWTToken(String token, String exipreAt) {
this.token = token;
this.exipreAt = exipreAt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
JWTFilter:继承BasicHttpAuthenticationFilter 类,添加@WebFilter注解,同时在 RcBootApplication 项目启动类添加
@ServletComponentScan 注解,会自动扫描 过滤器将之注册到spring容器中。
package com.sf.gis.boot.rcboot.shiro;
import com.google.gson.Gson;
import com.sf.gis.boot.rcboot.util.JsonResponse;
import com.sf.gis.boot.rcboot.util.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 80004819
* @ClassName:
* @Description:
* @date 2020年09月11日 17:34:49
*/
@Slf4j
@WebFilter(filterName = "JwtFilter", urlPatterns = "/*")
public class JwtFilter extends BasicHttpAuthenticationFilter {
private static final Gson GSON = new Gson();
private static final String TOKEN = "X-Access-Token";
/**
* 所有请求直接走login
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
throw new AuthenticationException("Token失效,请重新登录", e);
}
}
/**
* 判断是否是登陆请求
*
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(TOKEN);
String referer = req.getHeader("Referer");
if (token == null) {
try {
//如果响应还没有被提交,重定向到/
if (!response.isCommitted()) {
request.getRequestDispatcher("/").forward(request, response);
// WebUtils.issueRedirect(request, response, "/");
}
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
return false;
}
return true;
}
/**
* 执行登陆校验
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(TOKEN);
JWTToken jwtToken = new JWTToken(token);
try {
getSubject(request, response).login(jwtToken);
return true;
} catch (Exception e) {
log.error(e.getMessage());
return false;
}
}
/**
*如果校验失败会执行此方法
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
ShiroProperties geoProperties = SpringContextUtils.getBean(ShiroProperties.class);
String loginUrl = geoProperties.getLoginUrl();
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
if (response.isCommitted()) {
return false;
}
// WebUtils.issueRedirect(request, response, loginUrl);
response.getWriter().write(GSON.toJson(JsonResponse.error("token校验不通过")));
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
ShiroProperties : ShiroFilterFactoryBean 相关常量配置
package com.sf.gis.boot.rcboot.shiro;
import lombok.Data;
import org.springframework.stereotype.Component;
@Data
@Component
public class ShiroProperties {
// shiro redis缓存时长,默认值 1800 秒
private int expireIn = 1800;
// session 超时时间,默认 1800000毫秒
private long sessionTimeout = 1800000L;
// rememberMe 有效时长,默认为 86400 秒,即一天
private int cookieTimeout = 86400;
private String anonUrl;
private String loginUrl = "/login";
private String successUrl = "/index";
private String logoutUrl = "/logout";
private String unauthorizedUrl;
/**
* token默认有效时间 1天
*/
private Long jwtTimeOut = 86400L;
}
ShiroUtil : shrio工具类
package com.sf.gis.boot.rcboot.shiro;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.sf.gis.boot.rcboot.constants.Common;
import com.sf.gis.boot.rcboot.shiro.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
/**
* @author 80004819
* @ClassName:
* @Description:
* @date 2020年09月14日 15:10:11
*/
@Slf4j
public class ShiroUtil {
/**
* 获取subject
*
* @return
*/
public static Subject getSubject() {
try {
Subject subject = SecurityUtils.getSubject();
return subject;
} catch (Exception e) {
log.error("未登录", e);
return null;
}
}
/**
* 获取session
*
* @return
*/
public static Session getSession() {
return getSubject().getSession();
}
/**
* 获取session
*
* @return
*/
protected Session getSession(Boolean flag) {
return getSubject().getSession(flag);
}
/**
* 从 token中获取用户名
*
* @return token中包含的用户名
*/
public static String getUsername() {
return getUsername(null);
}
public static String getUsername(String token) {
SysUser sysUser = getUserInfo(token);
return StrUtil.isNotBlank(sysUser.getUsername()) ? sysUser.getUsername() : null;
}
/**
* 将token解密成SysUser对象
*
* @param token
* @return
*/
public static SysUser getUserInfo(String token) {
if (StrUtil.isEmpty(token)) {
if (null == getSubject() || null == getSubject().getPrincipal()) return null;
token = getSubject().getPrincipal().toString();
}
try {
DecodedJWT jwt = JWT.decode(token);
String userinfo = jwt.getClaim("userinfo").asString();
// JsonObject info = Common.GSON.fromJson(userinfo, JsonObject.class);
SysUser sysUser = Common.GSON.fromJson(userinfo, SysUser.class);
return sysUser;
} catch (JWTDecodeException e) {
log.error("error:{}", e.getMessage());
return null;
}
}
}
ShiroRealm : 自定义Realm
- 重写 doGetAuthorizationInfo方法: 授权方法,获取用户的角色和权限
- 重写 doGetAuthenticationInfo方法:认证方法,验证用户身份,用户名密码,token是否正确
package com.sf.gis.boot.rcboot.shiro;
import cn.hutool.core.util.StrUtil;
import com.sf.gis.boot.rcboot.shiro.entity.SysMenu;
import com.sf.gis.boot.rcboot.shiro.entity.SysRole;
import com.sf.gis.boot.rcboot.shiro.entity.SysUser;
import com.sf.gis.boot.rcboot.shiro.service.SysMenuService;
import com.sf.gis.boot.rcboot.shiro.service.SysRoleService;
import com.sf.gis.boot.rcboot.shiro.service.SysUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author 80004819
* @ClassName:
* @Description:
* @date 2020年09月14日 15:33:22
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Autowired
private SysMenuService menuService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 授权模块,获取用户角色和权限
*
* @param principal principal
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
// User user = (User) SecurityUtils.getSubject().getPrincipal();
// String userName = user.getUsername();
String username = ShiroUtil.getUsername(principal.toString());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取用户角色集
List<SysRole> roleList = this.roleService.findUserRole(username);
Set<String> roleSet = roleList.stream().map(role -> role.getRoleName()).collect(Collectors.toSet());
simpleAuthorizationInfo.setRoles(roleSet);
// 获取用户权限集
List<SysMenu> permissionList = this.menuService.findUserPermissions(username);
Set<String> permissionSet = new HashSet<>();
for (SysMenu m : permissionList) {
// 处理用户多权限 用逗号分隔
permissionSet.addAll(Arrays.asList(m.getPerms().split(",")));
}
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* 用户认证
*
* @param authenticationToken AuthenticationToken 身份认证 token
* @return AuthenticationInfo 身份认证信息
* @throws AuthenticationException 认证相关异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户输入的用户名和密码
// String userName = (String) token.getPrincipal();
// String password = new String((char[]) token.getCredentials());
//
// if (User.STATUS_LOCK.equals(user.getStatus())) {
// throw new LockedAccountException("账号已被锁定,请联系管理员!");
// }
String token = (String) authenticationToken.getCredentials();
SysUser sysUser = ShiroUtil.getUserInfo(token);
String username = sysUser.getUsername();
String password = sysUser.getPassword();
if (StrUtil.isBlank(username))
throw new AuthenticationException("账号为空,token校验不通过");
// 通过用户名查询用户信息
SysUser user = this.userService.findByNamePassword(username,password);
if (user == null)
throw new AuthenticationException("用户名或密码错误");
// JsonObject jo = new JsonObject();
// jo.addProperty("username", username);
// jo.addProperty("userid", user.getUserId());
if (!JWTUtil.verify(token, username, password))
throw new AuthenticationException("token校验不通过");
// 判断用户状态
if (null == user.getStatus() || user.getStatus() != 1) {
throw new AuthenticationException("账号已被冻结,请联系管理员!");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
}
ShiroConfig
package com.sf.gis.boot.rcboot.shiro;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
/**
* @author 80004819
* @ClassName:
* @Description:
* @date 2020年09月14日 15:21:28
*/
@Configuration
public class ShiroConfig {
@Autowired
private ShiroProperties shiroProperties;
//配置文件中配置项:用于设置是否开启对url的权限验证
@Value("${jwt.tokenAuth.enable}")
private boolean tokenAuth;
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录的 url
shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl());
// 登录成功后跳转的 url
shiroFilterFactoryBean.setSuccessUrl(shiroProperties.getSuccessUrl());
// 未授权 url
shiroFilterFactoryBean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl());
// 在 Shiro过滤器链上加入 JWTFilter
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("user", new JwtFilter());
shiroFilterFactoryBean.setFilters(filters);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置退出过滤器,其中具体的退出代码 Shiro已经替我们实现了
filterChainDefinitionMap.put(shiroProperties.getLogoutUrl(), "logout");
//配置免认证的url,url可以从配置文件读取
filterChainDefinitionMap.put("/about", "anon");
filterChainDefinitionMap.put("/jwt/getToken", "anon");
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/error", "anon");
filterChainDefinitionMap.put("/csrf", "anon");
filterChainDefinitionMap.put("/v2/api-docs/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
if (tokenAuth) {
//除上以外所有url都必须认证通过才可以访问,未通过认证自动访问 LoginUrl
filterChainDefinitionMap.put("/**", "user");
} else {
//所有url都可以直接匿名访问
filterChainDefinitionMap.put("/**", "anon");
}
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 配置url过滤器
// @Bean
// public ShiroFilterChainDefinition shiroFilterChainDefinition() {
// DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
//
// chainDefinition.addPathDefinition(shiroProperties.getLogoutUrl(), "logout");
// //配置免认证的url,url可以从配置文件读取
// chainDefinition.addPathDefinition("/login", "anon");
// chainDefinition.addPathDefinition("/about", "anon");
// chainDefinition.addPathDefinition("/jwt/getToken", "anon");
// chainDefinition.addPathDefinition("/", "anon");
// chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
// chainDefinition.addPathDefinition("/error", "anon");
// chainDefinition.addPathDefinition("/csrf", "anon");
// chainDefinition.addPathDefinition("/v2/api-docs/**", "anon");
// chainDefinition.addPathDefinition("/swagger-resources/**", "anon");
// //除上以外所有url都必须认证通过才可以访问,未通过认证自动访问 LoginUrl
// chainDefinition.addPathDefinition("/**", "user");
// return chainDefinition;
// }
@Bean("securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,并注入 shiroRealm
securityManager.setRealm(shiroRealm());
// 配置 rememberMeCookie
securityManager.setRememberMeManager(rememberMeManager());
// securityManager.setSessionManager(sessionManager());
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
// shiro 生命周期处理器
return new LifecycleBeanPostProcessor();
}
@Bean
public ShiroRealm shiroRealm() {
// 配置 Realm,需自己实现
return new ShiroRealm();
}
/**
* rememberMe cookie 效果是重开浏览器后无需重新登录
*
* @return SimpleCookie
*/
private SimpleCookie rememberMeCookie() {
// 设置 cookie 名称,对应 login.html 页面的 <input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// cookie.setSecure(true); // 只在 https中有效 注释掉 正常
// 设置 cookie 的过期时间,单位为秒,这里为一天
cookie.setMaxAge(shiroProperties.getCookieTimeout());
return cookie;
}
/**
* cookie管理对象
*
* @return CookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie 加密的密钥
//cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
cookieRememberMeManager.setCipherKey(new AesCipherService().generateNewKey().getEncoded());
return cookieRememberMeManager;
}
/**
* DefaultAdvisorAutoProxyCreator 和 AuthorizationAttributeSourceAdvisor 用于开启 shiro 注解的使用
* 如 @RequiresAuthentication, @RequiresUser, @RequiresPermissions 等
*
* @return DefaultAdvisorAutoProxyCreator
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
SpringContextUtils
package com.sf.gis.boot.rcboot.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Spring Context 工具类
*
* @author MrBird
*
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class<?> getType(String name) {
return applicationContext.getType(name);
}
/**
* 获取HttpServletRequest
*/
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
RcBootApplication
package com.sf.gis.boot.rcboot;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
//@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, SecurityFilterAutoConfiguration.class})
@SpringBootApplication
@MapperScan(basePackages = {"com.sf.gis.boot.rcboot.mapper","com.sf.gis.boot.rcboot.shiro.mapper"})
@EnableSwagger2
@EnableScheduling
@EnableTransactionManagement
@ServletComponentScan
public class RcBootApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(RcBootApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(RcBootApplication.class, args);
}
}
项目 配置文件查看文末 gitee 仓库。
四、功能测试
- 首先,我们不调用获取token接口,直接请求任一业务接口,发现会返回token验证不通过的信息
- 然后,我们调用getToken接口,获取到token,并在下次请求任一业务接口的时候在请求头header中添加参数
X-Access-Token :生成的token
发现可以认证成功,并且接口请求成功返回想要的数据信息。
五、shiro注解式权限控制
Shiro共有5个注解,可以在controller接口层添加这些注解实现更加细粒度的权限控制
-
RequiresAuthentication:
使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
-
RequiresGuest:
使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。
-
RequiresPermissions:
当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
-
RequiresRoles:
当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
-
RequiresUser
当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
使用方法:
Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):
RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest
示例:
RequiresRoles
//属于user角色
@RequiresRoles("user")
//必须同时属于user和admin角色
@RequiresRoles({"user","admin"})
//属于user或者admin之一;修改logical为OR 即可
@RequiresRoles(value={"user","admin"},logical=Logical.OR)
RequiresPermissions
//符合index:hello权限要求
@RequiresPermissions("index:hello")
//必须同时复核index:hello和index:world权限要求
@RequiresPermissions({"index:hello","index:world"})
//符合index:hello或index:world权限要求即可
@RequiresPermissions(value={"index:hello","index:world"},logical=Logical.OR)
附录:
shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置过滤指定url的访问权限。
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
shiro常用的权限控制注解,可以在控制器类上使用
注解 | 功能 |
---|---|
@RequiresGuest | 只有游客可以访问 |
@RequiresAuthentication | 需要登录才能访问 |
@RequiresUser | 已登录的用户或“记住我”的用户能访问 |
@RequiresRoles | 已登录的用户需具有指定的角色才能访问 |
@RequiresPermissions | 已登录的用户需具有指定的权限才能访问 |