Spring Security series # 5: User authorization for backend decoupage projects

chapter

Spring Security is a series of simple introduction and practical

The second part of the Spring Security series analyzes the authentication process

Spring Security series 3: Custom SMS Login Authentication

The fourth in the Spring Security series uses JWT for authentication

Spring Security series # 5: User authorization for backend decoupage projects

Spring Security Series 6 authorization Process Analysis

You’re already familiar with login authentication from the previous articles, so let’s start practicing another core feature of Spring Security: user permission authentication. In a system, different users have different permissions. For example, some users can only read a file, while others can modify it. Generally speaking, the system assigns different roles to different users, and each role has a series of permissions.

For a Web system, it is equivalent to specifying which roles users have to access the interface. For now, let’s assume that our system has two roles, one for the general user and one for the administrator. Ordinary users can do things the administrator can do, the administrator can do things ordinary people may not be able to do. Of course, the real business situation may be several times more complex than this, I just throw out a brick to lead jade here, I hope we don’t be ungrateful, continue to ask.

Create two Controller interfaces, one accessible to common users and administrators:

@RestController
@RequestMapping("normal")
public class NormalResourceController {
    @GetMapping("/resource")
    public ResponseEntity<String> getResource(a){
        return ResponseEntity.ok("Normal resource obtained successfully"); }}Copy the code

Create a Controller interface that only administrators can access:

@RestController
@RequestMapping("/admin")
public class AdminResourceController {
    @GetMapping("/resource")
    public ResponseEntity<String> getResource(a){
        return ResponseEntity.ok("Admin resource obtained successfully"); }}Copy the code

Add two roles to the database role table:

id role_name
1 admin
2 normal_user

Currently we have two users in the database:

id username password
1 test1
2 a 2a
10 $pjHyw9MSGC i6k546Ii / 0 ulfgtk4wyb4. 8 bsrq7yb4dy. ZpBLxOha
2 test2
2 a 2a
10 $pjHyw9MSGC i6k546Ii / 0 ulfgtk4wyb4. 8 bsrq7yb4dy. ZpBLxOha

Add admin and normal_user to user test1, normal_user to user test2, add data to user_role table:

id user_id role_id
1 1 1
2 1 2
3 2 2

Our system’s User object implements the UserDetails interface, and overrides the Getathorities method, which looks up the user’s role information as a GrantedAuthority object:

public class User implements UserDetails {
    private static final long serialVersionUID = -16523804109585173L;
    private Integer id;
    private String username;
    private String password;
    private List<Role> roleList;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roleList.stream().map(role ->
                newSimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList()); }}Copy the code

Simple implementation

With the above preparations in place, let’s add the following configuration to Security:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //super.configure(http);
    http.formLogin()
        .disable()
        // Add header Settings to support cross-domain and Ajax requests for local tests annotated first
        //.cors().and()
        //.addFilterAfter(corsFilter(), CorsFilter.class)
        .apply(smsAuthenticationSecurityConfig).and()
        .apply(jwtAuthenticationSecurityConfig).and()
        .apply(jwtRequestSecurityConfig).and()
        // Set URL authorization
        .authorizeRequests()
        // The login page must be allowed
        .antMatchers("/login"."/verifyCode"."/smsLogin"."/failure"."/jwtLogin").permitAll()
        .antMatchers("/admin/**").hasAuthority("admin")
        .antMatchers("/normal/**").hasAnyAuthority("admin"."normal_user")
        // anyRequest() All requests authenticated() must be authenticated
        .anyRequest()
        .authenticated().and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        / / close CSRF
        .csrf().disable();
}
Copy the code

Calling antMatchers().hasauthority () indicates that the user must have a role to access these paths.

To test this, we have no problem accessing either controller using the token generated by the test1 user login:

Error 403 is displayed when you use the token generated by user test2 to access admin:

Such a simple user authority authentication is realized.

Custom permission authentication

This is obviously not flexible enough. If we add other interfaces, we still need to configure them here. This kind of hard coding is not very good.

Implement permission authentication interface

To implement dynamic permission authentication, of course, resources must be obtained first, and then the relationship between them and which roles can access them must be represented.

Spring Security is the specific permissions required to access resources through the SecurityMetadataSource, so the first step is to implement SecurityMetadataSource. SecurityMetadataSource is an interface:

public interface SecurityMetadataSource extends AopInfrastructureBean {
    Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;

    Collection<ConfigAttribute> getAllConfigAttributes(a);

    boolean supports(Class
        var1);
}
Copy the code

It inherits the AopInfrastructureBean interface, which does not have any methods, but serves as a tag for the base class that implements AOP. If any class implements this interface, then that class will not be proxied by AOP, even if it can be cut in.

Then there are the methods for SecurityMetadataSource:

Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;
Copy the code

Gets the required permission information for a protected security object, returns a set of ConfigAttribute objects, If the security object object is not supported by the current SecurityMetadataSource object, an IllegalArgumentException is thrown.

An object is passed in and the permissions required to access the object are returned. If the current SecurityMetadataSource object does not support the current object, an error is reported, depending on method 3.

Collection<ConfigAttribute> getAllConfigAttributes(a);
Copy the code

Gets the collection of permission information for all security objects held in this SecurityMetadataSource object. The main purpose of this method are AbstractSecurityInterceptor to check each ConfigAttribute object when it is started.

This method does nothing but return a list of all permissions.

Take a look at the relational inheritance diagram for the SecurityMetadataSource interface:

You can see that the SecurityMetadataSource has two subinterfaces,

  • FilterInvocationSecurityMetadataSource is a marker interface, said security object is web request FilterInvocation security metadata sources, in and of itself without any content.

  • MethodSecurityMetadataSource said security object method calls the MethodInvocation metadata sources, the safety of the interface is as follows:

    public interface MethodSecurityMetadataSource extends SecurityMetadataSource {
    
    	Collection<ConfigAttribute> getAttributes(Method method, Class
              targetClass);
    
    }
    Copy the code

    Generally used for permission validation during intermethod calls.

We are here in a web project, implement FilterInvocationSecurityMetadataSource line:

@Component
public class UrlMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private PowerService powerService;
    private final AntPathMatcher matcher = new AntPathMatcher();
    public static final String NEED_LOGIN = "NEED_LOGIN";

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<Power> powers = powerService.queryAll();
        for (Power power : powers) {
            if (matcher.match(power.getUrl(),requestUrl)){
                // There is a route in the database, which requires the role to access it
                List<Role> roleList = power.getRoleList();
                if (CollectionUtils.isEmpty(roleList)){
                    break;
                }
                return roleList.stream().map(item -> newSecurityConfig(item.getRoleName().trim())).collect(Collectors.toList()); }}// The path has no role to access
        return SecurityConfig.createList(NEED_LOGIN);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes(a) {
        return null;
    }
    /** * tells the caller whether SecurityMetadataSource currently supports such security objects, and only if it does, can the getAttributes method */ be called on such security objects
    @Override
    public boolean supports(Class
        clazz) {
        returnclazz.isAssignableFrom(FilterInvocation.class); }}Copy the code

First of all, we obtained the currently accessed resources from request, and then used PowerService to query all resources in the database. The resource class is as follows:

@Data
public class Power implements Serializable {
    private static final long serialVersionUID = -25876673587503659L;
    private Integer id;
    private String title;
    private String url;
    private List<Role> roleList;
}
Copy the code

Then compare the request URL with all url patterns queried in the database one by one to see which URL pattern matches, and then obtain the role corresponding to the URL pattern.

If the getAttributes(Object O) method returns NULL, that means the current request does not require any role access, or even a login. However, in my entire business, there is no such request, and all unmatched paths need to be authenticated before they can be accessed, so I return a NEED_LOGIN role that does not exist in the database, so I will handle this role specifically in the next step of the role comparison process.

The list of roles returned by the getAttributes(Object O) method is ultimately passed to the AccessDecisionManager, so let’s look at the implementation of the AccessDecisionManager.

Implement permission decision maker

Knowing the specific permissions required for the currently accessed URL, it’s now up to you to decide whether the current access can pass permission authentication. The implementation is as follows:

@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
        for (ConfigAttribute attribute : configAttributes) {
            String needRole = attribute.getAttribute();
            if (UrlMetadataSource.NEED_LOGIN.equals(needRole) && authentication instanceof AnonymousAuthenticationToken) {
                throw new InsufficientAuthenticationException("User needs to log in");
            }
            // The role of the current user
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            // Compare the roles required for access, as long as one of them is satisfied
            for (GrantedAuthority userRole : authorities) {
                if (userRole.getAuthority().equals(needRole)){
                    return; }}}throw new AccessDeniedException("Insufficient authority");
    }

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

    @Override
    public boolean supports(Class
        clazz) {
        return true; }}Copy the code

We’ll focus on the Decide method, which takes three parameters

  • Authentication: contains the current user information, including the permissions. The source of permissions here is the user class of our systemgetAuthoritiesReturns a list of role permissions.
  • Object:FilterInvocationObject, you can getrequestSuch as web resources
  • configAttributes: It’s on the topgetAttributesMethod to return a list of roles

Because in our system for all the resources, the need to log in to access, through authentication instanceof AnonymousAuthenticationToken judgment have user login, no log in, throw an exception.

Then there is the judgment of user permissions. The judgment condition here is that as long as the current user has a role, he can access the resource. Brothers can also do it according to their own business.

Configuration implementation class

Now that we’ve implemented the resources and validation for the above permissions, it’s time to specify that Spring Security uses our custom implementation class:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
    @Autowired
    private JwtRequestSecurityConfig jwtRequestSecurityConfig;

    @Autowired
    private UrlMetadataSource urlMetadataSource;
    @Autowired
    private UrlAccessDecisionManager urlAccessDecisionManager;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/login"."/verifyCode"."/smsLogin"."/failure"."/jwtLogin");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        http.formLogin()
                .disable()
                // Add header Settings to support cross-domain and Ajax requests
                //.cors().and()
                //.addFilterAfter(corsFilter(), CorsFilter.class)
                .apply(smsAuthenticationSecurityConfig).and()
                .apply(jwtAuthenticationSecurityConfig).and()
                .apply(jwtRequestSecurityConfig).and()
                // Set URL authorization
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(urlAccessDecisionManager);
                        object.setSecurityMetadataSource(urlMetadataSource);
                        returnobject; }})// The login page must be allowed
                //.antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin").permitAll()
                //.antMatchers("/admin/**").hasAuthority("admin")
                //.antMatchers("/normal/**").hasAnyAuthority("admin","normal_user")
                // anyRequest() All requests authenticated() must be authenticated
                .anyRequest()
                .authenticated().and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                / / close CSRF.csrf().disable(); }}Copy the code

We withObjectPostProcessor method is used here, when creating the default FilterSecurityInterceptor our the accessDecisionManager and securityMetadataSource set in.

It is important to note that we use the original.antmatchers ().permitall () whitelist to block resources. Security actually blocks resources by setting up an anonymous user, which is blocked by our custom UrlMetadataSource. So the whitelist of this place needs to mention the outermost configuration.