Spring Security动态权限的实现方法详解
什么是动态权限?
在传统的企业应用中,权限被存储在静态的权限表中,着重强调的是用户拥有哪些权限。但是在现实生活中,我们会发现企业的角色是十分复杂的,拥有权限表面看起来是不够的。例如,对于一个CRM系统,管理员可能需要对某些用户进行一些特殊的操作。这种情况下,我们需要实现动态权限,即在运行时动态授权,而不是在静态权限表中授权。
Spring Security如何实现动态权限?
Spring Security 是 Spring 生态中很重要的一部分,它是一个安全框架,提供了认证和授权等功能。在 Spring Security 中,我们可以通过定义一个自定义的 AccessDecisionVoter 实现访问决策的投票。具体来说,我们需要基于自己的业务逻辑和数据访问层,实现 AccessDecisionVoter 中的两个方法:
-
supports(ConfigAttribute attribute),支持哪些 ConfigAttribute
-
vote(Authentication authentication, Object object, Collection
list) 检查当前登录用户是否满足访问保护规则
我们可以将自己的业务逻辑和数据库查询放到 supports(ConfigAttribute attribute) 方法中,检查当前用户是否允许访问某个资源,如果允许返回 ACCESS_GRANTED ,如果不允许返回 ACCESS_DENIED 。具体实现请看以下示例。
在这个例子中,假设我们需要实现一个 CRM 系统,某些操作需要特殊的权限,例如管理员和客户经理可以删除用户,而普通员工不能删除用户。
示例 1:动态权限实现方案一
首先我们需要创建一个接口来列出我们需要动态授权的资源,例如需要删除的用户:
public interface PermissionService {
boolean hasPermission(Long userId, String permission);
}
这个接口有两个参数:用户 ID 和需要授权的操作名称。真正的授权操作会在业务逻辑层实现。下面我们实现这个接口:
@Service
public class PermissionServiceImpl implements PermissionService {
private final UserRepository userRepository;
@Autowired
public PermissionServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean hasPermission(Long userId, String permission) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// 管理员和客户经理可以删除用户
return (user.getRole().equals(Role.ROLE_ADMIN) || user.getRole().equals(Role.ROLE_MANAGER))
&& permission.equals("delete_user");
}
}
在这个实现中,我们首先从数据库中获取用户信息,然后判断用户是否是管理员或客户经理,并且是否有删除用户的权限。如果满足条件,返回 true ,否则返回 false 。
然后,我们需要实现一个 AccessDecisionVoter ,以便能够进行投票。
@Component
public class PermissionBasedVoter implements AccessDecisionVoter<Object> {
private final PermissionService permissionService;
public PermissionBasedVoter(PermissionService permissionService) {
this.permissionService = permissionService;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null && attribute.getAttribute().startsWith("PERMISSION_");
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
String permission = attribute.getAttribute().substring("PERMISSION_".length());
if (authentication.isAuthenticated()
&& authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Long userId = Long.parseLong(userDetails.getUsername());
if (permissionService.hasPermission(userId, permission)) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
}
在这个实现中,我们主要是实现了两个方法 supports 和 vote 。supports 方法用于判断当前的 ConfigAttribute 是否是我们支持的类型,这里我们支持所有 ConfigAttribute 类型,只要它以 "PERMISSION_" 开头即可。
vote 方法用于检查当前登录用户是否授权访问某个资源。注意,这里我们将用户 ID 存放在登录凭据的用户名字段中。具体实现如下:
if (authentication.isAuthenticated()
&& authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Long userId = Long.parseLong(userDetails.getUsername());
if (permissionService.hasPermission(userId, permission)) {
return ACCESS_GRANTED;
}
}
这段代码中,我们从凭据中获取用户 ID ,并且调用 PermissionService 中的 hasPermission 方法检查是否授权。如果授权,返回 ACCESS_GRANTED ;如果不授权,返回 ACCESS_DENIED 。
最后,我们需要在 Spring Security 中注册我们的投票器 PermissionBasedVoter 。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final PermissionBasedVoter permissionBasedVoter;
public SecurityConfig(PermissionBasedVoter permissionBasedVoter) {
this.permissionBasedVoter = permissionBasedVoter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/users/**").hasAuthority("PERMISSION_DELETE_USER")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager())
.and().httpBasic()
.and().csrf().disable();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters
= Arrays.asList(permissionBasedVoter, new RoleVoter(), new AuthenticatedVoter());
return new AffirmativeBased(decisionVoters);
}
}
在上面的代码中,我们将我们创建的投票器 PermissionBasedVoter 添加到投票器列表中。然后我们对 DELETE 访问授予 "PERMISSION_DELETE_USER" 权限,并将访问决策管理器设置为 accessDecisionManager() 。最后开启 HTTP Basic 认证,并禁用 CSRF 保护。
示例 2:动态权限实现方案二
除了上面的方案,我们还可以使用 Spring Expression Language(SpEL)和数据库支持来实现动态权限。这个方法要略微简单一些。
首先我们需要定义一个访问控制表,存储每个资源和需要的权限:
CREATE TABLE `access_control` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`resource` varchar(255) NOT NULL,
`permission` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
然后我们需要在 Spring Security 配置中使用 SpEL 表达式定义访问规则:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
public SecurityConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/users/**").access("hasPermissionToDeleteUser()")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager())
.and().httpBasic()
.and().csrf().disable();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters
= Arrays.asList(new RoleVoter(), new AuthenticatedVoter());
return new AffirmativeBased(decisionVoters);
}
@Bean
public JdbcMutableAclService aclService() {
return new JdbcMutableAclService(dataSource, new LookupStrategyImpl(dataSource, aclCache(), aclAuthorizationStrategy(), consoleAuditLogger()));
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
@Bean
public AuditLogger consoleAuditLogger() {
return new ConsoleAuditLogger();
}
@Bean
public EhCacheBasedAclCache aclCache() {
return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
}
@Bean
public EhCacheFactoryBean aclEhCacheFactoryBean() {
EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
ehCacheFactoryBean.setCacheName("aclCache");
return ehCacheFactoryBean;
}
@Bean
public EhCacheManagerFactoryBean aclCacheManager() {
return new EhCacheManagerFactoryBean();
}
@Bean
public AclPermissionEvaluator aclPermissionEvaluator() {
return new AclPermissionEvaluator(aclService());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}password").roles("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/home")
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/login")
.and()
.authorizeRequests();
}
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) throws Exception {
JdbcMutableAclService aclService = aclService();
ObjectIdentityGenerator<Object> generator = new DefaultObjectIdentityGenerator<>();
List<Acl> acls = new ArrayList<>();
// 添加默认权限
acls.add(aclService.createAcl(new ObjectIdentityImpl(SampleObject.class, generator.next()),
Arrays.asList(new PrincipalSid("user"), new GrantedAuthoritySid("ROLE_USER")),
Arrays.asList(BasePermission.READ, BasePermission.WRITE, BasePermission.DELETE)));
// 添加动态权限
acls.add(aclService.createAcl(new ObjectIdentityImpl(SampleObject.class, generator.next()),
Arrays.asList(new PrincipalSid("admin"), new GrantedAuthoritySid("ROLE_ADMIN")),
Arrays.asList(BasePermission.READ, BasePermission.WRITE)));
AccessControlList acl = new AccessControlList();
acl.setEntries(acls);
acl.setOwner(new PrincipalSid("admin"));
acl.setAuditLogger(consoleAuditLogger());
// 将访问控制列表保存到数据库中
JdbcMutableAclService jdbcMutableAclService = aclService();
jdbcMutableAclService.setClassIdentityQuery("SELECT @@IDENTITY");
jdbcMutableAclService.setSidIdentityQuery("SELECT @@IDENTITY");
jdbcMutableAclService.save(acl);
// 将 SpEL 所需的 bean 注册到 Spring 容器中
registry
.antMatchers(HttpMethod.GET, "/foo").access("hasRole('ROLE_USER')")
.antMatchers(HttpMethod.DELETE, "/foo").access("hasPermissionToDelete()");
}
}
在这个示例中,我们创建了一个访问控制表来存储资源和所需的权限信息。然后,在 configure(HttpSecurity http) 方法中,我们使用 SpEL 表达式来使用动态权限。其中,"hasPermissionToDelete()" 会调用我们先前定义的 aclPermissionEvaluator() 方法。最后,我们使用 JdbcMutableAclService 将访问控制列表保存到数据库中。
总结
通过上述两个示例,我们了解到了 Spring Security 如何实现动态权限。我们可以根据不同的业务需求,使用不同的方法来实现动态权限。我们也可以将访问控制列表保存在数据库中,以便随时更改资源和权限。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Spring Security动态权限的实现方法详解 - Python技术站