美文网首页架构编程技巧
Spring Security基于资源的认证和授权

Spring Security基于资源的认证和授权

作者: 文景大大 | 来源:发表于2021-08-01 22:32 被阅读0次

    在前面的文章中,已经介绍了:

    《Spring Security入门案例》

    《Spring Security使用数据库进行认证和授权》

    但都是基于角色(Role Based Access Control)的案例,本文主要演示下基于资源(Resoure Based Access Control)的认证与授权案例。(本文的内容是基于以上两篇文章进行的延续,建议提前阅读前面两篇文章的内容)

    一、基于内存的案例

    首先新建一个Controller,里面只有新增和删除用户两个接口,其中root用户可以操作新增和删除,zhang用户只能删除。

    @RestController
    public class UserController {
    
        @GetMapping("/addUser")
        public String addUser(){
            return "add user success!";
        }
    
        @GetMapping("/deleteUser")
        public String deleteUser(){
            return "delete user success!";
        }
    }
    

    然后是Security的配置类:

    @EnableWebSecurity
    public class AnotherSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("root").password(passwordEncoder().encode("root999")).authorities("user:add", "user:delete")
                    .and()
                    .withUser("zhang").password(passwordEncoder().encode("mm111")).authorities("user:delete");
        }
    
        /**
         * 对请求进行鉴权的配置
         *
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 需要user:add权限才可以访问
                    .antMatchers("/addUser").hasAuthority("user:add")
                    // 需要user:delete权限才可以访问
                    .antMatchers("/deleteUser").hasAuthority("user:delete")
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable();
        }
    
        /**
         * 默认开启密码加密,前端传入的密码Security会在加密后和数据库中的密文进行比对,一致的话就登录成功
         * 所以必须提供一个加密对象,供security加密前端明文密码使用
         * @return
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }
    

    注意到,接口的资源名称是在配置类里面配置的,与基于角色的访问控制相比,基于资源的控制粒度更细,能够更加灵活地控制。

    二、基于数据库的案例

    我们需要基于前面的案例,再增加如下的资源表、用户资源对应关系表:

    CREATE TABLE `auth_resource` (
      `resource_id` int DEFAULT NULL,
      `resource_name` varchar(100) DEFAULT NULL,
      `resource_code` varchar(100) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    
    INSERT INTO auth.auth_resource (resource_id, resource_name, resource_code) VALUES(1, '添加用户', 'user:add');
    INSERT INTO auth.auth_resource (resource_id, resource_name, resource_code) VALUES(2, '删除用户', 'user:delete');
    
    CREATE TABLE `auth_user_resource` (
      `id` int DEFAULT NULL,
      `user_id` int DEFAULT NULL,
      `resource_code` varchar(100) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    
    INSERT INTO auth.auth_user_resource (id, user_id, resource_code) VALUES(1, 1, 'user:add');
    INSERT INTO auth.auth_user_resource (id, user_id, resource_code) VALUES(2, 1, 'user:delete');
    INSERT INTO auth.auth_user_resource (id, user_id, resource_code) VALUES(3, 2, 'user:delete');
    

    然后创建Dao层相关的内容:

    <select id="getAnotherUserByUserName" parameterType="string" resultType="com.example.securitydemo.po.AnotherUser">
        select
        au.user_id userId,
        au.user_name userName,
        au.password,
        au.expired,
        au.locked
        from
        auth_user au
        where
        au.user_name = #{userName}
    </select>
    
    <select id="getUserResourceByUserId" parameterType="integer" resultType="com.example.securitydemo.po.Resource">
        select
        ar.resource_id resourceId,
        ar.resource_code resourceCode,
        ar.resource_name resourceName
        from
        auth_user_resource aur
        left join auth_resource ar on
        aur.resource_code = ar.resource_code
        where
        aur.user_id = #{userId}
    </select>
    
    @Mapper
    public interface UserMapper {
    
        AnotherUser getAnotherUserByUserName(String userName);
    
        List<Resource> getUserResourceByUserId(Integer userId);
    
    }
    

    创建用户和资源的实体类:

    @Data
    public class AnotherUser {
    
        private Integer userId;
    
        private String userName;
    
        private String password;
    
        private List<Resource> resourceList;
    
    }
    
    @Data
    public class Resource {
    
        private Integer resourceId;
        private String resourceName;
        private String resourceCode;
    
    }
    

    然后,我们就可以开始编写Service层的代码了,实现将数据库中用户的账密和所属资源加载进来的操作:

    @Slf4j
    @Service
    public class AnotherUserService implements UserDetailsService {
    
        @Resource
        private UserMapper userMapper;
    
        /**
         * 根据用户名去数据库获取用户信息,SpringSecutity会自动进行密码的比对
         *
         * @param username
         * @return
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 用户名必须是唯一的,不允许重复
            AnotherUser user = userMapper.getAnotherUserByUserName(username);
            if (ObjectUtils.isEmpty(user)) {
                throw new UsernameNotFoundException("根据用户名找不到该用户的信息!");
            }
            List<com.example.securitydemo.po.Resource> resourceList = userMapper.getUserResourceByUserId(user.getUserId());
            if (ObjectUtils.isEmpty(resourceList)) {
                log.warn("该用户没有任何权限!");
                return null;
            }
            int num = resourceList.size();
            // 定义一个数组用来存放当前用户的所有资源权限
            String[] resourceCodeArray = new String[num];
            for (int i = 0; i < num; i++) {
                resourceCodeArray[i] = resourceList.get(i).getResourceCode();
            }
            return User.withUsername(user.getUserName())
                    .password(user.getPassword())
                    .authorities(resourceCodeArray).build();
        }
    }
    

    最后,还有我们的配置类(其它配置类需要先注释掉):

    @EnableWebSecurity
    public class AnotherDBSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private AnotherUserService userService;
    
        /**
         * 对请求进行鉴权的配置
         *
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                // 需要user:add权限才可以访问
                .antMatchers("/addUser").hasAuthority("user:add")
                // 需要user:delete权限才可以访问
                .antMatchers("/deleteUser").hasAuthority("user:delete")
                .and()
                .formLogin()
                .and()
                .csrf().disable();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService);
        }
    
        /**
         * 默认开启密码加密,前端传入的密码Security会在加密后和数据库中的密文进行比对,一致的话就登录成功
         * 所以必须提供一个加密对象
         * @return
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }
    

    如此,本案例的所有代码就都写好了,重新启动项目后,可以实现基于资源的认证和授权功能。

    补充:应该有注意到,本案例中使用数据库的实体类和UserService跟上一篇中的用法不太一样,其实这是两种写法,本案例也可以改造为和上一篇中一样的写法,而且较为推荐这种写法。

    需要改造的代码如下:

    @Data
    public class AnotherUser2 implements UserDetails{
    
        private Integer userId;
    
        private String userName;
    
        private String password;
    
        private Integer expired;
    
        private Integer locked;
    
        private List<Resource> resourceList;
    
        /**
         * 获取用户的所有角色信息
         * @return
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for(Resource resource : resourceList){
                authorities.add(new SimpleGrantedAuthority(resource.getResourceCode()));
            }
            return authorities;
        }
    
        /**
         * 指定哪一个是用户的密码字段
         * @return
         */
        @Override
        public String getPassword() {
            return password;
        }
    
        /**
         * 指定哪一个是用户的账户字段
         * @return
         */
        @Override
        public String getUsername() {
            return userName;
        }
    
    
        /**
         * 判断账户是否过期
         * @return
         */
        @Override
        public boolean isAccountNonExpired() {
            return (expired == 0);
        }
    
        /**
         * 判断账户是否锁定
         * @return
         */
        @Override
        public boolean isAccountNonLocked() {
            return (locked == 0);
        }
    
        /**
         * 判断密码是否过期
         * 可以根据业务逻辑或者数据库字段来决定
         * @return
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /**
         * 判断账户是否可用
         * 可以根据业务逻辑或者数据库字段来决定
         * @return
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    }
    
    @Slf4j
    @Service
    public class AnotherUserService2 implements UserDetailsService {
    
        @Resource
        private UserMapper userMapper;
    
        /**
         * 根据用户名去数据库获取用户信息,SpringSecutity会自动进行密码的比对
         *
         * @param username
         * @return
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 用户名必须是唯一的,不允许重复
            AnotherUser2 user = userMapper.getAnotherUser2ByUserName(username);
            if (ObjectUtils.isEmpty(user)) {
                throw new UsernameNotFoundException("根据用户名找不到该用户的信息!");
            }
            List<com.example.securitydemo.po.Resource> resourceList = userMapper.getUserResourceByUserId(user.getUserId());
            user.setResourceList(resourceList);
            return user;
        }
    }
    

    即将用户账密和权限的填充从service挪到Bean中进行,如此service会显得更加简洁。

    相关文章

      网友评论

        本文标题:Spring Security基于资源的认证和授权

        本文链接:https://www.haomeiwen.com/subject/zqorvltx.html