title: shiro
date: 2021-07-19 11:02:58
tags:
shiro介绍
- 区分认证和授权。
- 认证(authentication): 用户登录,能够访问系统
- 授权(authority): 用户有没有相应的权限能够访问该资源
-
授权即访问控制,控制谁能访问哪些资源,主题进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。通俗理解为主体(Subject)对系统资源(resource)有什么权限(Permission)
-
几个组成部分
3.1 主体
- 身份信息:主题进行认真的标识,必须唯一,用户名,手机号,邮箱,脸,指纹
- 凭证信息:只有主体自己知道的安全信息,如密码,证书等。
3.2 资源: 系统能提供的功能和服务。
3.3 权限: 一个主体能够访问哪些资源,主体能够使用哪些功能和服务。
权限管理的解决方案
-
url拦截,通过springmvc的拦截器和filter,对每一个请求进行拦截,判断是否需要认证,在判断是否需要权限。
-
权限框架
- shiro:简单易用
- springsecurity 复杂,入门门槛高
核心组件详解
- Subject(主体):subject是一个接口,定义了很多的认证授权相关的方法,通过securityManager安全管理器进行认证授权
- SecurityManager: 安全管理器,是shiro的核心,负责对所有的subject进行安全管理,securityManager通过Authentictor进行认证,通过Authorizer进行授权,通过sessionManager进行会话管理。
- 认证器: Authentictor,对主体进行认证
- 授权器: Authorizer,对主题进行授权,在访问资源时,判断用户有无此功能的操作权限。
- Realm: SecurityManager进行安全认证需要通过realm获取用户权限数据。有多种实现,从配置文件或者数据库中读取用户权限信息。
- 将用户的权限信写到配置文件
- 存储到数据库
- sessionManager: 会话管理器,shiro自己的会话管理器,和httpsession不同,可以用于非web环境,也可以将分布式应用的会话集中在一点管理。
- sessionDAO: 对session会话操作的一套接口,比如要将会话信息存储到redis数据库。
- cacheMananger: 缓存管理器,提高性能
- cryptography:密码管理器,提供一套加密解密的组件,提供常用的散列,加密方法
非web环境下的使用
- 直接上代码
public void test1() {
// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 从ini配置文件中读取用户权限信息
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
// 使用工具类将安全管理器设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
// 使用上面传入的安全管理器创建主体
Subject subject = SecurityUtils.getSubject();
// 创建token令牌,记录用户认证的身份和凭证(即账号和密码)
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
// 用户登录认证
subject.login(token);
System.out.println("用户认证状态:"+subject.isAuthenticated());
// 用户角色授权
System.out.println("是否拥有admin角色"+subject.hasRole("admin"));
// 用户资源授权状态
System.out.println("是否拥有删除产品权限"+subject.isPermitted("product:delete"));
subject.logout();
}
自定义realm
- 上面是将权限信息存储到配置文件中,但是更常用的是将数据存储到数据库中,因此需要自定一个realm。
- 常用的realm有
- AuthenticatingRealm: 认证realm
- AuthorizingRealm: 授权realm
- 自定义的授权realm常常继承AuthorizingRealm;
public class CustomRealm extends AuthorizingRealm {
/**
* 登录认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("登录认证");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
Object credentials = token.getCredentials();
// 从数据库中读取用户信息
if (!username.equals("fafd")) {
throw new UnknownAccountException("用户名不存在");
}
if (!credentials.equals("123456")) {
throw new CredentialsException("密码错误");
}
// 认证成功
// 创建简单用户信息对象,供shiro使用,用于存储到会话中
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), super.getName());
return simpleAuthenticationInfo;
}
/**
* 授权认证
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("授权认证");
// 这个就是上面认证方法传入的主体信息
String userName = principalCollection.getPrimaryPrincipal().toString();
// 模拟根据用户名从数据库中查询相应的权限信息
HashSet<String> roles = new HashSet<>();
roles.add("管理源");
roles.add("cj管理源");
// 模拟根据用户名从数据库中查询相应的权限信息
HashSet<String> permissions = new HashSet<>();
permissions.add("sys:user:list");
permissions.add("sys:user:create");
// 设置简单认证对象并返回
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
}
- 几种常见异常
- UnknownAccountException: 用户名不存在
- CredentialsException: 凭证不合法
- DisabledAccountException: 账号被禁用
- LockedAccountException: 账号被锁定
- ExpiredCredentialsException: 凭证过期(登录超时)
- AuthenticationException: 认证异常
密码加密
- 散列算法就是将任意类型的数据都能散列固定长度字符串,但是随着散列数据的增多,暴力破解的成功率越来越高,但是同一个文件,加上salt,就和之前的完全不同,salt就是混淆值。
public class MDtest {
public void test() {
String s = new Md5Hash("111111").toString();
System.out.println(s);
// 最后一个是散列次数
String saltafaf = new Md5Hash("111111", "saltafaf", 1).toString();
System.out.println(saltafaf);
// 指定哈希算法
SimpleHash simpleHash = new SimpleHash("MD5", "11111", "salt112", 2);
System.out.println(simpleHash);
}
}
- 能够加密之后,数据库中存储的就是密文了,这样我们收到前端发送的密码,首先要加密,在和数据库中的进行对比,如果相等,认证通过。
- 为了进一步加强保密,后端保存一个公共盐,用户注册的时候在生成一个私有盐
缓存管理
- 如果不设置缓存管理器,那么每次查询用户权限的时候都要从数据库中查询,效率不高,因此,将权限信息放入缓存中,可以大幅提高速度
public void stst() {
MemoryConstrainedCacheManager memoryConstrainedCacheManager = new MemoryConstrainedCacheManager();;
DefaultSecurityManager securityManager = ((DefaultSecurityManager) SecurityUtils.getSecurityManager());
securityManager.setCacheManager(memoryConstrainedCacheManager);
}
简单权限项目的搭建
权限模型
- 首先权限模型,分为用户,角色,资源(包含权限),其关系是一个用户多个角色,一个角色多个资源,一个用户多个资源。
- 5张基本表
- sys_user
- sys_role
- sys_user_role
- sys_resource
- sys_role_resource
/*
Navicat Premium Data Transfer
Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 80019
Source Host : localhost:3306
Source Schema : shirodemo
Target Server Type : MySQL
Target Server Version : 80019
File Encoding : 65001
Date: 20/07/2021 17:39:20
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_resource
-- ----------------------------
DROP TABLE IF EXISTS `sys_resource`;
CREATE TABLE `sys_resource` (
`resource_id` int(0) NOT NULL,
`parent_id` int(0) NOT NULL COMMENT '父id',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '资源名称',
`url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源url',
`permission` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限',
`type` int(0) NOT NULL COMMENT '资源类型,0,目录,1,菜单,2,按钮',
`icon` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源图标',
`order_num` int(0) NULL DEFAULT NULL COMMENT '方便设置资源的属性',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`resource_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_resource
-- ----------------------------
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`role_id` int(0) NOT NULL,
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
`remark` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`dept_id` int(0) NULL DEFAULT NULL,
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
-- ----------------------------
-- Table structure for sys_role_resource
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_resource`;
CREATE TABLE `sys_role_resource` (
`id` int(0) NOT NULL,
`role_id` int(0) NOT NULL,
`resource_id` int(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `role_id`(`role_id`, `resource_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_resource
-- ----------------------------
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` int(0) NOT NULL COMMENT '用户id',
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`salt` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '盐',
`email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`dept_id` int(0) NOT NULL COMMENT '部门id',
`status` int(0) NOT NULL DEFAULT 0 COMMENT '0正常,1禁用,2锁定',
`deleted` int(0) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建日期',
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改日期',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` int(0) NOT NULL,
`user_id` int(0) NOT NULL,
`role_id` int(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `user_id`(`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
mapper层的操作
- 认证的接口用my-plus自带的方法查询用户名和密码,做个校验就可以,不展开
- 授权的接口
2.1 根据用户id查询用户的角色
SELECT
DISTINCT NAME
FROM
sys_user_role ur,
sys_role r
WHERE
r.role_id = ur.role_id
AND ur.user_id = 2;
2.2 根据用户id查询用户的权限,这样在用户登录的时候能动态的展示左侧的资源菜单
SELECT
DISTINCT r.permission
FROM
sys_resource r,
sys_user_role ur,
sys_role_resource rr
WHERE
r.permission IS NOT NULL
AND r.resource_id = rr.resource_id
AND rr.role_id = ur.role_id
AND ur.user_id = 1;
基础模块的管理
树形结构
parent_id
- 一般是用parent_id表示层级关系
平铺用编码表示
- 如下如,35表示福建省,01表示福州市,依次类推,但是想要拿到并解析成树状结构,还是有点麻烦的。
实体类的设计
public class TreeNode {
private Integer id;
private Integer parentId;
private String name;
private List<TreeNode> children = new ArrayList<>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<TreeNode> getChildren() {
return children;
}
public void setChildren(List<TreeNode> children) {
this.children = children;
}
}
查询
使用sql递归语句进行查询
- 不是所有的数据库都支持递归查询,mysql就不支持。
- 数据库递归执行的速度比较慢。
程序执行
- 使用for循环,每次执行到一个节点就去数据库中查询。
- 一次性将数据全部加载到内存,在内存中生成树结构
用户管理
- 普通的CRUD罢了,不展开
- 新增用户,校验用户数据
- 删除用户,删除时应将给该用户分配的角色信息一并删除(sys_user_role)
角色管理
- 普通的CRUD罢了,不展开
- 新增/修改角色,校验角色名称是否重复或空
- 删除角色,将sys_user_role表中该角色的信息一并删除,将sys_role_resource表中该角色的信息一并删除
- 批量删除
资源管理
- 删除资源,如果该资源下有子资源,不能删除,只能删除叶子节点,将sys_role_resource中的该资源信息一并删除
- 批量删除资源
- 资源查询出来应该是和用户对应身份的资源树,下面是树的代码
public class TreeUtil {
private List<TreeNode> treeNodeList;
public TreeUtil(List<TreeNode> treeNodeList) {
this.treeNodeList = treeNodeList;
}
/**
* 根据节点id,获取节点对象信息
* @param id
* @return
*/
private TreeNode getById(Integer id) {
// 遍历所有的节点
for (TreeNode treeNode : treeNodeList) {
if (treeNode.getId() == id){
return treeNode;
}
}
return null;
}
/**
* 根据节点id查询儿子(不是子孙)节点
* @param id
* @return
*/
private List<TreeNode> getChildrenById(Integer id) {
ArrayList<TreeNode> children = new ArrayList<>();
for (TreeNode treeNode : treeNodeList) {
if (treeNode.getParentId()==id) {
children.add(treeNode);
}
}
return children;
}
/**
* 生成树形结构数据
* @param id
* @return
*/
public TreeNode generateTree(Integer id) {
// 根据节点id查询该节点信息
TreeNode treeNode = getById(id);
// 根据节点id查询该节点下的所有的子节点
List<TreeNode> children = getChildrenById(id);
// 遍历子节点的儿子节点
for (TreeNode node : children) {
TreeNode child = generateTree(node.getId());
treeNode.getChildren().add(child);
}
return treeNode;
}
}
class TreeNode {
private Integer id;
private Integer parentId;
private String name;
private List<TreeNode> children = new ArrayList<>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<TreeNode> getChildren() {
return children;
}
public void setChildren(List<TreeNode> children) {
this.children = children;
}
}
-
可以注意到,如果想要构造一棵树,因为不知道节点的父亲节点,需要有很多无谓的递归,因此,在资源表中添加一个path字段,这个字段记录,他的父亲的目录,这样省去很多递归。注意path要包含自己的资源id,这样方便查找某一级目录下的所有资源。
-
查询某个资源节点的下的所有资源,用path+/+resource_id,就能查出当前节点下所有资源id.
-- 包括节点管理自身
SELECT * FROM sys_resource WHERE path like '0/1/2%'
--不包括节点自身
SELECT * FROM sys_resource WHERE path like '0/1/2/%'
- 查询用户管理下的所有资源
// 给出一个节点查询该节点下的所有节点,并构造成一颗树
// 查询出当前节点
Resource resource = resourceService.getById(id);
QueryWrapper<Resource> queryWrapper = new QueryWrapper<>();
queryWrapper.likeRight("path",resource.getPath());
// 查询出当前节点下的所有资源
List<Resource> resourceList = resourceService.list(queryWrapper);
ArrayList<TreeNode> treeNodeList = new ArrayList<>();
// 构建树节点list
for (Resource item : resourceList) {
TreeNode node = new TreeNode();
node.setId(item.getResourceId());
node.setName(item.getName());
node.setParentId(item.getParentId());
treeNodeList.add(node);
}
// 用这些节点来构造树
TreeUtil treeUtil = new TreeUtil(treeNodeList);
TreeNode node = treeUtil.generateTree(id);
给用户分配角色
- 用户在分配角色方面有两个接口
- 展示所有角色,且该用户的角色处于选中状态
-- 使用这条语句,才能正确的查询所有角色并且显示当前用户的角色,因为过滤的时机不同,and是在左表和右表连接的时候进行过滤,where是在最后结果的基础上进行过滤。
SELECT * FROM sys_role r LEFT JOIN sys_user_role ur on r.role_id = ur.role_id and ur.user_id = 2;
-- where 和左外连接中的and的区别
SELECT * FROM sys_role r LEFT JOIN sys_user_role ur on r.role_id = ur.role_id WHERE ur.user_id = 2;
-- 这条语句将查询没有分配role的用户时会出错
SELECT * FROM sys_role r LEFT JOIN sys_user_role ur on r.role_id = ur.role_id WHERE ur.user_id = 2 OR ur.user_id is NULL;
给角色分配资源
- 和上面的过程基本上一样
-- 查询所有角色,并且角色拥有的资源被选中。
SELECT * FROM sys_resource r LEFT JOIN sys_role_resource rr on r.resource_id = rr.resource_id and rr.role_id = 2;
动态展示菜单
- 用户登陆时,根据用户id查询对应的资源,并展示
SELECT
r.resource_id,
r.parent_id,
r.`name`,
r.url,
r.icon,
r.type
FROM
sys_resource r,
sys_user_role ur,
sys_role_resource rr
WHERE
r.resource_id = rr.resource_id
AND rr.role_id = ur.role_id
-- 不要类型为2(按钮的资源)
AND r.type != 2
AND ur.user_id = 1
ORDER BY order_num ASC;
public List<TreeNode> getMenuTreeByUserId(Integer userId) {
// 用上面的sql语句将该用户拥有的资源节点全部查出,包括根节点
List<TreeNode> treeNodeList = userMapper.selectMenuList(userId);
// 如果用户没有任何资源节点,就返回空集合而不是null
if (CollectionUtils.isEmpty(treeNodeList)) {
return new ArrayList<>();
}
// 用来存储父id是0的节点
ArrayList<Integer> nodeIds = new ArrayList<>();
treeNodeList.forEach(treeNode -> {
Integer tempParentId = treeNode.getParentId();
if (tempParentId == 0) {
nodeIds.add(tempParentId);
}
});
TreeUtil treeUtil = new TreeUtil(treeNodeList);
// 资源目录树
ArrayList<TreeNode> treeNodeData = new ArrayList<>();
for (Integer resourceId : nodeIds) {
// 根据用户拥有的一级目录节点id,在用户拥有的所有资源节点中组装树
treeNodeData.add(treeUtil.generateTree(resourceId));
}
return treeNodeData;
}
- 另外登录时是被shiro拦截,会经过自定义realm,用户的认证信息会存储到简单认证对象中(simpleAuthenticationInfo),在获取目录树的时候,通过
SecurityUtil.getSubject().getPrinvipal()
拿到凭证对象,从凭证对象中拿到用户id并进行存储。
路径控制
- 在上面的章节中我们通过从数据库中查询数据,动态的展示了左侧的目录菜单,每个用户只能看到自己的菜单,但是相应的菜单url并没有做路径控制,用户还是可以通过url直接访问到相关数据,因此我们要使用shiro注解限制相关的权限
- 直接在要控制访问的controller方法上面加注解,
@RequiresPermissions("sys:user:list")
shiro高级操作
分布式缓存
自定义realm
- 自定义realm继承AuthorizingRealm,实现授权检查和认证检查两个方法,返回简单认证对象和简单授权对象,和用户登录时创建的subject绑定,可以通过securityUtils获得,在上面有例子
shiro配置文件
@Configuration
public class ShiroConfig1 {
/**
* 配置安全管理器
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入自定义realm,无状态会话采用的realm不同于平常的realm,需要换成jwt的
securityManager.setRealm(new JWTRealm());
return securityManager;
}
/**
* 配置权限过滤器
* @return
*/
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 注入安全管理器
bean.setSecurityManager(securityManager());
// 未认证的跳转地址,在controller中写一个路径方法来接受,用来返回json数据,return Result.error(401,"请先认证通过后在访问系统")
bean.setLoginUrl("/unauthorized");
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwtFilter",new JwtFilter());
// 设置过滤器链
bean.setFilters(filters);
LinkedHashMap<String, String> chain = new LinkedHashMap<>();
chain.put("/login","anon");// 登录url不拦截
chain.put("/css/**","anon");
// user过滤器会将为通过的用户踢到登录url去,但是我们这是用的token,需要实现自动认证,因此这里我们使用自定义的过滤器
// 禁用会话,否则一个请求就会创建一个会话
chain.put("/**","noSessionCreation,jwtFilter");
// 设置过滤拦截链,按顺序拦截
bean.setFilterChainDefinitionMap(chain);
return bean;
}
/**
* 启用shiro内部生命周期管理
* @return
*/
@Bean(name = "lifecycleBeanProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 启用AOP代理,让shiro自己代理,因此需要shiro自己管理自己的生命周期,不能删除,删除后shiro的权限注解会失效
* @return
*/
@Bean
@DependsOn("lifecycleBeanProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
/**
* 启用shiro注解
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
//securityUtil将securityManager设置到运行环境中,自动帮我们设置了
}
分布式会话
- 先不忙,现在都是jwt,无状态会话,因此暂时不写。
无状态会话
-
因为http是无状态的协议,因此每一次http请求都是新得请求,那么服务器就无法知道是哪个用户得请求,少了功能不说,如果每个请求服务器都不知道用户是谁,b那么就是每一次请求都需要重新登录,显然不现实,因此有状态会话产生了,将用户信息存储在服务端,返回sessionId,用户每次发送请求携带sessionId。就可以读取用户之前的状态,但是这样的话会加重服务端的负载,并且,在当今分布式的时代,用户请求可能会被负载到不同的服务器上,还需要实现分布式会话,加重了成本。经典的就是session和cookie技术
-
无状态会话是用户登录之后,将用户的有关信息,加密使用token之后,授权信息不需要缓存,因为每次请求都相当于一次新的请求。会话也不需要了
jwt
- 那么关键的token如何生成呢,这我们使用jwt技术,详情看这里
public class JwtTest {
/**
* 生成token
*/
public void testJwtCreate() {
JwtBuilder builder = Jwts.builder()
.setId("888") //设置唯一编号
.setSubject("admin")//设置主题 可以是JSON数据
.claim("perms","{'sys:user:add','sys:user:update'}")
.claim("roles","{'admin','root'}")
.setExpiration( new Date( )+1000*10 )//过期时间
.setIssuedAt(new Date())//设置签发日期
.signWith(SignatureAlgorithm.HS256, "hahaha");//设置签名 使用HS256算法,并设置SecretKey(字符串),hahaah就是你的密钥
//构建 并返回一个字符串
System.out.println(builder.compact());
}
/**
* 解析token
*/
public void testJwtParse() {
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIyNTM3NTQsImV4cCI6MTU2MjI1Mzc4Mywicm9sZXMiOiJhZG1pbiJ9.CY6CMembCi3mAkBHS3ivzB5w9uvtZim1HkizRu2gWaI";
Claims claims = Jwts.parser().setSigningKey( "hahaha" ).parseClaimsJws( token ).getBody();
System.out.println(claims);
System.out.println(claims.get( "roles" ));
}
}
tokenAuthenticcation
- 由于不再使用用户名和密码进行校验,而是使用token,因此自定义realm中的authentication就需要重新写。
- 代码
public class JWTToken implements AuthenticationToken {
private String jwtToken;
public JWTToken(String jwtToken) {
this.jwtToken = jwtToken;
}
/**
* 获取主题(用户名)
* @return
*/
@Override
public Object getPrincipal() {
return jwtToken;
}
/**
* 获取凭证(密码),因为使用密码生成token之后就不会在使用密码,因此密码可以随意返回
* @return
*/
@Override
public Object getCredentials() {
return Boolean.TRUE;
}
}
realm
- 无状态下的realm也不同,由原来的从数据库中查询数据转变为从jwttoken中解析数据。
public class JWTRealm extends AuthorizingRealm {
private static final String SECRETKEY = "coalis";
/**
* 配置该realm只支持JWTToken,否则会报UnsupportedAuthentication错误
* @return
*/
@Override
public Class getAuthenticationTokenClass() {
return JWTToken.class
}
/**
* 登录认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("登录认证");
// 转换成我们自定义的认证信息
JwtToken token = (JwtToken) authenticationToken;
String principal = (String) token.getPrincipal();
Claims claims;
User user = new User();
claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRETKEY))
.parseClaimsJws(token)
.getBody();
user.setId(claims.getId());
user.setUsername(claims.getId());
if (claims.get("roles")!=null) user.setRoles((String)claims.get("roles"));
if (claims.get("permissions")!=null) user.setPermissions((String)claims.get("permissions"))
// 创建简单用户信息对象,供shiro使用,用于存储到会话中
return new SimpleAuthenticationInfo(user,Boolean.TRUE,getName());
}
/**
* 授权认证
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 这个有待优化,因为token中不应该包含角色和权限等敏感信息,优化方案是根据用户名从数据中查询,并用spring缓存
log.info("授权认证");
// 这个就是上面认证方法传入的主体信息,就是简单对象里的第一个参数principal
User user = (User) principalCollection.getPrimaryPrincipal();
// 获取到json字符串
String roles = user.getRoles();
String permissions = user.getPermissions();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 解析json,可以用jackso或者fastjson,推荐使用fastjson,api简单易懂
info.addRoles(JSON.parseArray(roles,String.class));
info.addStringPermissions(JSON.parseArray(permissions,String.class));
return info;
}
}
jwtFilter
- 由于每次都是携带token,因此用一个filter来过滤,拦截不符合的请求,减少服务器压力
/**
* @author AZrookie
* @create 2021/8/11 0:41
* 自定义jwt的过滤器,自定义过滤规则,如果用默认过滤器user,会出现无法自动登录,有token,但是无法访问
*/
public class JWTFilter extends AccessControlFilter {
/**
* 这两个方法都是请求到来之前的处理,前置处理
*
* @param servletRequest
* @param servletResponse
* @param o
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
// 判断用户是否已经使用token登录
// 用户是否携带token访问
// 校验用户是否已经使用携带的token登录
// Subject subject = SecurityUtils.getSubject();
// if (subject!=null&& subject.isAuthenticated()) {
// System.out.println("token success.....");
// return true;
// }
// System.out.println("token无效");
// 这里可以直接返回false,因为token是无状态会话,isAuthenticated肯定是false。因此直接false返回就行
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setContentType("text/html;charset=UTF-8");
// response.setCharacterEncoding(); 这个是设置request编码的
try {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 在请求头中写死token的名字
String jwt = request.getHeader("jwt");
if (StringUtils.isEmpty(jwt)) {
return false;
}
// 调用subject的方法进行登录,会跳转到JwtRealm中进行认证
JWTToken jwtToken = new JWTToken(jwt);
Subject subject = SecurityUtils.getSubject();
// 发起对主体的认证,因为login方法一定会走realm中的认证和授权。因此就在这里抛出异常
subject.login(jwtToken);
// 但是这里抛异常controllerAdvice是无法捕获到异常的,因为这是filter,前置过滤器,请求还未到达controller,但是这里有request,可以直接写回请求
// 但是授权异常是可以捕获到的,因为注解是代理的,在controller层,抛出异常会被统一异常捕获
} catch (ExpiredJwtException e) {
throw new AuthenticationException("token过期" + e.getMessage());
} catch (UnsupportedJwtException e) {
throw new AuthenticationException("token无效" + e.getMessage());
} catch (MalformedJwtException e) {
throw new AuthenticationException("token格式错误" + e.getMessage());
String jsonString = JSON.toJSONString(Result.error(401, "token格式错误"));
response.getWriter().write(jsonString);
// 注意要return false,因为response的流并不是传输完成之后就断开连接,如果不返回false,就会走到下面的true,相当于又发送了一次请求
return false;
} catch (SignatureException e) {
throw new AuthenticationException("token签名无效" + e.getMessage());
} catch (IllegalArgumentException e) {
throw new AuthenticationException("token参数异常" + e.getMessage());
} catch (Exception e) {
throw new AuthenticationException("token 令牌错误" + e.getMessage());
}
return true;
}
}
jwtUtil
- 生成token工具类
public class JWTUtil {
// 私钥
private static final String SECRETKEY = "coalis";
/**
* 生成一个jwt方法
* @param subject 用户id
* @param roles 有效时间
* @param permissions 角色名称{"admin","superadmin"}
* @param expire 权限名称 {"sys:user:add","sys:user:list"}
* @return
*/
public static String generateToken(String subject,String roles,String permissions,Long expire) {
JwtBuilder builder = Jwts.builder()
// 设置token的唯一标识
.setId(UUID.randomUUID().toString()) //设置唯一编号
.setSubject(subject)//设置主题 可以是JSON数据
.setIssuer("coal")//设置签发者
.setIssuedAt(new Date());//设置签发日期
// 设置有效时间
if (null!=expire) {
Date expiration = new Date(System.currentTimeMillis() + expire);
builder.setExpiration(expiration);
}
if (StringUtils.isNotBlank(roles)) builder.claim("roles",roles); //设置角色
if (StringUtils.isNotBlank(permissions)) builder.claim("perms",permissions); // 设置权限
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(SECRETKEY);
builder.signWith(SignatureAlgorithm.HS256,secretKeyBytes);
}
}
controller
- 先攒着,之后处理
@RestController
@RequestMapping("/resource")
public class resourceTestCotroller {
@Autowired
private IResourceService resourceService;
// 查询某个资源节点下的所有资源子节点
@GetMapping("/test")
public void testResource(Integer id) {
Resource resource = resourceService.getById(id);
QueryWrapper<Resource> queryWrapper = new QueryWrapper<>();
queryWrapper.likeRight("path", resource.getPath());
List<Resource> resourceList = resourceService.list(queryWrapper);
ArrayList<TreeNode> treeNodeList = new ArrayList<>();
for (Resource item : resourceList) {
TreeNode node = new TreeNode();
node.setId(item.getResourceId());
node.setName(item.getName());
node.setParentId(item.getParentId());
treeNodeList.add(node);
}
TreeUtil treeUtil = new TreeUtil(treeNodeList);
TreeNode node = treeUtil.generateTree(id);
}
// 查询用户对应id所拥有的资源树菜单
@RequiresPermissions("sys:user:list")
public List<TreeNode> getMenuTreeByUserId(Integer userId) {
// 用上面的sql语句将该用户拥有的资源节点全部查出,包括根节点
List<TreeNode> treeNodeList = userMapper.selectMenuList(userId);
// 如果用户没有任何资源节点,就返回空集合而不是null
if (CollectionUtils.isEmpty(treeNodeList)) {
return new ArrayList<>();
}
// 用来存储父id是0的节点
ArrayList<Integer> nodeIds = new ArrayList<>();
treeNodeList.forEach(treeNode -> {
Integer tempParentId = treeNode.getParentId();
if (tempParentId == 0) {
nodeIds.add(tempParentId);
}
});
TreeUtil treeUtil = new TreeUtil(treeNodeList);
// 资源目录树
ArrayList<TreeNode> treeNodeData = new ArrayList<>();
for (Integer resourceId : nodeIds) {
// 根据用户拥有的一级目录节点id,在用户拥有的所有资源节点中组装树
treeNodeData.add(treeUtil.generateTree(resourceId));
}
return treeNodeData;
}
/**
* 用户名换取token,
* @param useename
* @param password
*/
public void auth(String useename,String password) {
// 1. 如果从数据库中查询有此用户,就通过,并返回token
List<String> roles = Arrays.asList("admin", "superadmin");
List<String> permissions = Arrays.asList("sys:user:add", "sys:user:update");
String token = JWTUtil.generateToken("admin",
JSON.toJSONString(roles), JSON.toJSONString(permissions), 1000L);
JWTToken jwtToken = new JWTToken(token);
return Result.OK(token);
}
}
treeUtil
- 生成树的工具类
public class TreeUtil {
private List<TreeNode> treeNodeList;
// 首先要传入要构造成树的所有元素
public TreeUtil(List<TreeNode> treeNodeList) {
this.treeNodeList = treeNodeList;
}
/**
* 根据节点id,获取节点对象信息
* @param id
* @return
*/
private TreeNode getById(Integer id) {
// 遍历所有的节点
for (TreeNode treeNode : treeNodeList) {
if (treeNode.getId() == id){
return treeNode;
}
}
return null;
}
/**
* 根据节点id查询儿子(不是子孙)节点
* @param id
* @return
*/
private List<TreeNode> getChildrenById(Integer id) {
ArrayList<TreeNode> children = new ArrayList<>();
for (TreeNode treeNode : treeNodeList) {
if (treeNode.getParentId()==id) {
children.add(treeNode);
}
}
return children;
}
/**
* 生成树形结构数据
* @param id
* @return
*/
public TreeNode generateTree(Integer id) {
// 根据节点id查询该节点信息
TreeNode treeNode = getById(id);
// 根据节点id查询该节点下的所有的子节点
List<TreeNode> children = getChildrenById(id);
// 遍历子节点的儿子节点
for (TreeNode node : children) {
TreeNode child = generateTree(node.getId());
treeNode.getChildren().add(child);
}
return treeNode;
}
}
- 树节点,这只是最简单的节点,做资源树时,可以添加一些节点
public class TreeNode {
private Integer id;
private Integer parentId;
private String name;
private List<TreeNode> children = new ArrayList<>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<TreeNode> getChildren() {
return children;
}
public void setChildren(List<TreeNode> children) {
this.children = children;
}
}
User
- jwt对应的用户model对象
public class User {
private String id;
private String username;
private String roles;
private String permissions;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
public String getPermissions() {
return permissions;
}
public void setPermissions(String permissions) {
this.permissions = permissions;
}
}