spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM

目录

6 FilterSecurityInterceptor 授权认证

6.1 FilterSecurityInterceptor 源码分析(不包括AccessDecisionVoter )

6.2 自定义FilterSecurityInterceptor授权认证

6.2.1 自定义FilterSecurityInterceptor授权认证步骤

6.2.2 1.数据库、持久层准备

6.2.3 2.修改自定义的 UserDetailsService

6.2.4 3.创建 Dynamic01FilterSecurityInterceptor 类

6.2.5 4.创建 DynamicSecurityMetadataSource 类

6.2.6 5.创建 DynamicAccessDecisionManager 类

6.2.7 6.修改 WebSecurityConfig 配置类

6.3 debug 查看 FilterSecurityInterceptor 执行以及思考的问题

6.3.1 debug 查看 FilterSecurityInterceptor 执行流程

 6.3.2 为什么要获取平台所有权限?然后在获取请求权限?

 6.3.3 Dynamic01FilterSecurityInterceptor 类 FILTER_APPLIED 常量有什么用?为什么要判断过滤器是否执行过?难道过滤器不是只执行一次吗?

 6.3.4 debug 的时候发现页面就请求了一次,但是debug会拦截到两次请求,怎么回事?


复制 demo02 ,拷贝一个 demo03,IDEA复制项目可以看 day04 。

 项目源代码:https://github.com/maojianqiu/Demos/tree/main/springsecurity01/demo03

6 FilterSecurityInterceptor 授权认证

之前学了身份认证,就是认证当前登录的用户是否是存在并真实的,身份认证成功后就可以访问认证后的接口(资源)了,但即便是已认证的用户也有能访问不能访问的接口(资源),那么就需要通过授权认证来判断。

今天学习授权认证,也就是认证当前登录用户都有哪些权限,并确定当前用户的权限是否能访问当前请求的接口。

之前在 spring security——学习笔记(day03)-UserDetailsService 修改获取数据方式 里面学习了 security 中默认的授权认证,也就是基于角色的权限认证。

但是 security 默认的这个多用户多角色认证是相对静态的授权认证,因为是在 security 配置类的 #http() 方法里面写死了每个请求路径需要的角色,

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                //设置自定义登录页面和请求不设置访问权限
                .antMatchers("/newlogin.html").permitAll()
                //设置游客、admin、user访问的权限,.hasRole(Str) 方法是给匹配的路径设置角色,当用户有这个角色时就可以访问这个路径。
                .antMatchers("/autho/all/**").permitAll()
                //比如说这里,写死了这个请求所需要的角色,那么如果我们想要修改这个路径所对应的角色就需要修改代码重新运行程序!
                .antMatchers("/autho/admin/**").hasRole("ADMIN")
                .antMatchers("/autho/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                //设置自定义的登录页面
                .loginPage("/newlogin.html")
                .and()
                .csrf().disable();
    }

这不是我们想要的,所以我们需要一种能够动态保存当前路径所需要的权限,并给用户添加他所需要的权限,这样,当我们访问请求时,先获取到当前用户已有的权限,然后去拿取当前请求需要的权限,并判断当前请求的权限有没有在用户权限范围内,在则能够访问,不在则没有权限访问!

security 已经帮我们实现了默认的授权认证业务了,就在 FilterSecurityInterceptor 类中,这个类也是一个过滤器,同时他还是一个拦截器:

Interceptor不是servlet规范中的java web组件, 而是Spring提供的组件, 功能上和Filter差不多. 但是实现上和Filter不一样.

Filter是servlet规范中定义的java web组件, 在所有支持java web的容器中都可以使用

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
...
...
}

 我们来看一下 FilterSecurityInterceptor 类是怎么实现的~

6.1 FilterSecurityInterceptor 源码分析(不包括AccessDecisionVoter )

我们需要知道 FilterSecurityInterceptor 类是在 security 过滤链中的最后一个,所以他的责任至关重要!最重要的授权认证就是在这里控制的!

因为有了一部分经验,所以直接 debug 读源码,如果有问题就上网搜(~˘▾˘)~

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第1张图片

通过调用登录接口,进入到 FilterSecurityInterceptor 类的 doFilter() 方法中

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
...
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
        //这里封装的 FilterInvocation fi ,是封装request, response, chain,方便参数传递、增加代码阅读性  
		invoke(new FilterInvocation(request, response, chain));
	}


	public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
		...
        //执行父类beforeInvocation方法,核心的授权业务就在这个里面,这里类似于aop中的before  
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
            //之后执行后面的//filter传递 
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		}
		...
	}

...
}
public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
...
    protected InterceptorStatusToken beforeInvocation(Object object) {
        ...
            //根据SecurityMetadataSource安全元数据获取当前接口所需的权限属性  
            Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);
            ...
                //这里是认证管理器,判断是否需要对认证实体重新认证,默认为否  
                Authentication authenticated = this.authenticateIfRequired();

                //这里是调用 attemptAuthorization() 尝试着授权认证,这里会传参FilterInvocation、接口所需权限、当前认证用户
                this.attemptAuthorization(object, attributes, authenticated);
                
            ...
    }

    private void attemptAuthorization(Object object, Collection attributes, Authentication authenticated) {
        ...
            //决策管理器开始决定是否授权,如果授权失败,直接抛出AccessDeniedException  
            this.accessDecisionManager.decide(authenticated, object, attributes);
        ...
    }

...
}


public class AffirmativeBased extends AbstractAccessDecisionManager {
...
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException {
        int deny = 0;
        //通过迭代循环拿到当前的 AccessDecisionVoter ,投票器可以是多个
        Iterator var5 = this.getDecisionVoters().iterator();

        while(var5.hasNext()) {
            //把具体的决策任务交给voter处理  
            AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
            //voter只返回-1、0、1,只有为1才算授权成功  
            int result = voter.vote(authentication, object, configAttributes);
            switch(result) {
            case -1:
                ++deny;
                break;
            case 1:
                //这里是只要一个 voter 认证成功就授权成功,就返回,不进行后面的了
                return;
            }
        }

        if (deny > 0) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }
...
}

Collection attributes =this.obtainSecurityMetadataSource().getAttributes(object);这里获取的是当前请求接口需要的权限列表信息,也就是

//我们配置了这两个接口所需要的角色
.antMatchers("/newlogin.html").permitAll()
.antMatchers("/autho/admin/**").hasRole("ADMIN")

那么当访问这两个接口时,attributes = this.obtainSecurityMetadataSource().getAttributes(object);这个 attributes 里面的 ConfigAttribute 分别包含 authenticated 、hasRole('ROLE_ADMIN'),就是说访问 "/newlogin.html" 接口时允许任意角色访问,  访问 "/autho/admin/**" 接口时只允许角色为 ROLE_ADMIN 的用户访问。

重点看这个 this.accessDecisionManager.decide(authenticated, object, attributes); 这个方法会将当前请求接口的权限当前用户权限进行对比:

这里有两个很重要的接口 AccessDecisionManager 、AccessDecisionVoter 

上方调用 decide() 方法的就是 AccessDecisionManager 的实现类,之后进入 decide() 方法后,会通过 AccessDecisionVoter 的实现类进行对比。

网上搜索:

AccessDecisionVoter 是一个投票器,负责对授权决策进行表决。然后,最终由唱票者AccessDecisionManager 统计所有的投票器表决后,来做最终的授权决策。security 提供的投票器有多个。

也及时实际核心的认证就是通过 AccessDecisionVoter 实现的。

投票器逻辑有些复杂(ಥ_ಥ)(待学习)

6.2 自定义FilterSecurityInterceptor授权认证

6.2.1 自定义FilterSecurityInterceptor授权认证步骤

先来理一下思路再来写代码:

首先,先保证身份认证业务模块的代码是正常的(可看 day01-day05),这样才能够拿取到用户的授权信息(角色,权限)。之后需要添加一个过滤器,能够拦截到所有请求的过滤器,因为所有请求都是需要经过授权认证的。在这个过滤器中我们需要将白名单(也就是不需要权限的请求)放行,之后根据当前用户拿到他的权限列表、根据当前请求接口拿到所需的最小权限(这里必然是拿到最小的所需权限,这样才能够匹配上),将所需最小权限与用户权限对比,若能够匹配上则能够访问然后执行后面的业务,若所有都匹配不上则没有权限访问直接返回403(403 forbidden,表示对请求资源的访问被服务器拒绝)。

这里需要了解,我们不用 security 默认的权限认证的,那我们就需要自己提供一个权限认证规则,我们使用 RBAC(Role-Based Access Control,基于角色的访问控制)的控制规则,一个用户拥有若干角色,每一个角色拥有若干权限。所以我们拿到用户后,再根据用户的角色(role)获取权限(resource)。

像下面这样,(补数据模型图) 

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第2张图片

然后思考一下步骤:

1.数据库、持久层准备,也就是数据库表和数据、表实体、mapper接口和xml文件。

2.修改自定义的 UserDetailsService,需要给封装用户信息的对象加入我们自己的用户信息和权限信息。用于获取用户信息和权限信息。

3.创建 Dynamic01FilterSecurityInterceptor 类,继承 FilterSecurityInterceptor 类,并覆写 invoke() 方法。用于拦截所有请求并确定是否校验。

4.创建 DynamicSecurityMetadataSource 类,实现 FilterInvocationSecurityMetadataSource,并覆写 getAttributes() 方法。用户获取平台所有权限。

5.创建 DynamicAccessDecisionManager 类,实现 AccessDecisionManager ,并覆写 decide() 方法。用于校验当前用户是否有请求权限。

6.修改 WebSecurityConfig 配置类,加入过滤链和需要的 bean 。

6.2.2 1.数据库、持久层准备

1.数据库表见源码,注意:ums_admin表只有 admin 账号,密码123456。

权限有:

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第3张图片

 可以自行添加对应的接口和权限。目前 admin 账号是 只有 resourceId=1、2权限。 

所以我的 controller 层只添加了这几个测试接口:

@Controller
@RequestMapping("/admin")
public class AdminController {

    @RequestMapping("/check")
    @ResponseBody
    public String check(){
        System.out.println("------------admincheck------------");
        return "admincheck~~";
    }
}

@Controller
@RequestMapping("/menu")
public class MenuController {

    @RequestMapping("/check")
    @ResponseBody
    public String check(){
        System.out.println("------------menucheck------------");
        return "menucheck~~";
    }
}

@Controller
@RequestMapping("/resource")
public class ResourceController {

    @RequestMapping("/check")
    @ResponseBody
    public String check(){
        System.out.println("------------resourcecheck------------");
        return "resourcecheck~~";
    }

}

2.持久层包括实体 entity,mybatis接口 mapper,mapper.xml,

看目录,具体代码看源码:

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第4张图片

除 User 类外,其余的文件是需要新增的,而 User 类是需要修改的,我们需要把我们自己的 UmsAdmin 、UmsResource 类引入 User 类,让 security 能够保存我们的用户信息和权限信息。

public class User implements UserDetails {

    //将原来的用户名密码替换我们的 UmsAdmin 类
    private UmsAdmin umsAdmin;

    //加上我们的权限类,因为权限是多个所以用 List 。
    private List resourceList;

    //这里通过构造器创建 User 对象,并设置变量数据
    public User(UmsAdmin umsAdmin, List resourceList) {
        this.umsAdmin = umsAdmin;
        this.resourceList = resourceList;
    }

    //这里获取用户权限就不是只返回角色那么简单了,这里就需要将我们的每个权限封装成 Authority 类型的变量,再添加到 List ,并返回
    @Override
    public Collection getAuthorities() {
        //返回当前用户的角色
        return resourceList.stream()
                .map(resource -> new SimpleGrantedAuthority(resource.getUrl()))
                .collect(Collectors.toList());
    }

    //这里也不要忘记修改
    @Override
    public String getPassword() {
        return umsAdmin.getPassword();
    }

    //这里也不要忘记修改
    @Override
    public String getUsername() {
        return umsAdmin.getUsername();
    }

...
    //这里也不要忘记修改
    @Override
    public boolean isEnabled() {
        return umsAdmin.getStatus()== 1? true : false;
    }
}

我们查询用户信息时,直接使用 mapper 文件中的语句就可以,但是查询用户权限时,就需要多表查询,通过用户找到角色,通过角色找到权限,所以需要在 dao 文件夹中添加对应的接口和语句,我们新增 UmsAdminRoleRelationDao 类和 UmsAdminRoleRelationDao.xml:

public interface UmsAdminRoleRelationDao {

    /**
     * 通过用户ID获取用户所有可访问资源
     */
    List getResourceListByUid(@Param("adminId") Long adminId);

}



    

3.最后不要忘记配置 application.properties 文件,我们需要添加 mapper dao文件扫描地址

#mybatis
mybatis.mapper-locations: classpath:/mapper/*.xml,classpath:/dao/*.xml

#安全路径白名单
secure.ignored.urls=/newlogin.html\
    ,/phoneUrl\ 
    ,/ \

同时还有 mybatis 配置类中的 mapper dao文件映射地址

@Configuration
@MapperScan({"com.vae.demo03.mapper","com.vae.demo03.dao"})
public class MybatisConfig {
}

6.2.3 2.修改自定义的 UserDetailsService

修改业务层 MyUserDetailsService 类

public class MyUserDetailsService implements UserDetailsService {

    //需要通过这个接口获取用户信息
    @Autowired
    UmsAdminMapper umsAdminMapper;

    //需要通过这个接口获取用户权限信息
    @Autowired
    private UmsAdminRoleRelationDao adminRoleRelationDao;

    //需要通过这个接口获取平台所有权限
    @Autowired
    private UmsResourceMapper umsResourceMapper;

    //这个方法中将用户信息和权限信息替换成我们自己的 UmsAdmin,UmsResource 实体类,并将获取到的封装成 User 类进行返回。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //使用 mybatis 的 Criteria 操作数据库(待记录)
        UmsAdminExample umsAdminExample = new UmsAdminExample();
        //这里是andUsernameEqualTo
        umsAdminExample.createCriteria().andUsernameEqualTo(username);
        List  lists = umsAdminMapper.selectByExample(umsAdminExample);
        if(lists.size() < 0){
            throw new UsernameNotFoundException("用户不存在!");
        }
        UmsAdmin umsAdmin = lists.get(0);

        //涉及到表关联获取,自己写 SQL,因为使用的是角色、资源、用户的授权模式,用户有对应的角色,查询到角色后在查询角色对应的权限
        List resourceList = getResourceList(umsAdmin.getId());

        User user = new User(umsAdmin,resourceList);

        return user;
    }

    //这个和上面的逻辑一样,就是需要修改成通过手机号码查询用户信息
    public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException {
        //使用 mybatis 的 Criteria 操作数据库
        UmsAdminExample umsAdminExample = new UmsAdminExample();
        //这里是andPhoneEqualTo
        umsAdminExample.createCriteria().andPhoneEqualTo(phone);
        List  lists = umsAdminMapper.selectByExample(umsAdminExample);
        if(lists.size() < 0){
            throw new UsernameNotFoundException("用户不存在!");
        }
        UmsAdmin umsAdmin = lists.get(0);

        //设计到表关联获取,自己写 SQL,因为使用的是角色、资源、用户的授权模式,用户有对应的角色,查询到角色后在查询角色对应的权限
        List resourceList = getResourceList(umsAdmin.getId());

        User user = new User(umsAdmin,resourceList);

        return user;
    }

    //获取用户权限方法
    public List getResourceList(Long adminId) {
        List resourceList;
        resourceList = adminRoleRelationDao.getResourceListByUid(adminId);
        return resourceList;
    }

    //这里是获取平台所有的权限
    public Map loadDataSource() {
        Map map = new ConcurrentHashMap<>();
        List resourceList = umsResourceMapper.selectByExample(new UmsResourceExample());
        for (UmsResource resource : resourceList) {
            map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getUrl()));
        }
        return map;
    }

}

思考:为什么获取平台的权限呢?

6.2.4 3.创建 Dynamic01FilterSecurityInterceptor 类


public class Dynamic01FilterSecurityInterceptor extends FilterSecurityInterceptor {

    //FILTER_APPLIED:针对当前请求,当前 filter 是否执行过一次的标志 key。
    // 若没有执行过会将该常量作为 key 添加到 request Attribute 中,
    // 后续如果又进入当前 filter bean 中(一定执行过一次),判断 request 中有这个 key ,就直接跳过当前业务调取下一个过滤链。
    //思考:为什么加这个呢?
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

    //白名单,配置在 applicatio.p 中,通过 @Value 获取。
    @Value("${secure.ignored.urls}")
    private List urls ;

    //通过 set 方式注入 AccessDecisionManager Bean,直接注入到 super 中即可
    @Autowired
    public void setMyDynamicAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);

    }

    //通过 set 方式注入 SecurityMetadataSource Bean,直接注入到 super 中即可
    @Autowired
    public void setMyDynamicSecurityMetadataSource(DynamicSecurityMetadataSource dynamicAccessDecisionManager) {
        super.setSecurityMetadataSource(dynamicAccessDecisionManager);

    }

    //核心 doFilter()
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        //封装对象,方便调用后续过滤器
        FilterInvocation fi =  new FilterInvocation(request, response, chain);

        //如果当前请求已经调用过这个 filter bean,就直接执行后续过滤器
        if ((fi.getRequest() != null)
                && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && isObserveOncePerRequest()) {
            // 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());

            logger.info("request " + fi.getRequestUrl() + " has already been security checking.");
        }
        //如果当前请求没有调用过这个 filter bean,就需要执行业务
        else {
            logger.info("request " + fi.getRequestUrl() + " will be security checking.");

            // 没有执行过就是第一次,就需要修改状态为已执行过
            if (fi.getRequest() != null && isObserveOncePerRequest()) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            //OPTIONS请求直接放行
            if (httpRequest.getMethod().equals(HttpMethod.OPTIONS.toString())) {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                return;
            }
            //白名单请求直接放行
            PathMatcher pathMatcher = new AntPathMatcher();
            for (String path : urls) {
                if (pathMatcher.match(path, httpRequest.getRequestURI())) {
                    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                    return;
                }
            }
            //到这里的都是需要权限的,所以需要进行授权认证
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                logger.info("request " + fi.getRequestUrl() + " .");
            }
            finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, null);
        }
    }

}

思考:为什么判断当前请求是否走过滤器呢?而且 FILTER_APPLIED 常量值可以随意写吗?

6.2.5 4.创建 DynamicSecurityMetadataSource 类

这个类是获取平台所有权限集合的,在 FilterSecurityInterceptor 的 super.beforeInvocation(fi);会用到

/**
 * 动态权限数据源,用于获取动态权限规则
 */
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    //存储当前平台所有权限列表
    private static Map configAttributeMap = null;

    //注入加载平台权限的业务类
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    //加载平台权限
    public void loadDataSource() {
        configAttributeMap = myUserDetailsService.loadDataSource();
    }

    //清除平台权限,如果权限又新增了,那么需要清空这里的权限
    public void clearDataSource() {
        configAttributeMap.clear();
        configAttributeMap = null;
    }

    //根据当前访问请求判断所需要的权限集合
    @Override
    public Collection getAttributes(Object o) throws IllegalArgumentException {
        //判断是否有平台权限列表
        if (configAttributeMap == null) this.loadDataSource();

        List configAttributes = new ArrayList<>();
        //获取当前访问的路径
        String url = ((FilterInvocation) o).getRequestUrl();
        //这里记得导入 hutool 包
        String path = URLUtil.getPath(url);

        //路径匹配器,路径匹配的工具
        PathMatcher pathMatcher = new AntPathMatcher();
        //拿到当前所有的权限
        Iterator iterator = configAttributeMap.keySet().iterator();
        //获取访问该路径所需所有资源,资源是可包含的
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            if (pathMatcher.match(pattern, path)) {
                configAttributes.add(configAttributeMap.get(pattern));
            }
        }
        System.out.println("DynamicSecurityMetadataSource : " + "configAttributes=" + configAttributes.toString());
        // 未设置操作请求权限,返回空集合
        return configAttributes;
    }

    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class aClass) {
        return true;
    }
}

6.2.6 5.创建 DynamicAccessDecisionManager 类

/**
 * 动态权限决策管理器,用于判断用户是否有访问权限
 */
public class DynamicAccessDecisionManager implements AccessDecisionManager {

    /**
     *
     * @param authentication 当前用户信息
     * @param object
     * @param configAttributes 平台权限集合
     */
    @Override
    public void decide(Authentication authentication, Object object,
                       Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //当接口未被配置资源时直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            return;
        }
        Iterator 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;
    }

}

6.2.7 6.修改 WebSecurityConfig 配置类

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authenticationProvider(getPhoneAuthenticationProvider())
                .userDetailsService(getUserDetailsService())
                .addFilterBefore(getPhonePasswordFilter(),UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(getDynamicSecurityFilter(), FilterSecurityInterceptor.class);

        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/newlogin.html")
                .and()
                .csrf().disable();
    }

    @Bean
    public BCryptPasswordEncoder getPasswordEncoder(){
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder;
    }

    @Bean
    public MyUserDetailsService getUserDetailsService(){
        return new MyUserDetailsService();
    }

    @Bean
    public PhoneAuthenticationProvider getPhoneAuthenticationProvider(){
        PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider();
        return new PhoneAuthenticationProvider();
    }

    @Bean
    public PhonePasswordFilter getPhonePasswordFilter() throws Exception {
        PhonePasswordFilter phonePasswordFilter = new PhonePasswordFilter("/phoneUrl");
        phonePasswordFilter.setAuthenticationManager(authenticationManager());
        return phonePasswordFilter;
    }

    @Bean
    public Dynamic01FilterSecurityInterceptor getDynamicSecurityFilter() {
        return new Dynamic01FilterSecurityInterceptor();
    }

    @Bean
    public DynamicAccessDecisionManager getDynamicAccessDecisionManager() {
        return new DynamicAccessDecisionManager();
    }

    @Bean
    public DynamicSecurityMetadataSource getDynamicSecurityMetadataSource() {
        return new DynamicSecurityMetadataSource();
    }

}

运行项目,未登录,当我们访问任意接口时,都会跳转到登陆页面 /newlogin.html 页面。通过用户名或手机号码登陆成功后,访问 /admin/check 、/menu/check访问成功,但访问 /resource/check报错403无权限。

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第5张图片

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第6张图片

 spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第7张图片

编写成功!

6.3 debug 查看 FilterSecurityInterceptor 执行以及思考的问题

6.3.1 debug 查看 FilterSecurityInterceptor 执行流程

我们在以下几个类中添加对应断点:

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第8张图片

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第9张图片 spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第10张图片

 spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第11张图片

 每个请求都会被 Dynamic01FilterSecurityInterceptor 类拦截,然后调用他的 super.beforeInvocation(fi) ,在这个方法里面调用 DynamicSecurityMetadataSource 的 getAttributes() 方法拿取访问当前请求所需要的资源 configAttributes,可以是 null。之后将 attributes 与 当前用户 authentication 的 getAuthorities() 对比,若有一个匹配上了就返回执行后面的业务,若匹配不上就抛无访问权限异常。

所以上面的断点是每个请求都会走的。

 6.3.2 为什么要获取平台所有权限?然后在获取请求权限?

也不一定是需要获取平台所有权限,看项目实际业务。

这里使用的权限表是分大小权限的,比如:/admin/** 、/admin/check、/admin/check/** 这三个,

当用户有 /admin/** 权限时,那他就可以访问 /admin/ 接口开头的所有接口,当然包括上面的三个接口啦。

例如:登录只有 /admin/** 权限的用户,访问 /admin/check 接口,那么先获取到 /admin/check 接口的访问权限有 /admin/** 、/admin/check,然后再拿这两个和 /admin/** 对比,只要有相同的就可以访问!

思考,如果那当前请求接口与用户权限对比来判断呢?

例如:登录只有 /admin/** 权限的用户,访问 /admin/check 接口,那么通过 PathMatche 将请求与用户权限进行路径匹配,pathMatcher.match( grantedAuthority.getAuthority(),path),只要有匹配的就可以访问!

我们可以将 DynamicAccessDecisionManager 类中的 decide() 方法修改成下面:

@Override
    public void decide(Authentication authentication, Object object,
                       Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //当接口未被配置资源时直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            return;
        }
//--------------------- 现在的
        String url = ((FilterInvocation) object).getRequestUrl();
        String path = URLUtil.getPath(url);

        PathMatcher pathMatcher = new AntPathMatcher();
        for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {

            if (pathMatcher.match( grantedAuthority.getAuthority(),path)) {
                //若匹配到一个权限,则直接返回
                return;
            }
        }
        //若没有权限则提示
        throw new AccessDeniedException("抱歉,您没有访问权限");

//--------------------- 原来的
//        Iterator 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("抱歉,您没有访问权限");
    }

原来我们是直接 string 判等,也就是权限必须一摸一样才可以。现在是路径匹配,只要请求路径符合用户任意一个权限的模式,就可以。

若直接路径匹配的话,就需要权限中保存请求路径的信息,这里需要根据业务进行取舍与编辑。那么 DynamicSecurityMetadataSource 类还有意义吗?路径匹配的话意义不大了,但是 Dynamic01FilterSecurityInterceptor 类是需要注入这个bean的(security 是按照原来的那种方式进行权限认证的。)。

 6.3.3 Dynamic01FilterSecurityInterceptor 类 FILTER_APPLIED 常量有什么用?为什么要判断过滤器是否执行过?难道过滤器不是只执行一次吗?

网上说不同的web container 的执行流程不一样,也就是说并不是所有的container都只过滤一次,servlet版本不同,执行过程也不同。(待确认)

我们先考虑保证过滤器执行一次的问题,过滤器执行两次也不会造成什么业务影响,该通过会通过不该通过就不通过(只是针对于目前代码来说,我分析不会影响业务,当然还要判断多种情况),但是会影响到性能,毕竟执行了两次身份验证,属于多余的业务。

既然会有执行过滤器两次(网上说的),那么就需要判断是否执行过该过滤器,如果执行过就不再执行这个业务,那么就需要一个状态来记录。用什么状态呢?我们是通过过滤链来调用的下一个过滤器的,一般 doFilter(request, response, chain);都只传这三个值,有时候只传 request 、response,所以最好把这个状态记录到 request 里面,只要这个状态没有代表没有执行过,只要有就代表执行过!

也有人说只要 Filter 继承 OncePerRequestFilter 类就可以,是的,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤。这里就不将源码了,因为他的实现方式与我们上面的思路描述是一样的。可看这篇文章博客园:OncePerRequestFilter原理简介

那我们来看一下 Dynamic01FilterSecurityInterceptor 类的实际:

public class Dynamic01FilterSecurityInterceptor extends FilterSecurityInterceptor {

    //FILTER_APPLIED:针对当前请求,当前 filter 是否执行过一次的标志 key。
    // 若没有执行过会将该常量作为 key 添加到 request Attribute 中,
    // 后续如果又进入当前 filter bean 中(一定执行过一次),判断 request 中有这个 key ,就直接跳过当前业务调取下一个过滤链。
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

...
    //核心 doFilter()
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
...
        //fi.getRequest().getAttribute(FILTER_APPLIED)在这里获取 request 中对应这个常量key的 value,若没有也就是 null,那么就代表没有执行过,若不是null就代表执行过
        if ((fi.getRequest() != null)
                && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && isObserveOncePerRequest()) {
            //就直接进入下一个过滤连
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());

        ...
        }
        else {
        ...

            // 没有执行过就是第一次,就需要将常量作为 key 加入到 request 的 Attribute 里面
            if (fi.getRequest() != null && isObserveOncePerRequest()) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }
            //然后执行后面的认证逻辑
            ...
        }
    }

}

因为 request 会贯穿整个过滤链,这样就不用怕执行重复啦!

思考:FILTER_APPLIED 常量的值怎么写?只保证唯一就可以了吗?

是的只要保证唯一,是最基本的,如果不唯一会和其他的 attribute 相撞,出现问题。那么这里为什么写  "__spring_security_filterSecurityInterceptor_filterApplied" 呢?我们可以发现 FilterSecurityInterceptor 类里面也有一个这个常量,与我们这里写的一摸一样!

为什么?因为我们已经有自己的 filter 过滤器了,那么 security 默认的根据 ROLE_ADMIN 这种的身份认证就可以不需要了,也就是不用走这个过滤器的业务,那么我们就将这个常量写成一样的就行!

为什么不把 FilterSecurityInterceptor 的过滤器去掉呢?答案是去不掉(〝▼皿▼),网上都说这个作为最后一个过滤器是必须的有的,我还不确定,这要学习 security 配置的源码才能找到答案(待学习)

所以先按照这个来配置。

 6.3.4 debug 的时候发现页面就请求了一次,但是debug会拦截到两次请求,怎么回事?

还以为是前端页面请求了两次,但是发现确实没有,于是一点一点走发现原来走了两个过滤链! 

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第12张图片

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第13张图片 网上查询后发现原来 web 过滤链不止一个,之前一直没有深究过,

spring security——学习笔记(day06)-实现授权认证-FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionM_第14张图片

可以看到,Spring Security Filter 并不是直接嵌入到 Web Filter 中的,而是通过 FilterChainProxy 来统一管理 Spring Security Filter,FilterChainProxy 本身则通过 Spring 提供的 DelegatingFilterProxy 代理过滤器嵌入到 Web Filter 之中。

DelegatingFilterProxy 很重要的一个类(待学习)

回到 debug 图里面,我们可以看到第二个图里面的过滤链其实就是第一图里面 FilterChainProxy 类的内部类 VirtualFilterChain 类的 private final FilterChain originalChain; 成员变量

originalChain 表示原生的过滤器链,也就是 Web Filter,tomcat的过滤链。我们走完 security 的过滤链之后,会接着走Web Filter的。就是如果 currentPosition == size,表示security过滤器链已经执行完毕,此时通过调用 originalChain.doFilter 进入到原生过滤链方法中,同时也退出了 Spring Security 过滤器链。

但是不知道为什么我们自定义的 security Filter 会被加入到这个里面,而 security 自带的 filter 是没有加入到里面的,可能跟 security 配置类有关,(待学习)

文章参考:

史上最简单的Spring Security教程(十九):AccessDecisionVoter简介及自定义访问权限投票器

深入理解 FilterChainProxy【源码篇】

你可能感兴趣的:(springsecurity,spring,安全,spring,boot)