1.默认的权限控制
在之前的学习中,使用到了Spring Security的@PreAuthorize注解来定义一个接口所需要的权限,基本的步骤如下:
@Controller
@Api(tags = "PmsProductController", description = "商品管理")
@RequestMapping("/product")
public class PmsProductController {
@Autowired
private PmsProductService productService;
@ApiOperation("创建商品")
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
//在此处使用PreAuthorize注解控制权限
@PreAuthorize("hasAuthority('pms:product:create')")
public CommonResult create(@RequestBody PmsProductParam productParam, BindingResult bindingResult) {
int count = productService.create(productParam);
if (count > 0) {
return CommonResult.success(count);
} else {
return CommonResult.failed();
}
}
}
然后,通过自定义的loadUserByUsername方法去数据库中查询出用户的权限信息:
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
@Override
public UserDetails loadUserByUsername(String username){
//获取用户信息
UmsAdmin admin = getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
}
}
之后Spring Security把用户拥有的权限值和接口上注解定义的权限值进行比对,如果包含则可以访问,反之就不可以访问;
使用这种方法来配置接口的权限比较清晰,易于理解,但是如果接口的数量一多,配置的工作量就比较大了,修改起来也比较麻烦
2.基于路径的动态权限控制
因为上述的方法配置起来存在一定问题,所以,学习新的方法来优化一下
从这种方法的名字就可以知道,这种权限校验的方式是通过请求带来的路径自动校验的,这样一来,就能为我们省下不少功夫
首先,需要创建一个过滤器,来实现动态权限控制:
在这一步骤中,重要的是super.beforeInvocation(fi)这段代码,调用父类的beforeInvocation方法进行鉴权逻辑的总体控制
DynamicSecurityFilter
/**
* 动态权限过滤器,用于实现基于路径的动态权限过滤
*/
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
@Autowired
private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Autowired
public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
super.setAccessDecisionManager(dynamicAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
//OPTIONS请求直接放行,否则会出现跨域问题
if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
//白名单请求直接放行
PathMatcher pathMatcher = new AntPathMatcher();
for (String path : ignoreUrlsConfig.getUrls()) {
if(pathMatcher.match(path,request.getRequestURI())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
}
//此处开始进入动态权限校验的流程
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return dynamicSecurityMetadataSource;
}
}
以下是一个简化版的beforeInvocation方法示例:
public Object beforeInvocation(Object object) {
// 获取与当前请求相关的安全元数据(即访问当前接口所需权限)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 检查是否需要安全处理
if (attributes == null || attributes.isEmpty()) {
return object;
}
// 获取当前用户的身份信息
Authentication authenticated = SecurityContextHolder.getContext().getAuthentication();
// 进行访问决策,将用户拥有的权限与此接口所需权限进行比对
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException e) {
// 权限不足,抛出异常
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, e));
throw e;
}
// 其他前置处理逻辑(如日志记录、审计等)
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authorization successful");
}
// 返回处理结果
return object;
}
可以看到,这个方法中调用到了obtainSecurityMetadataSource().getAttributes方法和accessDecisionManager.decide方法,方法的作用已在注释中写明,这两个方法都是需要我们自己实现的,具体的方法示例如下:
getAttributes
/**
* 动态权限数据源,用于获取动态权限规则
*/
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static Map<String, ConfigAttribute> configAttributeMap = null;
@Autowired
private DynamicSecurityService dynamicSecurityService;
@PostConstruct
public void loadDataSource() {
//获取所有权限信息,key为路径,value为id:name
configAttributeMap = dynamicSecurityService.loadDataSource();
}
//在对权限数据库进行修改后,需要清空存储在内存中的Map,以更新权限信息
public void clearDataSource() {
configAttributeMap.clear();
configAttributeMap = null;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (configAttributeMap == null) this.loadDataSource();
List<ConfigAttribute> configAttributes = new ArrayList<>();
//获取当前访问的路径
String url = ((FilterInvocation) o).getRequestUrl();
String path = URLUtil.getPath(url);
PathMatcher pathMatcher = new AntPathMatcher();
Iterator<String> iterator = configAttributeMap.keySet().iterator();
//一一比对,获取访问该路径所需资源,将值存入configAttributes返回
while (iterator.hasNext()) {
String pattern = iterator.next();
if (pathMatcher.match(pattern, path)) {
configAttributes.add(configAttributeMap.get(pattern));
}
}
// 未设置操作请求权限,返回空集合
return configAttributes;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
decide
/**
* 动态权限决策管理器,用于判断用户是否有访问权限
*/
public class DynamicAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 当接口未被配置资源时直接放行
if (CollUtil.isEmpty(configAttributes)) {
return;
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
//将访问接口所需资源与用户拥有资源进行比对
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("抱歉,您没有访问权限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
在这里有个细节,在配置Bean的时候,使用@ConditionalOnBean注解来判断是否使用动态权限管理,这样一来,在其他模块使用Spring Security时,配置UserDetailsService和DynamicSecurityService对象即可
使用@ConditionalOnBean注解
/**
* SpringSecurity通用配置
* 包括通用Bean、Security通用Bean及动态权限通用Bean
*/
@Configuration
public class CommonSecurityConfig {
@ConditionalOnBean(name = "dynamicSecurityService")
@Bean
public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
return new DynamicAccessDecisionManager();
}
@ConditionalOnBean(name = "dynamicSecurityService")
@Bean
public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
return new DynamicSecurityMetadataSource();
}
@ConditionalOnBean(name = "dynamicSecurityService")
@Bean
public DynamicSecurityFilter dynamicSecurityFilter(){
return new DynamicSecurityFilter();
}
}
配置我们自定义的动态权限过滤器:
/**
* SpringSecurity相关配置,仅用于配置SecurityFilterChain
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;
@Autowired(required = false)
private DynamicSecurityFilter dynamicSecurityFilter;
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//省略若干代码...
//有动态权限配置时添加动态权限校验过滤器
if(dynamicSecurityService!=null){
registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
}
return httpSecurity.build();
}
}