Spring Security基于资源的认证和授权
2021-08-01 本文已影响0人
文景大大
在前面的文章中,已经介绍了:
但都是基于角色(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会显得更加简洁。