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
- spring-boot 2.1.8
- mybatis-plus 2.2.0
- mysql database
- maven project
For the introduction of Spring Security, please refer to the previous articles:
- SpringBoot integrated Spring Security entry experience (1)
https://blog.csdn.net/qq_38225558/article/details/101754743 - Spring Security custom login authentication (2)
https://blog.csdn.net/qq_38225558/article/details/102542072
II. Database table building
Table relationship introduction:
- User table t sys user association role table t sys role establish intermediate relationship table t sys user role
- Role table t sys role associated permission table t sys permission establish an intermediate relationship between them t sys role permission
- 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.
- logRequestBody() method: record the body of the request message
- 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
- Custom not logged in permission processor AdminAuthenticationEntryPoint - custom not logged in access to the unauthorized url response content
- Customize the access authentication filter MyAuthenticationFilter - record the request response log, check whether the access is legal, verify that the token is expired, etc.
- Custom UrlFilterInvocationSecurityMetadataSource - get the role permissions required to access the url
- Custom UrlAccessDecisionManager - authorization processing for access url
- Custom UrlAccessDeniedHandler - failed to access the unauthorized url after login processor - Custom 403 no permission response content
- Configure the above processors and filters in the Security core configuration class