我们接着上一章(四)Spring Security基于数据库的用户认证,进行开发
一:重写并实现了基于数据库的权限数据源
/**
* 权限资源
* FilterInvocationSecurityMetadataSource的默认实现是
* DefaultFilterInvocationSecurityMetadataSource
*/
@Service
public class CustomInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
private static final Logger log = LoggerFactory.getLogger(CustomInvocationSecurityMetadataSourceService.class);
private final PermissionRepository permissionRepository;
/* key 是url+method ,value 是对应url资源的角色列表 */
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();
@Autowired
public CustomInvocationSecurityMetadataSourceService(PermissionRepository permissionRepository) {
this.permissionRepository = permissionRepository;
}
/**
* 注意:
* @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
* 此方法必须在将类放入服务之前调用,且只执行一次。
*/
@PostConstruct
public void init(){
log.info("[自定义权限资源数据源]:{}","初始化权限资源");
List<Permission> permissions = permissionRepository.findAll();
permissions.forEach(item->{
Set<String> roleNames = item.getRoleNames();
List<ConfigAttribute> configAttributes = new ArrayList<>();
for (String roleName : roleNames) {
configAttributes.add(new SecurityConfig(roleName));
}
requestMap.put(new AntPathRequestMatcher(item.getUrl()),configAttributes);
});
System.out.println(requestMap.toString());
}
/**
* getAttributes方法返回本次访问需要的权限,可以有多个权限。
* 在上面的实现中如果没有匹配的url直接返回null,
* 也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。
*
* 访问配置属性(ConfigAttribute)用于给定安全对象(通过的验证)
*
* @param object 安全的对象
* @return 用于传入的安全对象的属性。 如果没有适用的属性,则应返回空集合。
* @throws IllegalArgumentException 如果传递的对象不是SecurityDatasource实现支持的类型,则抛出异常
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
log.info("[自定义权限资源数据源]:{}","获取本次访问需要的权限");
if(requestMap.isEmpty()){
init();
}
final HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(request)) {
log.info("[自定义权限资源数据源]:当前路径[{}]需要的资源权限:[{}] ==> 触发鉴权决策管理器",entry.getKey(),entry.getValue().toString());
return entry.getValue();
}
}
log.info("[自定义权限资源数据源]:{}==> {}","白名单路径",request.getRequestURI());
return null;
}
/**
*
*
* getAllConfigAttributes方法如果返回了所有定义的权限资源,
* Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。
*
*
* 如果可用,则返回由实现类定义的所有ConfigAttribute。
*
* AbstractSecurityInterceptor使用它对针对它ConfigAttribute的每个配置属性执行启动时验证。
*
* @return ConfigAttribute,如果没有适用的,就返回null
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<>();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
allAttributes.addAll(entry.getValue());
}
log.info("[自定义权限资源数据源]:获取所有的角色==> {}",allAttributes.toString());
return allAttributes;
}
/**
* AbstractSecurityInterceptor 调用
* supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
*
* @param clazz 正在查询的类
* @return 如果实现可以处理指定的类,则为true
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
二:重写权限决策
根据URL资源权限和用户角色,进行鉴权
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {
private static final Logger log = LoggerFactory.getLogger(CustomAccessDecisionManager.class);
/**
* 权限鉴定
*
* @param authentication from SecurityContextHolder.getContext() => userDetails.getAuthorities()
* @param object 就是FilterInvocation对象,可以得到request等web资源。
* @param configAttributes from MetaDataSource.getAttributes(),已经被框架做了非空判断
* @throws AccessDeniedException 如果由于身份验证不具有所需的权限或ACL特权而拒绝访问
* @throws InsufficientAuthenticationException 如果由于身份验证没有提供足够的信任级别而拒绝访问
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
log.info("****************************************权限鉴定********************************************");
/*FilterInvocation filterInvocation = (FilterInvocation) object; // object 是一个URL
log.info("[当前路径[{}]需要的资源权限]:{}",filterInvocation.getRequestUrl(),configAttributes);*/
log.info("[登录用户[{}]权限]:{}",authentication.getName(),authentication.getAuthorities());
if(configAttributes == null){
return;
}
for (ConfigAttribute configAttribute : configAttributes) {
/* 资源的权限 */
String attribute = configAttribute.getAttribute();
/* 用户的权限 */
for (GrantedAuthority authority : authentication.getAuthorities()) { // 当前用户的权限
if(authority.getAuthority().trim().equals("ROLE_ANONYMOUS"))return;
log.info("[资源角色==用户角色] ? {} == {}", attribute.trim(), authority.getAuthority().trim());
if (attribute.trim().equals(authority.getAuthority().trim())) {
log.info("[鉴权决策管理器]:登录用户[{}]权限匹配",authentication.getName());
return;
}
}
}
log.info("[鉴权决策管理器]:登录用户[{}]权限不足",authentication.getName());
throw new AccessDeniedException("权限不足");
}
/**
* AbstractSecurityInterceptor 调用,遍历ConfigAttribute集合,筛选出不支持的attribute
*
* @param attribute a configuration attribute that has been configured against the
* <code>AbstractSecurityInterceptor</code>
* @return true if this <code>AccessDecisionManager</code> can support the passed
* configuration attribute
*/
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
/**
* AbstractSecurityInterceptor 调用,验证AccessDecisionManager是否支持这个安全对象的类型。
* supports(Class)方法被安全拦截器实现调用,
* 包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。
*
* @param clazz the class that is being queried
* @return <code>true</code> if the implementation can process the indicated class
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
三:实现AbstractSecurityInterceptor
默认实现是FilterSecurityInterceptor,进行访问资源时,会通过这个拦截器拦截
访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截, 其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息, 还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等), 如果权限足够,则返回,权限不够则报错并调用权限不足页面
@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private static final Logger log = LoggerFactory.getLogger(CustomFilterSecurityInterceptor.class);
private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";
private final CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService;
private final CustomAccessDecisionManager customAccessDecisionManager;
@Autowired
public CustomFilterSecurityInterceptor(CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService, CustomAccessDecisionManager customAccessDecisionManager) {
this.customInvocationSecurityMetadataSourceService = customInvocationSecurityMetadataSourceService;
this.customAccessDecisionManager = customAccessDecisionManager;
}
/**
* 初始化时将定义的DecisionManager,注入到父类AbstractSecurityInterceptor中
* 注意:
* @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
* 此方法必须在将类放入服务之前调用,且只执行一次。
*/
@PostConstruct
public void init(){
log.info("设置==========================================鉴权决策管理器");
super.setAccessDecisionManager(customAccessDecisionManager);
}
/**
* 向父类提供要处理的安全对象类型,因为父亲被调用的方法参数类型大多是Object,框架需要保证传递进去的安全对象类型相同
*
* @return 子类为其提供服务的安全对象的类型
*/
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
/**
* 获取到自定义MetadataSource的方法
*
* 启动时会有3次调用
* 第一次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 135行
* 第二次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 137行
* 第三次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 156行
*
* 登录时调用
* 调用:{@link AbstractSecurityInterceptor#beforeInvocation(Object)} 196行
*
* @return 权限资源映射的数据源
*/
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.customInvocationSecurityMetadataSourceService;
}
/**
*
* 由Web容器调用,以向filter指示正在将其放入服务中。
* servlet容器在实例化filter后,只调用一个init方法。
* 在要求filter执行任何过滤之前,init方法必须成功完成。
* 如果init方法满足以下条件之一,则web容器无法将筛选器放入服务:抛出 servletException
* 在web容器定义的时间内不返回
* 默认实现时NO-OP
* @param filterConfig 与正在初始化的filter实例关联的配置信息
* @throws ServletException 如果实例化失败
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("filer==========================================init");
}
/**
* 每当request/response对由于客户端请求链末端的资源而通过链时,容器调用过滤器的doFilter方法。
* 传入此方法的filter chain 允许Filter传递请求并响应链中的下一个实体。
*
* 此方法的典型实现将遵循以下模式:
* 1.检查请求
* 2.也可以使用自定义实现包装请求对象,输入filter的内容或头
* 3.(可选)使用自定义实现包装响应对象,以Filter 内容或头进行输出过滤
* 4.使用FilterChain对象的chain.doFilter()调用链中的下一个实体
* 5.在调用FilterChain中的下一个实体后,直接在响应上设置头。
* @param request 要处理的请求
* @param response 与请求关联的响应
* @param chain 提供对链中下一个Filter的访问,以便此Filter将请求和响应传递给以进行进一步处理
* @throws IOException 如果在此筛选器处理请求期间发生I/O错误
* @throws ServletException 如果由于其他原因处理失败
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("[自定义过滤器]:{}","CustomFilterSecurityInterceptor.doFilter()");
FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
invoke(filterInvocation);
}
/**
* 由Web容器调用,以向filter指示它正在退出服务。
* 只有当filter的doFilter方法中的所有线程都退出或超出时间间后,才调用此方法。
* 在web容器调用此方法之后,它将不再在此filter的实例上调用doFilter方法。
* 此方法使filter有机会clean正在保留的任何资源(例如内存、文件句柄、线程)
* 并确保任何持久状态与filter在内存中的当前状态同步。
*
* 默认实现时NO-OP
*/
@Override
public void destroy() {
log.info("filer==========================================destroy");
}
private void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null ) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
/* 调用父类的beforeInvocation ==> accessDecisionManager.decide(..) */
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
}
注意:在spring容器托管的AbstractSecurityInterceptor的bean,都会自动加入到servlet的filter chain,不用在websecurityconfig配置
示例
当权限不足时,会自动跳转到403