One, foreword

This article will describe Spring Security’s dynamic allocation of URL permissions, non-login permission control, and granting access to URL permissions based on the role of the login user

Basic environment

  1. Spring – the boot 2.1.8
  2. Mybatis – plus 2.2.0
  3. The mysql database
  4. Maven project
To get started with Spring Security, refer to previous articles:

  1. SpringBoot integration Spring Security Introduction experience (1) blog.csdn.net/qq_38225558…
  2. Spring Security Custom login authentication (2) blog.csdn.net/qq_38225558…

Second, database building table

Table relationships:

  1. The users tablet_sys_userAssociated role tablet_sys_roleThe two establish the intermediate relation tablet_sys_user_role
  2. Character sheett_sys_roleAssociated permission tablet_sys_permissionThe two establish the intermediate relation tablet_sys_role_permission
  3. The final effect is that the role of the current login user is associated with all urls that can be accessed by the role. You only need to assign url permissions to the role

Warm tips: the logic here is defined according to personal business, xiaobian here to explain the case only to the corresponding role of the user to allocate access rights, like other directly to the user to allocate permissions and so on can be realized by themselves

Table simulation data is as follows:

Spring Security dynamic permission control

1. Control of access permission without login

Custom AdminAuthenticationEntryPoint class implements AuthenticationEntryPoint class

Here is the authentication permission entry -> that is, access to all interfaces without logging in will be blocked to this (except for the pass ignore interface)

ResponseUtils and ApiResult are the ResponseUtils and ApiResult classes used to simulate the return of JSON data in the case of front-end and back-end separation. For specific implementation, please refer to the demo source code given at the end of this 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!!" )); }}Copy the code

2. Customize filtersMyAuthenticationFilterinheritanceOncePerRequestFilterAccess authentication is implemented

Every time we access the interface, we can record the request parameters, response content, or deal with the case of front end separation, exchange the user permission information with token, whether the token expires, whether the request header type is correct, prevent illegal requests and so on

  1. logRequestBody()Method: Record the body of the request message
  2. logResponseBody()Method: Record the body of the response message

[note: the request flow can only read it once, the next time will not be able to read, so here is to use a custom MultiReadHttpServletRequest tool to solve the problem of flow can only be read once, the response by the same token, the concrete at the end of the article for reference the demo source code to achieve 】

@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 request's message body logRequestBody(wrappedRequest); // String token = "123"; // In the case of front-end separation, the token is stored in the cookie after the front-end login. String token = Wrappedrequest.getheader (Constants.REQUEST_HEADER); Log.debug (" Background check token :{}", token); If (StringUtils. IsNotBlank (token)) {/ / check the token SecurityUser SecurityUser = userDetailsService. GetUserByToken (token); If (securityUser = = null | | securityUser. GetCurrentUserInfo () = = null) {throw new AccessDeniedException (" TOKEN expired, Please log in again!" ); } UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); / / global injection role authorization information and login user basic information SecurityContextHolder. GetContext () setAuthentication (authentication); } filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { stopWatch.stop(); long usedTimes = stopWatch.getTotalTimeMillis(); // Record the response message body 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. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the request url: "+ url +" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "); Constants.URL_MAPPING_MAP.put(url, url); Log.info (" parameters received by '{}' : {}", 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 :{} MS return parameters :{} ", Constants. Url_mapping_map. get(request.getrequesturi ()), useTime, payload); }}}}Copy the code

3. CustomizationUserDetailsServiceImplimplementationUserDetailsServiceAnd customSecurityUserimplementationUserDetailsAuthentication User Details

This was also mentioned in the last article, but we didn’t do the role permissions last time, so let’s add them together this time

@Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private UserRoleMapper userRoleMapper; /*** * Obtain user information by account * @param username: * @return: org.springframework.security.core.userdetails.UserDetails */ @Override public UserDetails loadUserByUsername(String The username) throws UsernameNotFoundException {/ / from the database to retrieve User information List < User > userList = userMapper. SelectList (new EntityWrapper<User>().eq("username", username)); User user; // Check whether the user exists if (! CollectionUtils.isEmpty(userList)) { user = userList.get(0); } else {throw new UsernameNotFoundException (" the user name does not exist!" ); Return new SecurityUser(user, getUserRoles(user.getid ())); } /*** * Obtain user permissions and basic information by 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; } @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; }}Copy the code

Now let’s talk about customizationSecurityUser Because Spring Security comes with itUserDetailsSometimes it may not meet our needs, so we can define our own to extend our needsgetAuthorities()Method: Grant permission information to the current user role

@data@slf4j public class SecurityUser implements UserDetails {/** * 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; } @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; }}Copy the code

4. CustomizeUrlFilterInvocationSecurityMetadataSourceimplementationFilterInvocationSecurityMetadataSourcerewritegetAttributes()Method to obtain role permission information required to access the URL

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

@Component public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired PermissionMapper permissionMapper; @Autowired RolePermissionMapper rolePermissionMapper; @Autowired RoleMapper roleMapper; ** @param object: Stores requested URL information * @return: Null: */ @override public Collection<ConfigAttribute> getAttributes(Object Object) throws String requestUrl = ((FilterInvocation) object).getrequesturl (); / / TODO ignore url here to filter, please release the if ("/login ". The equals (requestUrl) | | requestUrl. The contains (" logout ")) {return null; } / / all the database url List < Permission > permissionList = permissionMapper. SelectList (null); for (Permission permission : 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 url corresponding role permission information return SecurityConfig. CreateList (roles. ToArray (new String [roles. The size ()))); }} / / if the data is not found in the corresponding url resources for the illegal access, require users to log in to operate the return SecurityConfig. CreateList (the ROLE_LOGIN); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<? > aClass) { return FilterInvocation.class.isAssignableFrom(aClass); }}Copy the code

5. CustomizeUrlAccessDecisionManagerimplementationAccessDecisionManagerrewritedecide()Method to authenticate access to THE URL

The processing logic of this editor is to include only one of the roles

@Component public class UrlAccessDecisionManager implements AccessDecisionManager { /** * @param authentication: Role information of the current logged-in user * @param Object: Request URL information * @param Collection: ` UrlFilterInvocationSecurityMetadataSource ` the getAttributes method, said the current request need role (there can be multiple) * @ return: void */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, AuthenticationException {// Traverses the role for (ConfigAttribute ca: String needRole = ca.getAttribute(); 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 Collection of the current user <? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : Authorities) {// If (author.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; }}Copy the code

6. Custom processor without permissionUrlAccessDeniedHandlerimplementationAccessDeniedHandlerrewritehandle()methods

You can customize 403 no permission response content here. The permission processing after login is separated from the permission processing after 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())); }}Copy the code

7. At lastSecurity core configuration classTo configure the above processing

@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {/ * * * access authentication, the authentication token, signature... */ private final MyAuthenticationFilter myAuthenticationFilter; / * * * access authentication exception handling * / private final AdminAuthenticationEntryPoint AdminAuthenticationEntryPoint; / * * * user password checking filter * / private final AdminAuthenticationProcessingFilter AdminAuthenticationProcessingFilter; / / it is the login authentication related permissions for url below - = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / * * * The role of the needed to access the url information * / private final UrlFilterInvocationSecurityMetadataSource UrlFilterInvocationSecurityMetadataSource; /** * Authentication permission processing - compare the role permissions obtained above with the role of the current login user. */ Private Final UrlAccessDecisionManager UrlAccessDecisionManager; Private Final UrlAccessDeniedHandler; private Final 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 enable cross-domain http.csrf().disable().cors(); / / not login authentication exception HTTP. ExceptionHandling (.) authenticationEntryPoint (adminAuthenticationEntryPoint); Http.exceptionhandling (). AccessDeniedHandler (urlAccessDeniedHandler); / / url access 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 a session - that is, the front-end transmits the token to the background filter to verify whether the access permission exists // http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // Registry. AntMatchers ("/home").hasrole ("ADMIN"); // Indicates that the '/home' interface can only be accessed from the server's local IP address [127.0.0.1 or localhost], Other IP addresses cannot access Registry.antmatchers ("/home").hasipaddress ("127.0.0.1"); // Allow anonymous URLS - can be read as a permit interface - multiple interfaces used, split registry.antmatchers ("/login", "/index").permitall (); // registry.antMatchers("/**").access("hasAuthority('admin')"); // OPTIONS: Find the communication OPTIONS applicable to a particular url resource. Allows clients to determine resource-related OPTIONS and/or requirements, or the performance of a server registry.antmatchers (httpmethod.options, "/**").denyall (), without performing specific actions involving data transfer; Registry. and().rememberme (); // All other requests require registrie.anyrequest ().authenticated(); // Prevent iframe from causing cross-domain registry.and().headers().frameoptions ().disable(); / / custom filters. Upon login authentication username, password, HTTP addFilterAt (adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(myAuthenticationFilter, BasicAuthenticationFilter.class); } /** * Ignoring the url or static resource folder - web.ignoring(): No action is required. Does not bypass springsecurity authentication, * @param web * @throws Exception */ @override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers(HttpMethod.GET, "/favicon.ico", "/*.html", "/**/*.css", "/**/*.js"); }}Copy the code

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 (" login person: "+ name); return "Hello~ " + name; } @getMapping (value ="/admin") // Access path '/admin' has the admin role permission // @preauthorize ("hasPermission('/admin',' admin')") public String admin() {return "Hello~ administrator "; @preauthorize ("hasPermission('/admin',' admin')") public String admin() {return "Hello~ administrator "; } @getMapping ("/test") public String test() {return "Hello~ test access interface "; }}Copy the code

Page and other related code is not posted here, specific can refer to the end of the demo source

Five, run the access test effect

1. Do not log in

2. Access is normal if you have permission after login

3. No permission is granted after login

Alter database role permission association table T_SYS_ROLE_PERMISSION

Security dynamic URL permission is dependent on this table to determine, as long as the table is modified to allocate roles corresponding TO the URL permission resources, users will be dynamic to determine when accessing the URL, no need to do other processing, if the permission information is placed in the cache, modify table data timely update the cache can be!

4. After login, access urls not configured in the database and intercepting urls not ignored in Security

Six, summarized

  1. Custom processor without login permissionAdminAuthenticationEntryPoint– Customize the content of the URL response without permission when the user does not log in
  2. Custom access authentication filtersMyAuthenticationFilter– Records request response logs, whether the access is valid, and verifying the token expiration
  3. The customUrlFilterInvocationSecurityMetadataSource– Obtains the role permissions required to access the URL
  4. The customUrlAccessDecisionManager– Authenticates the permission to access the URL
  5. The customUrlAccessDeniedHandler– Failed to access the unauthorized URL after login processor – Custom 403 Unauthorized response content
  6. inSecurity core configuration classIs configured with the above processors and filters
Security dynamic permission code:

This article case demo source

Gitee.com/zhengqingya…