shiro

本文围绕Shiro权限管理框架展开,介绍了认证和授权概念,解析核心组件如Subject、SecurityManager等。阐述了非web环境使用、自定义realm、密码加密、缓存管理等内容,还涉及简单权限项目搭建,包括权限模型、基础模块管理,最后提及Shiro高级操作如分布式缓存、无状态会话等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


title: shiro
date: 2021-07-19 11:02:58
tags:

shiro介绍

  1. 区分认证和授权。
  • 认证(authentication): 用户登录,能够访问系统
  • 授权(authority): 用户有没有相应的权限能够访问该资源
  1. 授权即访问控制,控制谁能访问哪些资源,主题进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。通俗理解为主体(Subject)对系统资源(resource)有什么权限(Permission)

  2. 几个组成部分
    3.1 主体

  • 身份信息:主题进行认真的标识,必须唯一,用户名,手机号,邮箱,脸,指纹
  • 凭证信息:只有主体自己知道的安全信息,如密码,证书等。
    3.2 资源: 系统能提供的功能和服务。
    3.3 权限: 一个主体能够访问哪些资源,主体能够使用哪些功能和服务。

权限管理的解决方案

  1. url拦截,通过springmvc的拦截器和filter,对每一个请求进行拦截,判断是否需要认证,在判断是否需要权限。
    请添加图片描述

  2. 权限框架

  • shiro:简单易用
  • springsecurity 复杂,入门门槛高

核心组件详解

  1. Subject(主体):subject是一个接口,定义了很多的认证授权相关的方法,通过securityManager安全管理器进行认证授权
  2. SecurityManager: 安全管理器,是shiro的核心,负责对所有的subject进行安全管理,securityManager通过Authentictor进行认证,通过Authorizer进行授权,通过sessionManager进行会话管理。
  3. 认证器: Authentictor,对主体进行认证
  4. 授权器: Authorizer,对主题进行授权,在访问资源时,判断用户有无此功能的操作权限。
  5. Realm: SecurityManager进行安全认证需要通过realm获取用户权限数据。有多种实现,从配置文件或者数据库中读取用户权限信息。
  • 将用户的权限信写到配置文件
  • 存储到数据库
  1. sessionManager: 会话管理器,shiro自己的会话管理器,和httpsession不同,可以用于非web环境,也可以将分布式应用的会话集中在一点管理。
  2. sessionDAO: 对session会话操作的一套接口,比如要将会话信息存储到redis数据库。
  3. cacheMananger: 缓存管理器,提高性能
  4. cryptography:密码管理器,提供一套加密解密的组件,提供常用的散列,加密方法

非web环境下的使用

  1. 直接上代码
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

  1. 上面是将权限信息存储到配置文件中,但是更常用的是将数据存储到数据库中,因此需要自定一个realm。
  2. 常用的realm有
  • AuthenticatingRealm: 认证realm
  • AuthorizingRealm: 授权realm
  1. 自定义的授权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;
    }
}
  1. 几种常见异常
  • UnknownAccountException: 用户名不存在
  • CredentialsException: 凭证不合法
  • DisabledAccountException: 账号被禁用
  • LockedAccountException: 账号被锁定
  • ExpiredCredentialsException: 凭证过期(登录超时)
  • AuthenticationException: 认证异常

密码加密

  1. 散列算法就是将任意类型的数据都能散列固定长度字符串,但是随着散列数据的增多,暴力破解的成功率越来越高,但是同一个文件,加上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);
    }

}
  1. 能够加密之后,数据库中存储的就是密文了,这样我们收到前端发送的密码,首先要加密,在和数据库中的进行对比,如果相等,认证通过。
  2. 为了进一步加强保密,后端保存一个公共盐,用户注册的时候在生成一个私有盐

缓存管理

  1. 如果不设置缓存管理器,那么每次查询用户权限的时候都要从数据库中查询,效率不高,因此,将权限信息放入缓存中,可以大幅提高速度
public void stst() {
        MemoryConstrainedCacheManager memoryConstrainedCacheManager = new MemoryConstrainedCacheManager();;
        DefaultSecurityManager securityManager = ((DefaultSecurityManager) SecurityUtils.getSecurityManager());
        securityManager.setCacheManager(memoryConstrainedCacheManager);
    }

简单权限项目的搭建

权限模型

  1. 首先权限模型,分为用户,角色,资源(包含权限),其关系是一个用户多个角色,一个角色多个资源,一个用户多个资源。
  2. 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层的操作

  1. 认证的接口用my-plus自带的方法查询用户名和密码,做个校验就可以,不展开
  2. 授权的接口
    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
  1. 一般是用parent_id表示层级关系
    请添加图片描述
平铺用编码表示
  1. 如下如,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递归语句进行查询
  1. 不是所有的数据库都支持递归查询,mysql就不支持。
  2. 数据库递归执行的速度比较慢。
程序执行
  1. 使用for循环,每次执行到一个节点就去数据库中查询。
  2. 一次性将数据全部加载到内存,在内存中生成树结构

用户管理

  1. 普通的CRUD罢了,不展开
  2. 新增用户,校验用户数据
  3. 删除用户,删除时应将给该用户分配的角色信息一并删除(sys_user_role)

角色管理

  1. 普通的CRUD罢了,不展开
  2. 新增/修改角色,校验角色名称是否重复或空
  3. 删除角色,将sys_user_role表中该角色的信息一并删除,将sys_role_resource表中该角色的信息一并删除
  4. 批量删除

资源管理

  1. 删除资源,如果该资源下有子资源,不能删除,只能删除叶子节点,将sys_role_resource中的该资源信息一并删除
  2. 批量删除资源
  3. 资源查询出来应该是和用户对应身份的资源树,下面是树的代码
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;
    }
}
  1. 可以注意到,如果想要构造一棵树,因为不知道节点的父亲节点,需要有很多无谓的递归,因此,在资源表中添加一个path字段,这个字段记录,他的父亲的目录,这样省去很多递归。注意path要包含自己的资源id,这样方便查找某一级目录下的所有资源。
    请添加图片描述

  2. 查询某个资源节点的下的所有资源,用path+/+resource_id,就能查出当前节点下所有资源id.

-- 包括节点管理自身
SELECT * FROM sys_resource WHERE path like '0/1/2%'

--不包括节点自身
SELECT * FROM sys_resource WHERE path like '0/1/2/%'
  1. 查询用户管理下的所有资源

// 给出一个节点查询该节点下的所有节点,并构造成一颗树

// 查询出当前节点
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);

给用户分配角色

  1. 用户在分配角色方面有两个接口
  • 展示所有角色,且该用户的角色处于选中状态
-- 使用这条语句,才能正确的查询所有角色并且显示当前用户的角色,因为过滤的时机不同,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;

给角色分配资源

  1. 和上面的过程基本上一样
-- 查询所有角色,并且角色拥有的资源被选中。
SELECT * FROM sys_resource r LEFT JOIN sys_role_resource rr on r.resource_id = rr.resource_id and rr.role_id = 2;

动态展示菜单

  1. 用户登陆时,根据用户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;
    }
  1. 另外登录时是被shiro拦截,会经过自定义realm,用户的认证信息会存储到简单认证对象中(simpleAuthenticationInfo),在获取目录树的时候,通过SecurityUtil.getSubject().getPrinvipal()拿到凭证对象,从凭证对象中拿到用户id并进行存储。

路径控制

  1. 在上面的章节中我们通过从数据库中查询数据,动态的展示了左侧的目录菜单,每个用户只能看到自己的菜单,但是相应的菜单url并没有做路径控制,用户还是可以通过url直接访问到相关数据,因此我们要使用shiro注解限制相关的权限
  2. 直接在要控制访问的controller方法上面加注解,@RequiresPermissions("sys:user:list")

shiro高级操作

分布式缓存

自定义realm

  1. 自定义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设置到运行环境中,自动帮我们设置了

}

分布式会话

  1. 先不忙,现在都是jwt,无状态会话,因此暂时不写。

无状态会话

  1. 因为http是无状态的协议,因此每一次http请求都是新得请求,那么服务器就无法知道是哪个用户得请求,少了功能不说,如果每个请求服务器都不知道用户是谁,b那么就是每一次请求都需要重新登录,显然不现实,因此有状态会话产生了,将用户信息存储在服务端,返回sessionId,用户每次发送请求携带sessionId。就可以读取用户之前的状态,但是这样的话会加重服务端的负载,并且,在当今分布式的时代,用户请求可能会被负载到不同的服务器上,还需要实现分布式会话,加重了成本。经典的就是session和cookie技术
    请添加图片描述

  2. 无状态会话是用户登录之后,将用户的有关信息,加密使用token之后,授权信息不需要缓存,因为每次请求都相当于一次新的请求。会话也不需要了
    请添加图片描述

jwt

  1. 那么关键的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

  1. 由于不再使用用户名和密码进行校验,而是使用token,因此自定义realm中的authentication就需要重新写。
  2. 代码
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

  1. 无状态下的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

  1. 由于每次都是携带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

  1. 生成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

  1. 先攒着,之后处理
@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

  1. 生成树的工具类
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;
    }

}
  1. 树节点,这只是最简单的节点,做资源树时,可以添加一些节点
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

  1. 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;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值