Spring Security dynamic url permission control

I. Preface

This article will talk about the dynamic allocation of url permission by Spring Security without login permission control. After login, access url permission will be granted according to the login user role.

Basic environment
  1. spring-boot 2.1.8
  2. mybatis-plus 2.2.0
  3. mysql database
  4. maven project
For the introduction of Spring Security, please refer to the previous articles:
  1. SpringBoot integrated Spring Security entry experience (1)
    https://blog.csdn.net/qq_38225558/article/details/101754743
  2. Spring Security custom login authentication (2)
    https://blog.csdn.net/qq_38225558/article/details/102542072

II. Database table building

Table relationship introduction:
  1. User table t sys user association role table t sys role establish intermediate relationship table t sys user role
  2. Role table t sys role associated permission table t sys permission establish an intermediate relationship between them t sys role permission
  3. The final effect is that all URLs that can be accessed by the role Association of the current login user, as long as the corresponding url permission is assigned to the role.

Warm tip: the logic here is defined according to personal business. The case explained here only assigns access rights to the corresponding roles of users, such as directly assigning permissions to users.

Table simulation data is as follows:

3. Spring Security dynamic permission control

1. Access control without login

The custom AdminAuthenticationEntryPoint class implements the AuthenticationEntryPoint class

Here is the authentication permission entry - > that is to say, all interfaces accessed without login will be blocked here (except for the interface ignored by release)

Warm tip: ResponseUtils and ApiResult are the tool classes used to return json format data under the condition of front-end and back-end separation. For specific implementation, please refer to the demo source code given at the end of the article.

@Component
public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
        ResponseUtils.out(response, ApiResult.fail("Not logged in!!!"));
    }
}

2. User defined filter MyAuthenticationFilter inherits OncePerRequestFilter to realize access authentication

Every time the provider goes through this, we can record the request parameters and response contents here, or change the user permission information with token when the front and back ends are separated, whether the token expires, whether the request header type is correct, and prevent illegal requests.

  1. logRequestBody() method: record the body of the request message
  2. logResponseBody() method: record the response message body

[Note: the requested HttpServletRequest stream can only be read once, and it can't be read the next time. Therefore, the user-defined MultiReadHttpServletRequest tool should be used here to solve the problem that the stream can only be read once. The response is the same. For details, please refer to the demo source code at the end of the article]

@Slf4j
@Component
public class MyAuthenticationFilter extends OncePerRequestFilter {

    private final UserDetailsServiceImpl userDetailsService;

    protected MyAuthenticationFilter(UserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("Request header type: " + request.getContentType());
        if ((request.getContentType() == null && request.getContentLength() > 0) || (request.getContentType() != null && !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE))) {
            filterChain.doFilter(request, response);
            return;
        }

        MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
        MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response);
        StopWatch stopWatch = new StopWatch();
        try {
            stopWatch.start();
            // Record the message body of the request
            logRequestBody(wrappedRequest);

//            String token = "123";
            // In the case of front-end and back-end separation, after the front-end login, the token is stored in the cookie, and each time the interface is accessed, the user authority is retrieved through the token.
            String token = wrappedRequest.getHeader(Constants.REQUEST_HEADER);
            log.debug("Background check token:{}", token);
            if (StringUtils.isNotBlank(token)) {
                // Check token
                SecurityUser securityUser = userDetailsService.getUserByToken(token);
                if (securityUser == null || securityUser.getCurrentUserInfo() == null) {
                    throw new AccessDeniedException("TOKEN Expired, please login again!");
                }
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
                // Global injection role permission information and login user basic information
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            stopWatch.stop();
            long usedTimes = stopWatch.getTotalTimeMillis();
            // Message body for recording response
            logResponseBody(wrappedRequest, wrappedResponse, usedTimes);
        }

    }

    private String logRequestBody(MultiReadHttpServletRequest request) {
        MultiReadHttpServletRequest wrapper = request;
        if (wrapper != null) {
            try {
                String bodyJson = wrapper.getBodyJsonStrByJson(request);
                String url = wrapper.getRequestURI().replace("//", "/");
                System.out.println("-------------------------------- request url: " + url + " --------------------------------");
                Constants.URL_MAPPING_MAP.put(url, url);
                log.info("`{}` Parameters received: {}",url , bodyJson);
                return bodyJson;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    private void logResponseBody(MultiReadHttpServletRequest request, MultiReadHttpServletResponse response, long useTime) {
        MultiReadHttpServletResponse wrapper = response;
        if (wrapper != null) {
            byte[] buf = wrapper.getBody();
            if (buf.length > 0) {
                String payload;
                try {
                    payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    payload = "[unknown]";
                }
                log.info("`{}`  time consuming:{}ms  Parameters returned: {}", Constants.URL_MAPPING_MAP.get(request.getRequestURI()), useTime, payload);
            }
        }
    }

}

3. The user detailsserviceimpl implements the UserDetailsService and the user SecurityUser implements the UserDetails authentication user details.

This was mentioned in the last article, but we didn't do role permission processing last time. Let's add it together this time.

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;

    /***
     * Get user information according to account
     * @param username:
     * @return: org.springframework.security.core.userdetails.UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Retrieve user information from database
        List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username));
        User user;
        // Judge whether the user exists
        if (!CollectionUtils.isEmpty(userList)) {
            user = userList.get(0);
        } else {
            throw new UsernameNotFoundException("User name does not exist!");
        }
        // Return UserDetails implementation class
        return new SecurityUser(user, getUserRoles(user.getId()));
    }

    /***
     * Get user rights and basic information according to token
     *
     * @param token:
     * @return: com.zhengqing.config.security.dto.SecurityUser
     */
    public SecurityUser getUserByToken(String token) {
        User user = null;
        List<User> loginList = userMapper.selectList(new EntityWrapper<User>().eq("token", token));
        if (!CollectionUtils.isEmpty(loginList)) {
            user = loginList.get(0);
        }
        return user != null ? new SecurityUser(user, getUserRoles(user.getId())) : null;
    }

    /**
     * Get role permission information according to user id
     *
     * @param userId
     * @return
     */
    private List<Role> getUserRoles(Integer userId) {
        List<UserRole> userRoles = userRoleMapper.selectList(new EntityWrapper<UserRole>().eq("user_id", userId));
        List<Role> roleList = new LinkedList<>();
        for (UserRole userRole : userRoles) {
            Role role = roleMapper.selectById(userRole.getRoleId());
            roleList.add(role);
        }
        return roleList;
    }

}

Here, we can customize the SecurityUser because the user details (store the basic information of the current user) provided by Spring Security may not meet our needs sometimes, so we can define one ourselves to expand our needs.

getAuthorities() method: that is, grant the current user role permission information

@Data
@Slf4j
public class SecurityUser implements UserDetails {
    /**
     * Current login user
     */
    private transient User currentUserInfo;
    /**
     * role
     */
    private transient List<Role> roleList;

    public SecurityUser() { }

    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    public SecurityUser(User user, List<Role> roleList) {
        if (user != null) {
            this.currentUserInfo = user;
            this.roleList = roleList;
        }
    }

    /**
     * Get the role of the current user
     *
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        if (!CollectionUtils.isEmpty(this.roleList)) {
            for (Role role : this.roleList) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getCode());
                authorities.add(authority);
            }
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

4. Customize UrlFilterInvocationSecurityMetadataSource to implement FilterInvocationSecurityMetadataSource and override getAttributes() method to obtain the role permission information needed to access the url.

After execution, go to the next step, UrlAccessDecisionManager, to authenticate the permissions.

@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    PermissionMapper permissionMapper;
    @Autowired
    RolePermissionMapper rolePermissionMapper;
    @Autowired
    RoleMapper roleMapper;

    /***
     * Return the required user permission information of the url
     *
     * @param object: Save request url information
     * @return: null: Identity does not need any permission to access
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // Get current request url
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        // TODO ignore url, please put it here for filtering and release
        if ("/login".equals(requestUrl) || requestUrl.contains("logout")) {
            return null;
        }

        // All URLs in the database
        List<Permission> permissionList = permissionMapper.selectList(null);
        for (Permission permission : permissionList) {
            // Get the permission corresponding to the url
            if (requestUrl.equals(permission.getUrl())) {
                List<RoleMenu> permissions = rolePermissionMapper.selectList(new EntityWrapper<RoleMenu>().eq("permission_id", permission.getId()));
                List<String> roles = new LinkedList<>();
                if (!CollectionUtils.isEmpty(permissions)){
                    Integer roleId = permissions.get(0).getRoleId();
                    Role role = roleMapper.selectById(roleId);
                    roles.add(role.getCode());
                }
                // Save the role permission information corresponding to the url
                return SecurityConfig.createList(roles.toArray(new String[roles.size()]));
            }
        }
        // If the corresponding url resource is not found in the data, it is illegal to access, and the user is required to log in and operate again.
        return SecurityConfig.createList(Constants.ROLE_LOGIN);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

5. User defined UrlAccessDecisionManager implements AccessDecisionManager rewrites the decision() method to authenticate the access url.

The processing logic of the editor is that only one role can be accessed.

@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {

    /**
     * @param authentication: Role information of current login user
     * @param object: Request url information
     * @param collection: `UrlFilterInvocationSecurityMetadataSource`From the getAttributes method in, indicating the roles required by the current request (there may be more than one)
     * @return: void
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, AuthenticationException {
        // Traversal role
        for (ConfigAttribute ca : collection) {
            // ① permissions required for the current url request
            String needRole = ca.getAttribute();
            if (Constants.ROLE_LOGIN.equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("Not logged in!");
                } else {
                    throw new AccessDeniedException("Unauthorized url!");
                }
            }

            // ② the role of the current user
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                // Just include one of the roles to access
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("Please contact the administrator to assign permissions!");
    }

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

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

6. User defined no permission processor UrlAccessDeniedHandler implements AccessDeniedHandler override handle() method

Here you can customize 403 no permission response content and handle the permission after login.
[Note: it should be distinguished from the permission processing without login ~]

@Component
public class UrlAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        ResponseUtils.out(response, ApiResult.fail(403, e.getMessage()));
    }
}

7. Finally, configure the above processing in the Security core configuration class

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * Access authentication - authentication token, signature...
     */
    private final MyAuthenticationFilter myAuthenticationFilter;
    /**
     * Exception handling of access authority authentication
     */
    private final AdminAuthenticationEntryPoint adminAuthenticationEntryPoint;
    /**
     * User password verification filter
     */
    private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;

    // The above is about login authentication and the following is about url permission -========================================================================================

    /**
     * Get the role information needed to access the url
     */
    private final UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
    /**
     * Authentication permission processing - compare the role permission obtained above with the role of the current login user. If one of the roles is included, it can be accessed normally.
     */
    private final UrlAccessDecisionManager urlAccessDecisionManager;
    /**
     * 403 response content when the user accesses the unauthorized interface
     */
    private final UrlAccessDeniedHandler urlAccessDeniedHandler;

    public SecurityConfig(MyAuthenticationFilter myAuthenticationFilter, AdminAuthenticationEntryPoint adminAuthenticationEntryPoint, AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter, UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource, UrlAccessDeniedHandler urlAccessDeniedHandler, UrlAccessDecisionManager urlAccessDecisionManager) {
        this.myAuthenticationFilter = myAuthenticationFilter;
        this.adminAuthenticationEntryPoint = adminAuthenticationEntryPoint;
        this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;
        this.urlFilterInvocationSecurityMetadataSource = urlFilterInvocationSecurityMetadataSource;
        this.urlAccessDeniedHandler = urlAccessDeniedHandler;
        this.urlAccessDecisionManager = urlAccessDecisionManager;
    }


    /**
     * Permission configuration
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();

        // Disable CSRF to enable cross domain
        http.csrf().disable().cors();

        // Authentication not logged in exception
        http.exceptionHandling().authenticationEntryPoint(adminAuthenticationEntryPoint);
        // Customize 403 response content when accessing unauthorized interface after login
        http.exceptionHandling().accessDeniedHandler(urlAccessDeniedHandler);

        // url Authority authentication processing
        registry.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                o.setAccessDecisionManager(urlAccessDecisionManager);
                return o;
            }
        });

        // Do not create session - that is, pass token from the front end to the background filter to verify whether there is access right
//        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // The identity access' / home 'interface needs to have the' ADMIN 'role
//        registry.antMatchers("/home").hasRole("ADMIN");
        // The identity can only be accessed on the server's local ip[127.0.0.1 or localhost], and other ip addresses cannot access it.
        registry.antMatchers("/home").hasIpAddress("127.0.0.1");
        // Allow anonymous URLs - understood as release interfaces - multiple interfaces to use, split
        registry.antMatchers("/login", "/index").permitAll();
//        registry.antMatchers("/**").access("hasAuthority('admin')");
        // OPTIONS(Options): find the communication options that apply to a specific URL resource. Allows clients to determine resource related options and / or requirements, or the performance of a server, without performing specific actions involving data transfer
        registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
        // Automatic login - cookie storage
        registry.and().rememberMe();
        // All other requests require authentication
        registry.anyRequest().authenticated();
        // Prevent iframe from cross domain
        registry.and().headers().frameOptions().disable();

        // User defined filter authenticates user name and password when logging in
        http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(myAuthenticationFilter, BasicAuthenticationFilter.class);
    }

    /**
     * Ignore blocking url or static resource folder - web.ignoring(): will filter the url directly - will not pass through the Spring Security filter chain
     *                             http.permitAll(): Does not bypass spring security validation, rather allowing the path to pass
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.GET,
                "/favicon.ico",
                "/*.html",
                "/**/*.css",
                "/**/*.js");
    }

}

4. Write test code

Control layer:

@Slf4j
@RestController
public class IndexController {

    @GetMapping("/")
    public ModelAndView showHome() {
        return new ModelAndView("home.html");
    }

    @GetMapping("/index")
    public String index() {
        return "Hello World ~";
    }

    @GetMapping("/login")
    public ModelAndView login() {
        return new ModelAndView("login.html");
    }

    @GetMapping("/home")
    public String home() {
        String name = SecurityContextHolder.getContext().getAuthentication().getName();
        log.info("Landers:" + name);
        return "Hello~ " + name;
    }

    @GetMapping(value ="/admin")
    // The access path '/ ADMIN' has the 'ADMIN' role permission [this is a write dead method]
//    @PreAuthorize("hasPermission('/admin','ADMIN')")
    public String admin() {
        return "Hello~ Administrators";
    }

    @GetMapping("/test")
    public String test() {
        return "Hello~ Test authority provider";
    }
    
}

The page and other relevant codes will not be posted here. For details, please refer to the demo source code at the end of the article.

V. effect of running access test

1. When not logged in

2. Access normally if you have permission after login

3. No permission after login

Here we can modify the database role permission association table t sys role permission to test.~

Security dynamic url permission depends on this table to judge. As long as you modify this table to assign the url permission resource corresponding to the role, users will judge dynamically when accessing the url, without any other processing. If you put the permission information in the cache, you can update the cache when modifying the table data!

4. After login, when accessing the url not configured in the database and not ignoring the blocked url in Security

Six, summary

  1. Custom not logged in permission processor AdminAuthenticationEntryPoint - custom not logged in access to the unauthorized url response content
  2. Customize the access authentication filter MyAuthenticationFilter - record the request response log, check whether the access is legal, verify that the token is expired, etc.
  3. Custom UrlFilterInvocationSecurityMetadataSource - get the role permissions required to access the url
  4. Custom UrlAccessDecisionManager - authorization processing for access url
  5. Custom UrlAccessDeniedHandler - failed to access the unauthorized url after login processor - Custom 403 no permission response content
  6. Configure the above processors and filters in the Security core configuration class
Security dynamic permission related code:

demo source code of this case

https://gitee.com/zhengqingya/java-workspace

Keywords: Java Spring Database Mybatis MySQL

Added by Swerve1000 on Sat, 19 Oct 2019 12:09:31 +0300