1. 简介

在Spring Security中,我们可以使用基于注解的安全性,以控制方法响应、请求类型等。但是,如果我们需要跟具体的业务数据绑定的话,我们就需要根据规则来控制具体的访问权限。

在这种情况下,就需要使用Spring Security提供的“动态授权”功能了。本文将介绍如何使用动态授权功能来精细管理用户权限。

2. 动态授权原理

Spring Security中的动态授权,是通过实现权限控制的接口来达到的。主要通过实现AccessDecisionManager接口和AccessDecisionVoter接口来实现。

AccessDecisionManager接口提供了访问决策,它用于限定某个Subject允许访问哪种资源。

AccessDecisionVoter接口是AccessDecisionManager的一个具体实现。它可以进行“投票”,来判断是否允许访问某个资源。

3. 实现动态授权

下面是具体的实现步骤:

3.1. 定义自定义权限管理器

首先,在项目中定义一个自定义的权限管理器,在这里我们继承了AccessDecisionManager接口。

定义完成后,重写父接口中的方法decide()decide()方法会对用户的拥有角色和尝试访问资源进行比较,如果判断用户拥有该资源访问权限则返回,否则就抛出InvalidAuthorityException异常。

@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        if(configAttributes == null) {
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while(iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            String needRole = configAttribute.getAttribute();
            for(GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if(grantedAuthority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("您没有访问该资源的权限");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

3.2. 定义自定义投票器

定义完权限管理器后,我们就要定义自定义的投票器了,继承AccessDecisionVoter接口,这个类主要负责解析用户请求中的资源路径、资源类型和权限集合,进行投票决策。

和之前一样,我们要实现该接口中重写的方法vote()。根据用户的访问路径和权限,它会“投票”给权限管理器,最终决定用户是否有权访问。

在这里,我们实现了两个投票器来进行权限控制。其中,一些接口需要admin角色才能访问,而其他接口不需要。

示例代码:

@Component
public class UrlAccessDecisionVoter implements AccessDecisionVoter<Object>{
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        int result = AccessDecisionVoter.ACCESS_ABSTAIN;
        Object principal = authentication.getPrincipal();
        if (!(principal instanceof UserDetails)) {
            return result;
        }
        UserDetails user = (UserDetails) principal;

        for (ConfigAttribute attribute : attributes) {
            if ("ROLE_ADMIN".equals(attribute.getAttribute())) {
                result = AccessDecisionVoter.ACCESS_DENIED;
                for (GrantedAuthority authority : user.getAuthorities()) {
                    if ("ROLE_ADMIN".equals(authority.getAuthority())) {
                        return AccessDecisionVoter.ACCESS_GRANTED;
                    }
                }
            } else {
                result = AccessDecisionVoter.ACCESS_GRANTED;
            }
        }
        return result;
    }
}

3.3. 配置remember-me、session

在web.xml中配置remember-me和session的过期时间。

<session-config>
        <session-timeout>30</session-timeout>
    </session-config>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>RememberMeFilter</filter-name>
        <filter-class>org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>RememberMeFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

3.4. 配置SecurityConfig

最后,在Spring Security的配置文件SecurityConfig.java中,我们要做两件事情:

  1. 配置自定义权限管理器。
  2. 指定需要进行方法安全拦截的package,并且指定自定义投票验证器。

示例代码:

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = {"com.example.demo"})
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    private UrlAccessDecisionVoter urlAccessDecisionVoter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
               .logout().logoutUrl("/logout").permitAll()
               .and()
                .exceptionHandling().accessDeniedPage("/noAuth");

    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers("/css/**", "/js/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123456").roles("ADMIN", "USER")
                .and()
                .withUser("user").password("123456").roles("USER");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers("/css/**", "/js/**");
    }

    @Bean
    public FilterRegistrationBean<Filter> registrationBean() {
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new DelegatingFilterProxy());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("springSecurityFilterChain");
        return registrationBean;
    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
        decisionVoters.add(urlAccessDecisionVoter);
        //这里可以按照自己需求添加更多的投票器
        AffirmativeBased accessDecisionManager = new AffirmativeBased(decisionVoters);
        accessDecisionManager.setDecisionVoters(decisionVoters);
        return customAccessDecisionManager;
    }
}

4. 示例

下面是两个基于Spring Security动态授权的示例:

4.1. 部分接口只允许admin角色访问

在上面的代码中,我们在SecurityConfig.java中配置了urlAccessDecisionVoter,用于具体控制某些URL只允许admin角色访问。

我们可以使用这个功能来控制某些URL,比如在员工管理系统中,启用了一个特殊的功能模块,只允许部门经理和人事经理访问员工数据。

代码:

@GetMapping("/employee/list")
    @Log(title = "员工管理", businessType = BusinessType.LIST)
    public AjaxResult<List<Employee>> getEmployeeList() {
        return ajaxResponse(employeeService.getAllEmployees());
    }

    @PostMapping("/employee/add")
    @Log(title = "员工管理", businessType = BusinessType.INSERT)
    public AjaxResult<String> addEmployee(@RequestBody Employee employee) {
        if(employeeService.addEmployee(employee) == 0) {
            return ajaxFailed("添加员工失败,请检查员工信息后重试");
        }
        return ajaxResponse("添加员工成功!");
    }

4.2. 个别接口不需要权限控制,直接访问

如果一个URL的访问权限比较低,比如说是一个报道页面,或者是一个新闻页面,我们可以让它不受权限管理器的控制,直接访问。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注