,最近在忙着做权限控制项目,少有发布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', 'admin@sfmail.com', '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, 'caojunior@163.com', 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');
二、添加依赖
<dependency> <groupId>com.auth0groupId> <artifactId>java-jwtartifactId> <version>3.10.3version> dependency> <dependency> <groupId>org.apache.shirogroupId> <artifactId>shiro-spring-boot-web-starterartifactId> <version>1.6.0version> dependency> <dependency> <groupId>cn.hutoolgroupId> <artifactId>hutool-allartifactId> <version>5.3.10version> dependency> <dependency> <groupId>com.google.code.gsongroupId> <artifactId>gsonartifactId> <version>2.8.4version> dependency> <dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <optional>trueoptional> 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; @Slf4jpublic 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 */@Datapublic 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@Componentpublic 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 */@Slf4jpublic 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 roleList = this.roleService.findUserRole(username); Set roleSet = roleList.stream().map(role -> role.getRoleName()).collect(Collectors.toSet()); simpleAuthorizationInfo.setRoles(roleSet); // 获取用户权限集 List permissionList = this.menuService.findUserPermissions(username); Set 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 */@Configurationpublic 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 filters = new LinkedHashMap<>(); filters.put("user", new JwtFilter()); shiroFilterFactoryBean.setFilters(filters); LinkedHashMap 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 页面的 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 * */@Componentpublic 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 getBean(Class clazz){ return applicationContext.getBean(clazz); } public static T getBean(String name, Class 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@ServletComponentScanpublic 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); } }
四、功能测试
- 首先,我们不调用获取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 | 已登录的用户需具有指定的权限才能访问 |