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); ListgetUserResourceByUserId(Integer userId); }
创建用户和资源的实体类:
@Data public class AnotherUser { private Integer userId; private String userName; private String password; private ListresourceList; } @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("根据用户名找不到该用户的信息!"); } ListresourceList = 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 ListresourceList; /** * 获取用户的所有角色信息 * @return */ @Override public Collection extends GrantedAuthority> 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("根据用户名找不到该用户的信息!"); } ListresourceList = userMapper.getUserResourceByUserId(user.getUserId()); user.setResourceList(resourceList); return user; } }
即将用户账密和权限的填充从service挪到Bean中进行,如此service会显得更加简洁。