具有动态匹配器和角色的Spring Boot安全配置

问题描述

我正在寻找一种解决方案,可以在其中使用Spring Boot安全性从以下方面管理用户访问权限:

  1. 每个用户都绑定到一个组列表。 组是动态的,这意味着可以随时向用户分配新组或从用户删除新组。

  2. 有动态创建的实体。 例如,让我们将其视为某些博客网站上的“文章” :) 与用户类似,那些文章”会厌倦可以访问它的组列表

总结:在运行时知道组和访问列表的系统。

我对Spring安全性了解不多,因此我一直在网上寻找一些答案,但找不到任何东西。所有代码段均基于静态antMatchers和角色,例如:

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().and().authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/something_else_instead_of_this").hasRole("ROLE_and_instead_of_this_static_role");
    }

    (...)

}

是否可以创建您自己的自定义身份验证器之类的方法,该方法基于(1) URL会找到“文章”,因此允许组来访问,并(2)用户登录,因此分配的组 => 将确定我是否可以继续处理请求或立即返回401;)

在此先感谢您的帮助!

解决方法

我认为您不能仅在WebSecurityConfigurerAdapter中执行此操作,但这是一个类似的设置,它利用了Spring Security的优势,并演示了如何将访问检查添加到控制器方法。

其他pom.xml依赖项:

    ...
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    ...
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
    ...

(仅当您使用Thymeleaf时才使用后者。)

WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@NoArgsConstructor @Log4j2
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
    @Autowired private UserDetailsService userDetailsService;
    @Autowired private PasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
            .antMatchers("/css/**","/js/**","/images/**","/webjars/**","/webjarsjs");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().anyRequest().permitAll()
            .and().formLogin().loginPage("/login").permitAll()
            .and().logout().permitAll();
    }
}

在Web MVC @Controller中,任何人都可以阅读文章(无论是否已登录):

    @RequestMapping(method = { GET },value = { "/article/{slug}/" })
    @PreAuthorize("permitAll()")
    public String article(Model model,@PathVariable String slug) {
        ...
    }

但是只有作者可以使用预览功能:

    @RequestMapping(method = { GET },value = { "/preview/" })
    @PreAuthorize("hasAuthority('AUTHOR')")
    public String preview(Model model) {
        ...
    }

    @RequestMapping(method = { POST },value = { "/preview/" })
    @PreAuthorize("hasAuthority('AUTHOR')")
    public String previewPOST(Model model,Principal principal,HttpSession session,HttpServletRequest request,@Valid PreviewForm form,BindingResult result) {
        ...
    }

Thymeleaf模板也支持此功能,如果用户是AUTHOR,则有条件显示菜单。

              <li th:ref="navbar-item" sec:authorize="hasAuthority('AUTHOR')">
                <button th:text="'Author'"/>
                <ul th:ref="navbar-dropdown">
                  <li><a th:text="'Preview'" th:href="@{/preview/}"/></li>
                </ul>
              </li>

并处理“登录/注销”菜单以演示其他可用的安全谓词:

              <li th:ref="navbar-item" sec:authorize="!isAuthenticated()">      
                <a th:text="'Login'" th:href="@{/login}"/>                      
              </li>                                                             
              <li th:ref="navbar-item" sec:authorize="isAuthenticated()">       
                <button sec:authentication="name"/>                             
                <ul th:ref="navbar-dropdown">                                   
                  <li><a th:text="'Change Password'" th:href="@{/password}"/></\
li>                                                                             
                  <li><a th:text="'Logout'" th:href="@{/logout}"/></li>         
                </ul>                                                           
              </li>

(其余的实现细节可能有助于举例说明,但不一定特定于您的问题。具体来说,我建议您在这里使用“动态”组。)

UserDetailsService实现依赖于JpaRepository实现并设置用户的授权:

@Service
@NoArgsConstructor @ToString @Log4j2
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired private CredentialRepository credentialRepository;
    @Autowired private AuthorRepository authorRepository;
    @Autowired private SubscriberRepository subscriberRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = null;
        Optional<Credential> credential =
            credentialRepository.findById(username);

        if (credential.isPresent()) {
            HashSet<GrantedAuthority> set = new HashSet<>();

            subscriberRepository.findById(username)
                .ifPresent(t -> set.add(new SimpleGrantedAuthority("SUBSCRIBER")));

            authorRepository.findById(username)
                .ifPresent(t -> set.add(new SimpleGrantedAuthority("AUTHOR")));

            user = new User(username,credential.get().getPassword(),set);
        } else {
            throw new UsernameNotFoundException(username);
        }

        return user;
    }
}

还有JpaRepository之一的示例:

@Repository
@Transactional(readOnly = true)
public interface AuthorRepository extends JpaRepository<Author,String> {
    public Optional<Author> findBySlug(String slug);
}