SpringBoot & SpringSecurity 下 - 动态角色和权限验证
之前写的是 SpringSecurity 的标准用法,每个接口需要提前定义哪些角色可以访问,下面讲解如何动态增加角色以及动态配置链接可访问角色。
一、修改 WebSecurityConfig
package com.bjy.qa.util.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.annotation.Resource; /** * SpringSecurity 配置 */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别的权限认证,后续不需要,要删除 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 过滤器 @Resource AuthenticationEntryPointImpl autoetaticaticcAutryPointImpl; // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发) @Resource AccessDeniedPointImpl accessDeniedPointImpl; // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发) @Resource CustomFilterInvocationSecurityMetadataSource appFilterInvocationSecurityMetadataSource; @Resource CustomAccessDecisionManager customerAccessDecisionManger; @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 关闭跨站请求防护 .cors().and() // 配置 CORS支持 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不通过 session 获取 SecurityContext // 配置自定义的权限验证类 http .authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(appFilterInvocationSecurityMetadataSource); // 自定义请求 URL 与配置属性之间的映射类(根据请求的 URL,查找适用于该 URL 的权限配置属性,比如角色,权限等信息) o.setAccessDecisionManager(customerAccessDecisionManger); // 自定义密名账户验证类,它负责根据 Authentication 对象、访问主体的权限信息以及资源的安全配置,决定主体是否有权限访问资源 return o; } }); // 配置异常处理器 http .exceptionHandling() .authenticationEntryPoint(autoetaticaticcAutryPointImpl) // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发) .accessDeniedHandler(accessDeniedPointImpl); // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发) // 添加 JWT 过滤器 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
主要改动如下:
1、去掉 EnableWebSecurity,不去也没问题
2、替换自定义权限和认证
二、CustomAccessDecisionManager
package com.bjy.qa.util.security; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; /** * 自定义密名账户验证类,它负责根据 Authentication 对象、访问主体的权限信息以及资源的安全配置,决定主体是否有权限访问资源 */ @Component public class CustomAccessDecisionManager implements AccessDecisionManager { /** * 判断当前登录的用户是否具备当前请求URL所需要的角色信息。如果不具备,就抛出 AccessDeniedException 异常,否则不做任何事即可 * @param authentication 当前登录用户的信息 * @param object FilterInvocation对象,可以获取当前请求对象 * @param configAttributes FilterInvocationSecurityMetadataSource 中的 getAttributes() 方法的返回值,即当前请求URL所需的角色 */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> auths = authentication.getAuthorities(); // 获取当前登录用户的角色信息(之前在 JwtAuthenticationTokenFilter 中存入的用户角色信息) // 遍历当前请求 URL 所需要的角色信息(这里角色以 |admin|user| 形式放回所以只判断一次) for (ConfigAttribute configAttribute : configAttributes) { if ("ROLE_ANONYMOUS".equals(configAttribute.getAttribute())) { return; } // 遍历当前登录用户的角色信息,判断是否具备当前请求 URL 所需要的角色信息 for (GrantedAuthority authority : auths) { if (configAttribute.getAttribute().contains("|" + authority.getAuthority() + "|")) { return; } } } throw new AccessDeniedException("拒绝访问 - 未授权"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
三、CustomFilterInvocationSecurityMetadataSource
package com.bjy.qa.util.security; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * 自定义请求 URL 与配置属性之间的映射类(根据请求的 URL,查找适用于该 URL 的权限配置属性,比如角色,权限等信息) */ @Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static List<MenuRole> menuRoleList; // 所有权限 private List<MenuRole> getRoleMenu() { if (menuRoleList != null) { return menuRoleList; } // 临时写死 url 对应权限,后续要从数据库中查询 menuRoleList = new ArrayList<>(); MenuRole menuRole = new MenuRole(); menuRole.setUrl("/user/user/login123/*"); // url 链接 menuRole.setRole("|ADMIN|user|"); // 角色信息,以 | 包裹,这个链接哪些角色可以访问都写到一起 CustomFilterInvocationSecurityMetadataSource.menuRoleList.add(menuRole); MenuRole menuRole1 = new MenuRole(); menuRole1.setUrl("/user/user/logout"); menuRole1.setRole("|admin|"); CustomFilterInvocationSecurityMetadataSource.menuRoleList.add(menuRole1); return menuRoleList; } /** * 判断当前请求的 URL 是否需要权限验证,如果是返回角色信息(目前是现实的黑名单,未匹配到都放行) * @param url * @return */ // TODO: 2023/7/25 后续这里放到 starter-cache 中,不用每次都匹配 private String matcher(String url) throws IllegalArgumentException { AntPathMatcher antPathMatcher = new AntPathMatcher(); // Spring URL 匹配器,支持 Ant 风格的路径匹配表达式(支持通配符 * 和 **。`*匹配单路径段,**`匹配多路径段。) for (MenuRole menu : getRoleMenu()) { if (antPathMatcher.match(menu.getUrl(), url)) { return menu.getRole(); // 匹配到了,需要验证权限 } } return "ROLE_ANONYMOUS"; // 没有匹配到,不需要验证权限 } /** * 返回当前请求 URL 所需要的角色信息 * @param object * @return * @throws IllegalArgumentException */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) object).getRequestUrl(); return SecurityConfig.createList(matcher(requestUrl)); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return false; } }
四、MenuRole
package com.bjy.qa.util.security; public class MenuRole { String url; String role; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } }
五、测试
1、login 登录正常
2、logout接口有权限可以访问
3、getinfo没在黑名单,可以访问
4、没有权限,拒绝访问
参考文档:
https://blog.csdn.net/weixin_43740982/article/details/120979904