Spring Security的认证与鉴权

一、前言

该文只是自己在学习微人事时,对Spring Security的初步了解。仅记录一下,供自己温习。

二、认证
spring security登录认证的流程----以用户名/密码验证为准

  1. request:http请求
  2. UsernamePaawordAuthenticationFilter:此处可以使用一个类来继承该类实现自定义的认证逻辑,如果需要从数据库读取用户名及密码,则需要自己实现一个UserDetailService的类,重loadUserByUsername方法,返回一个UserDetails对象,然后该Filter可以从request中获取到前端传来的用户名和密码,封装成一个认证请求authRequest,然后交给AuthenticationManager来认证。
  3. AuthenticationManager:接口,认证部分使用的实现类为ProviderManager。
  4. ProviderManager:负责将Filter传来的认证请求转交给某个Provider,该对象用List列表维护着Provider,当一个认证请求到来,会遍历这些Provider,如果有Provider的support方法支持这种请求,那么开始验证。
  5. AuthenticationProvider(接口):具体的认证类,当我们需要与数据库中存储的用户信息进行比对时,使用的实现类是DaoAuthenticationProvider。当我们重写UserDetailService的loadUserByUsername方法后,应该使用的是DaoAuthenticationProvider目前不清楚,为什么会是Dao这个Provider,猜测是因为UserDetailService要作为一个Bean注入容器,当检测到容器有该Service时,将DaoAuthenticationProvider加入到ProviderManager的List中。
  6. 认证:AuthenticationProvider的实现类DaoAuthenticationProvider内部持有一个UserDetailService对象,通过该对象的loadUserByUsername方法从数据库或我们自定义的地方取得真正的用户名和密码,然后与用户传输过来的进行匹配验证。

vhr这里的逻辑主要是:改写的主要是一个UserDetailService的loadUserByUsername(HrService)同时提供一个继承UsernamePasswordAuthenticationFilter并替代其工作的LoginFilter完成认证功能。LoginFilter额外添加的逻辑主要是对用户输入的验证码进行认证。

三、鉴权

Spring Security的认证与鉴权_第1张图片
认证部分主要是绿色的这些过滤器。

鉴权则主要是橙色部分的这个FilterSecurityInterceptor。

大致流程:

  1. FilterInvocation.doFilter()
  2. 在doFilter方法内调用invoke()方法
  3. invoke()方法判断请求是否鉴权过了,鉴权过了直接放行,如果没有则调用父类AbstractSecurityInterceptor的beforeInvocation()方法进行鉴权。
  4. AbstractSecurityInterceptor的beforeInvocation()方法调用AccessDecisionManager的dicide()方法进行鉴权。
  5. 如果是Wb应用,那么我们应该自己定义一个类去实现AccessDecisionManager接口,系统默认提供的一般都用不到。

进行鉴权时执行的是doFilter()方法。该方法源码只有两行,首先创建一个FilterInvocation对象,然后执行将FilterInvocation对象作为invoke方法参数,执行this.invoke()。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        this.invoke(fi);
    }

invoke方法的源码如下:

   public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //该请求已经认证过了
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }
            //鉴权
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }

    }

所以,调用了父类AbstractSecurityInterceptor的beforeInvocation()方法,该方法主要通过一个AbstractSecurityInterceptor得到一个Collection< ConfigAttribute>对象,该容器存储着配置文件中配置的过滤规则。然后通过authenticateIfRequired()得到一个Authentication对象,最后使用持有的AccessDecisionManager的dicide()方法去进行鉴权。

所以,最终的鉴权逻辑在AccessDecisionManager中完成。

示例:

Web应用的话,我们假设自定义一个CustomAccessDecisionManager类去实现AccessDecisionManager。

public class CustomAccessDecisionManager implements AccessDecisionManager {
    
    //主要鉴权逻辑,authentication包含了当前请求用户的信息,包括权限信息,权限来自于登录认证时
    //UserDetailsService中提供的authorities
    //Object其实是FileterInvocation对象,可以得到request
    //configAttributes为我们配置的过滤规则,即访问所需的权限
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {

            if (authentication == null) {
                throw new AccessDeniedException("当前访问没有权限");
            }
            //需要的权限
            ConfigAttribute configAttribute = iterator.next();
            String needCode = configAttribute.getAttribute();
            //用户所拥有的权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (StringUtils.equals(authority.getAuthority(), "ROLE_" + needCode)) {
                    return;
                }
            }
        }

        throw new AccessDeniedException("当前访问没有权限");
    }

    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

vhr鉴权部分主要是自定义:

  1. 实现了AccessDecisionManager接口的CustomUrlDecisionManager类
  2. 实现了FilterInvocationSecurityMetadataSource接口 (该接口用于加载访问时所需要的具体权限)的CustomFilterInvocationSecurityMetadataSource (简单情况下,我们直接在配置类中通过一些方法来设置一些鉴权规则,但是该项目中的权限信息是和Role表中的角色绑定的,即权限信息存储在数据库中。所以,需要自定义一个类来加载鉴权规则)

AccessDecisionManager是鉴权的主要逻辑所在:

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            //获得访问所需要的角色
            String needRole = configAttribute.getAttribute();
            //如果是登录请求
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请登录!");
                }else {
                    //只要认证通过以后,鉴权部分直接通过,不需要任何权限
                    return;
                }
            }
            //对于其他请求,将该请求所需要的角色与用户所拥有的角色进行挨个匹配
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

CustomFilterInvocationSecurityMetadataSource 用于从数据库中读取鉴权规则:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    //menu表是存储ROLE所对应的权限
    @Autowired
    MenuService menuService;  
    //用来做urls字符串匹配
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    //该方法返回本次访问需要的权限
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        //获取所有角色所对应的菜单(menu)
        List<Menu> menus = menuService.getAllMenusWithRole();
        //匹配哪些菜单拥有该请求路径,即得到哪些角色可以访问该路径
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                List<Role> roles = menu.getRoles();
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
        //如果该路径无法和任何一个menu所拥有的路径匹配,返回一个登录认证所需的角色 
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    //返回所有定义的权限
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
    //返回类对象是否支持校验
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

你可能感兴趣的:(#,java框架学习,java,spring,boot)