简体   繁体   中英

Spring boot security configuration with both dynamic matchers and roles

I am looking for a solution where I could use spring boot security to manage user access with specifics from below:

  1. Every user is tied to a list of groups. Groups are dynamic , meaning that a new group can be assigned/removed to/from the user at any point.

  2. There are dynamically created entities. For example let's think of them as "articles" on some blog website:) Similarly like users, those "articles" are tired to a list of groups that can access it.

In summary : a system where groups and access list are known at runtime.

I do not know much about spring security, so I was looking for some answers on the web, but couldn't find anything. All snippets are based on static antMatchers and roles, like this:

@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");
    }

    (...)

}

Is there any way of creating your own custom Authenticator or something like that, which based on (1) URL would find the "article", therefore the groups allowed to access, and (2) the user logged, therefore the assigned groups => would determine if I can proceed with processing the request or return 401 immediately;)

Thanks in advance for your help!

I don't think you can do this solely in the WebSecurityConfigurerAdapter but here is a similar set-up that takes advantage of Spring Security and demonstrates how to add the access checks to controller methods.

Additional pom.xml dependencies:

    ...
    <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>
    ...

(The latter only if you're using Thymeleaf.)

The 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();
    }
}

In the web MVC @Controller , anyone can read an article (whether logged-in or not):

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

But only AUTHORs may use the preview feature:

    @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) {
        ...
    }

This is also supported in the Thymeleaf template where the menu is conditionally displayed if the user is an 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>

And handling Login/Logout menus to demonstrate other available security predicates:

              <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>

(The rest are implementation details that may be helpful for illustration but not necessarily specific to your question. Specifically, I suggest this is where your logic would be for "dynamic' groups.)

The UserDetailsService implementation relies on JpaRepository implementations and sets the user's grants:

@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;
    }
}

And an example of one of JpaRepository :

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM