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

作者: adm 分类: java 发布时间: 2023-05-30

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

《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层相关的内容:




@Mapper
public interface UserMapper {

    AnotherUser getAnotherUserByUserName(String userName);

    List getUserResourceByUserId(Integer userId);

}

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

@Data
public class AnotherUser {

    private Integer userId;

    private String userName;

    private String password;

    private List 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 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 resourceList;

    /**
     * 获取用户的所有角色信息
     * @return
     */
    @Override
    public Collection getAuthorities() {
        List 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 resourceList = userMapper.getUserResourceByUserId(user.getUserId());
        user.setResourceList(resourceList);
        return user;
    }
}

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

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!