• 继认证后弄清Spring Security实现授权


    前言

    在简单学习完成Redis之后,又进行了Spring Security的学习,这里学习的资源为三更草堂SpringSecurity框架教程,讲得感觉还不错,推荐😁。

    这里主要是对学习过程进行一个记录和总结,参考的仍然是三更草堂的笔记,但中间加上了自己的一些理解和看法以及一些遇到的问题总结,如果哪里有问题还请各位指点指点😻。

    这里是基于前面一篇文章《清晰搞懂Spring Security的登录认证》进一步完善的,因此很多在另外一篇文章里面提及到的东西就不会过多的在这边进行重复赘述。

    1、权限系统

    1.1、引入


    例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。

    总结起来就是不同的用户有着自己的权力范围,从而可以使用不同的功能。这就是权限系统要去实现的效果。

    我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作,因此我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。

    1.2、RBAC权限模型


    **RBAC(Role-Based Access Control)——基于角色的访问控制。**这是目前最常被开发者使用也是相对易用、通用权限模型。在传统权限模型中,我们直接把权限赋予用户。而在RBAC中,增加了“角色”的概念,我们首先把权限赋予角色,再把角色赋予用户。这样,由于增加了角色,授权会更加灵活方便。

    image-20220719235530554

    在RBAC中,根据权限的复杂程度,又可分为RBAC0、RBAC1、RBAC2、RBAC3。其中,RBAC0是基础,RBAC1、RBAC2、RBAC3都是以RBAC0为基础的升级。我们可以根据自家产品权限的复杂程度,选取适合的权限模型。而在这里只会简单介绍基本模型RBAC0。

    RBAC0是基础,很多产品只需基于RBAC0就可以搭建权限模型了。在这个模型中,我们把权限赋予角色,再把角色赋予用户。用户和角色,角色和权限都是多对多的关系。用户拥有的权限等于他所有的角色持有权限之和。

    image-20220719235712352

    1.3、数据库设计


    如果将其分解进行数据库设计,RBAC0基本模型可以分成以下五个部分:

    • User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
    • Role(角色):不同角色具有不同的权限
    • Permission(权限):访问权限
    • 用户-角色映射:用户和角色之间的映射关系
    • 角色-权限映射:角色和权限之间的映射

    img

    转换成数据库表的话大致如下所示:

    image-20211222110249727

    数据库SQL,这里的sys_user和上一篇文章中的是相同的,不需要的小伙伴记得把这一块的SQL去掉:

    CREATE DATABASE IF NOT EXISTS db_security;
    
    USE db_security;
    
    -- ----------------------------
    -- Table structure for sys_menu
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_menu`;
    CREATE TABLE `sys_menu`  (
      `id` bigint(0) NOT NULL AUTO_INCREMENT,
      `menu_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
      `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由地址',
      `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',
      `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
      `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
      `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
      `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '#' COMMENT '菜单图标',
      `create_by` bigint(0) NULL DEFAULT NULL,
      `create_time` datetime(0) NULL DEFAULT NULL,
      `update_by` bigint(0) NULL DEFAULT NULL,
      `update_time` datetime(0) NULL DEFAULT NULL,
      `del_flag` int(0) NULL DEFAULT 0 COMMENT '是否删除(0未删除 1已删除)',
      `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of sys_menu
    -- ----------------------------
    INSERT INTO `sys_menu` VALUES (1, '部门管理', ' dept', ' system/dept/index', '0', '0', 'system:dept:list', '#', NULL, NULL, NULL, NULL, 0, NULL);
    INSERT INTO `sys_menu` VALUES (2, ' 测试', 'test', 'system/test/index', '0', '0', 'system:test:list', '#', NULL, NULL, NULL, NULL, 0, NULL);
    
    -- ----------------------------
    -- Table structure for sys_role
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role`;
    CREATE TABLE `sys_role`  (
      `id` bigint(0) NOT NULL AUTO_INCREMENT,
      `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色权限字符串',
      `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
      `del_flag` int(0) NULL DEFAULT 0 COMMENT 'del_flag',
      `create_by` bigint(0) NULL DEFAULT NULL,
      `create_time` datetime(0) NULL DEFAULT NULL,
      `update_by` bigint(0) NULL DEFAULT NULL,
      `update_time` datetime(0) NULL DEFAULT NULL,
      `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of sys_role
    -- ----------------------------
    INSERT INTO `sys_role` VALUES (1, 'CEO', 'ceo', '0', 0, NULL, NULL, NULL, NULL, NULL);
    INSERT INTO `sys_role` VALUES (2, 'Coder', ' coder', '0', 0, NULL, NULL, NULL, NULL, NULL);
    
    -- ----------------------------
    -- Table structure for sys_role_menu
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role_menu`;
    CREATE TABLE `sys_role_menu`  (
      `role_id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
      `menu_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '菜单id',
      PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of sys_role_menu
    -- ----------------------------
    INSERT INTO `sys_role_menu` VALUES (1, 1);
    INSERT INTO `sys_role_menu` VALUES (1, 2);
    INSERT INTO `sys_role_menu` VALUES (2, 2);
    
    -- ----------------------------
    -- Table structure for sys_user
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user`  (
      `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',
      `nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',
      `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',
      `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
      `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
      `phonenumber` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
      `sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
      `avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
      `user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
      `create_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人的用户id',
      `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
      `update_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人',
      `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
      `del_flag` int(0) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of sys_user
    -- ----------------------------
    INSERT INTO `sys_user` VALUES (1, 'xbaozi', '陈宝子', '$2a$10$WCD7xp6lxrS.PvGmL86nhuFHMKJTc58Sh0dG1EQw0zSHjlLFyFvde', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);
    INSERT INTO `sys_user` VALUES (2, 'test', '测试用户', '$10$WCD7xp6lxrS.PvGmL86nhuFHMKJTc58Sh0dG1EQw0zSHjlLFyFvde', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);
    
    -- ----------------------------
    -- Table structure for sys_user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user_role`;
    CREATE TABLE `sys_user_role`  (
      `user_id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '用户id',
      `role_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '角色id',
      PRIMARY KEY (`user_id`, `role_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of sys_user_role
    -- ----------------------------
    INSERT INTO `sys_user_role` VALUES (1, 1);
    INSERT INTO `sys_user_role` VALUES (2, 2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117

    2、上手干活

    2.1、思路分析


    这里同样对大致的思路流程分析一遍,方便后面的理解,但是是基于第一篇文章的基础上进行分析的,所以小伙伴们可以去隔壁《清晰搞懂Spring Security的登录认证》看一眼其中的2.3、大思路分析

    • 第一步,补充新的环境。如新的实体类和对应MVC架构的内容;

    • 第二步,编写Mapper方法从数据库中获取权限数据;

    • 第三步,在loadUserByUsername方法中根据用户查询权限信息添加到LoginUser中;

    • 第四步,在自定义过滤器中获取权限信息封装到Authentication

    • 第五步,启动方法前预授权

    2.2、补充新环境


    由于这里加入了新的表,因此在这里也需要加入新的实体类。

    @TableName(value="sys_menu")
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class Menu implements Serializable {
        private static final long serialVersionUID = -54979041104113736L;
    
        @TableId
        private Long id;
        /** 菜单名 */
        private String menuName;
        /** 路由地址 */
        private String path;
        /** 组件路径 */
        private String component;
        /** 菜单状态(0显示 1隐藏) */
        private String visible;
        /** 菜单状态(0正常 1停用) */
        private String status;
        /** 权限标识 */
        private String perms;
        /** 菜单图标 */
        private String icon;
    
        private Long createBy;
    
        private Date createTime;
    
        private Long updateBy;
    
        private Date updateTime;
        /** 是否删除(0未删除 1已删除) */
        private Integer delFlag;
        /** 备注 */
        private String remark;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.3、定义查询方法


    在MenuMapper查询获取数据库中的权限字段,这里和视频中的不同,视频中是采用在代码中直接多表查询,而我个人偏向于视图,所以在这里就自己建了一个视图,并在xml中进行sql查询(主要是偷懒没建视图对应的类,要不然直接使用MP)

    -- 视图创建
    CREATE VIEW user_perms_view AS SELECT
    ur.user_id,
    m.perms 
    FROM
    	sys_user_role ur
    	LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
    	LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
    	LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` 
    WHERE
    	r.`status` = 0 
    	AND m.`status` = 0 
    ORDER BY
    	ur.user_id;
    	
    -- 检测视图是否正常运行
    SELECT
    	perms 
    FROM
    	user_perms_view 
    WHERE
    	user_id = 1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    MenuMapper中定义listPermsByUserId方法

    @Repository
    public interface MenuMapper extends BaseMapper<Menu> {
        /**
         * @method listPermsByUserId
         * @description 根据用户id查询用户权限
         * @author xbaozi 
         * @date 2022/7/20 20:58 
         * @param: userId 
         * @return: java.util.List
         **/
        public List<String> listPermsByUserId(Long userId);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在对应的XML文件中编写SQL语句

    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="top.xbaoziplus.security.mapper.MenuMapper">
    
        <select id="listPermsByUserId" resultType="java.lang.String">
            SELECT
                DISTINCT perms
            FROM
                user_perms_view
            WHERE
                user_id = #{userId};
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.4、查询权限信息


    loadUserByUsername方法中根据用户查询权限信息 添加到LoginUser中,在这里值得注意的是,再查询出用户对应的权限信息后,我们还需要封装到UserDetails中进行返回,这时就需要对UserDetails的实现类LoginUser进行修改。

    • 添加了存储数据库中查询出来的权限信息的List集合permissions
    • 添加了存储SpringSecurity所需要的权限信息的集合
      • 这里需要注意的是因为SimpleGrantedAuthority无法序列化,如果直接使用,fastjson转换时就会出错,即Redis存取时会运行异常,因此我们需要使用@JSONField关闭其序列化;
    • 重写了getAuthorities方法,将数据库查询出来的权限信息转换成Spring Security所需要的权限信息的集合,这里使用到了函数式编程的stream进行转换。
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginUser implements UserDetails {
    
        private User user;
    
        /** 存储权限信息 */
        private List<String> permissions;
    
        /** 存储SpringSecurity所需要的权限信息的集合 */
        @JSONField(serialize = false)
        private List<GrantedAuthority> authorities;
    
        /** 重写getAuthorities方法,将String类型的权限List转换成GrantedAuthority类型的集合 */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            authorities = permissions.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
        @Override
        public String getUsername() {
            return user.getUserName();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    LoginUser修改完后我们就可以在UserDetailsServiceImpl中去把权限信息封装LoginUser中了

    @Service
    @Slf4j
    public class UserDetailsServiceImpl implements UserDetailsService {
        @Autowired
        private MenuMapper menuMapper;
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //根据用户名查询用户信息
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(User::getUserName,username);
            User user = userMapper.selectOne(wrapper);
            //如果查询不到数据就通过抛出异常来给出提示
            if(ObjectUtils.isEmpty(user)){
                throw new RuntimeException("用户名或密码错误");
            }
            // 根据用户查询权限信息 添加到LoginUser中
            List<String> perms = menuMapper.listPermsByUserId(user.getId());
            log.info("查询权限为{}", perms);
            //封装成UserDetails对象返回 
            return new LoginUser(user, perms, null);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2.5、过滤器鉴权


    对于权限鉴定,这是在Spring Security中进行的,因此我们就需要将获取到的权限信息封装到Authentication中。

    @Component
    @Slf4j
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        private RedisCache redisCache;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //获取token
            String token = request.getHeader("token");
            if (!StringUtils.hasText(token)) {
                log.info("无token");
                //放行
                filterChain.doFilter(request, response);
                return;
            }
            //解析token
            String userId;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                userId = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("token非法");
            }
            //从redis中获取用户信息
            String redisKey = "login:" + userId;
            LoginUser loginUser = redisCache.getCacheObject(redisKey);
            if(Objects.isNull(loginUser)){
                throw new RuntimeException("账号登录已超时,请重新登录");
            }
            //存入SecurityContextHolder,因为这里是认证,是假设已经登录之后的状态,所以参数列表分别为用户数据,空,鉴权信息
            // 获取权限信息封装到Authentication中
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            log.info("放行的路径为{}", request.getRequestURI());
            //放行
            filterChain.doFilter(request, response);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    不知道大家有没有想过这么一个问题,就是在这个自定义的过滤器中,我们不只是将权限信息添加到了SecurityContextHolder中吗,这只是一个存放的过程,框架是如何鉴权的呢?


    这其实就跟上一篇文章《清晰搞懂Spring Security的登录认证》中的2.1、过滤器链2.2、认证流程有一定的联系,图也同时贴在了下面。

    当我们仔细想想我们自定义的过滤器是为什么放在UsernamePasswordAuthenticationFilter前面的就很轻松理解这个问题了。

    • 在认证时我们知道是需要将用户信息存放到SecurityContextHolder中,然后让后方的UsernamePasswordAuthenticationFilter进行认证;
    • 同时在鉴权授权时也同样如此,将用户权限存放到SecurityContextHolder中,而后让后方的FilterSecurityInterceptor进行鉴权授权,我们自定义的过滤器只是起到一个信息获取存放的功能。

    image-20211214144425527

    2.6、启动预授权


    SpringSecurity为我们提供了基于注解的权限控制方案,我们可以使用注解去指定访问对应的资源所需的权限,但是这是需要我们去开启的,因此我们需要在SecurityConfig配置类上开启预授权功能。

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	// 内容没有变化,所以被啃了
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在开启了预授权之后,我们就可以使用@PreAuthorize注解对接口进行权限控制了,该注解中使用SPEL表达式进行权限配置,表达式中的参数为权限名,对应我们获取的perms集合。其中在编写过程中发现:

    • 直接加在controller上时,整个controller下的接口都接受该权限配置;
    • 单独加载某个接口上时是单一配置;
    • 以上两个都配置时遵循就近原则,即单独配置优先级更高。
    @RestController
    @PreAuthorize("hasAuthority('system:test:list')")
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello() {
            return "Hello";
        }
    
    
        @PreAuthorize("hasAuthority('system:test:list1')")
        @GetMapping("/hello1")
        public String hello1() {
            return "Hello1";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.7、运行测试


    在前面的数据库建表的时候可以看到xbaozi这个用户一共有两个权限,分别为 system:dept:list, system:test:list,而上面的测试接口按道理来说只有/hello接口可以访问,而/hello1接口没有权限访问。

    • 使用账号密码登录

    image-20220721000101292

    • 携带token数据访问/hello接口,成功访问

    image-20220721000158589

    • 携带token数据访问/hello1接口,没有权限,访问失败

    image-20220721000238579

    3、自定义异常处理器

    3.1、仔细分析


    这写着认证授权时我们会发现有些地方我们是手动抛了异常的,但是为什么在返回的数据中并没有我们想要的数据呢?

    image-20220721000445046

    这是因为在Spring Security中默认对抛出的异常进行了捕获并对其进行了处理,因此就会返回上图中的数据。但是没明显我们并不是很想让他返回这种数据,而是在抛异常时也能返回我们的统一返回格式,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道Spring Security的异常处理机制。

    ​ 在Spring Security中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

    ​ 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

    ​ 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

    ​ 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPointAccessDeniedHandler然后配置给Spring Security即可。

    3.2、自定义实现类


    • 认证:AuthenticationEntryPoint的实现类
    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), authException.getMessage());
            String json = JSON.toJSONString(result);
            WebUtils.renderString(response,json);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 授权:AccessDeniedHandler的实现类
    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "用户权限不足");
            String json = JSON.toJSONString(result);
            WebUtils.renderString(response,json);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3、配置处理器


    在编写完成对应的处理器之后,我们还得配置给Spring Security,让框架使用我们自定义的异常处理器,这同样是在SecurityConfig配置类中的configure方法进行配置(已省略第一篇或以及前面的相关配置)

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private AuthenticationEntryPoint authenticationEntryPoint;
    
        @Autowired
        private AccessDeniedHandler accessDeniedHandler;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 配置自定义异常处理器
            http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).
                    accessDeniedHandler(accessDeniedHandler);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.4、检验成果


    • 登录认证时账号密码错误

    image-20220721002443685

    • 权限不足错误

    image-20220721003533736

    4、总结

    在学习了Spring Security之后其实没有多大的感觉,最终还是得使用到项目中才算是真正的掌握,与此同时还有一个问题还未解决,那就是在自定义的过滤器中抛出异常时会被框架捕获去然后抛出Full authentication is required to access this resource的异常信息,带着疑问下班!
    在这里插入图片描述

  • 相关阅读:
    混合背包问题
    Java并发(三)----创建线程的三种方式及查看进程线程
    西邮计科嵌入式复习
    Prim算法
    35 个 Spark 常用算子总结
    Linux用户分类和权限详解
    【数据结构初阶】一. 复杂度讲解
    Java编程注意事项若干(长期更新)
    浅析Hive窗口分析函数
    读书笔记-《ON JAVA 中文版》-摘要26[第二十三章 注解]
  • 原文地址:https://blog.csdn.net/Aqting/article/details/125903697