一文弄懂spring security 授权方式及源码跟踪

spring security 授权方式(自定义)及源码跟踪

​ 这节我们来看看 spring security 的几种授权方式,及简要的源码跟踪。在初步接触spring security时,为了实现它的授权,特别是它的自定义授权,在网上找了特别多的文章以及例子,觉得好难,但是现在自己尝试结合官方文档及demo来学习,颇有收获。

基于表达式Spel的访问控制

​ Spring Security 使用 Spring EL 进行表达式支持,不了解Spring EL的童鞋自行学习。根据文档https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#el-access 在IDEA中查看 SecurityExpressionRoot 类的上下继承关系,SecurityExpressionOperations声明了各个表达式接口,最终由WebSecurityExpressionRoot、MethodSecurityExpressionRoot实现各个具体的表达式逻辑。继承关系如下所示:

​ 在这里我们能够知道,最常用的应该就是基于Web\Method这两种方式来进行授权我们的应用。再来看下 SecurityExpressionRoot 类中定义的最基本的 SpEL 有哪些:

​ 我们简单介绍几个表达式接口:

Expression Description
hasRole(String role) 如果当前主体具有指定的角色,则返回 true
hasAnyRole(String… roles) 如果当前主体具有任何提供的角色,则返回 true
hasAuthority(String authority) 如果当前主体具有指定的权限,则返回 true。
hasAnyAuthority(String… authorities) 如果当前主体具有任何提供的权限,则返回 true
authentication 允许直接访问从 SecurityContext 获得的当前 Authentication 对象
principal 允许直接访问代表当前用户的主体对象

授权方式

基于Web/Url的安全表达式

这种方式可以对单个Url进行安全验证,也可以对批量的Url进行安全验证,比如

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
        	// 针对/admin/api/hello 这个接口要有p1权限
            .antMatchers("/admin/api/hello").hasAuthority("p1")
        	// 对于访问/user/api/** 的接口要有user权限
            .antMatchers("/user/api/**").hasRole("USER")
            .antMatchers("/app/api/**").permitAll()
            .antMatchers("/css/**", "/index").permitAll()
            .antMatchers("/user/**").hasRole("USER")
            .and()
            .formLogin()
            .loginPage("/login")
            .failureUrl("/login-error")
            .permitAll();
}

基于Method的安全表达式

Method Security Expressions

Method security is a bit more complicated than a simple allow or deny rule. Spring Security 3.0 introduced some new annotations in order to allow comprehensive support for the use of expressions.

Spring Security 3.0 引入了一些新的注解,以便全面支持表达式的使用,分别是 @PreAuthorize , @PreFilter , @PostAuthorize and @PostFilter 相信大家有web开发的基础,不难知道这几个注解的意思。

  • @PreAuthorize:在访问方法前进行鉴权

  • @PreFilter:同上

  • @PostAuthorize:在访问方法后进行鉴权

  • @PostFiltert:同上

    public class AdminController {
    
        @GetMapping("hello")
        @PostAuthorize("hasRole('User')")
        public String hello() {
            return "hello, admin";
        }
    
        @GetMapping("p1")
        @PreAuthorize("hasAuthority('p1')")
        public String p1() {
            return "hello, p1";
        }
    }

但是基于方法的需要事先在配置类添加注解,表示开启方法验证。

@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)

根据官网介绍还有其他2中方式(基于AOP 、spring security原生的注释@Secure),有兴趣的小伙伴可以自行参阅https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#secure-object-impls

授权原理

​ 根据文档https://docs.spring.io/spring-security/site/docs/5.4.9/reference/html5/#secure-object-impls指出,spring security提供了拦截器来控制安全对象的访问,例如方法调用、web请求。AccessDecisionManager 做出关于是否允许调用继续进行的调用前决定。

​ 查看 AccessDecisionManager 接口:

decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)

参数说明:

  • authentication:当前登录对象主体
  • object: 当前安全保护对象
  • configAttributes:访问当前对象必须要有的权限属性

再看看看它的三个实现类,默认的实现是根据各个实现类的投票机制来决定是否能够访问当前安全保护对象的:

AffirmativeBased:只要configAttributes中有一个权限满足就可以访问当前保护对象

ConsensusBased:满足超过一半的权限就能够访问当前保护对象

UnanimousBased:configAttributes中所有的权限都满足才能访问当前保护对象

由于我们不知道默认是哪个实现类,所以我们在三个类上的decide方法都打上断点,这样我们就能知道默认是哪个实现类了,

内部的投票实现有兴趣的小伙伴自行探索,到这样我们大概就明白spring security默认的授权实现机制了。接着我们根据该机制去实现我们的自定义授权方式。

给出官网的一张原理图

  1. 首先,FilterSecurityInterceptor 从 SecurityContextHolder 获得一个 Authentication
  2. 其次,FilterSecurityInterceptor 从传入 FilterSecurityInterceptor 的 HttpServletRequest、HttpServletResponse 和 FilterChain 创建一个 FilterInvocation
  3. 接下来,它将 FilterInvocation 传递给 SecurityMetadataSource 以获取 ConfigAttributes
  4. 最后,它将 Authentication、FilterInvocation 和 ConfigAttributes 传递给 AccessDecisionManager。
    1. 如果授权被拒绝,则抛出 AccessDeniedException。在这种情况下,ExceptionTranslationFilter 处理 AccessDeniedException
    2. 如果访问被授予,FilterSecurityInterceptor 继续使用允许应用程序正常处理的 FilterChain。

自定义授权方式

​ 根据葫芦画瓢,我们首先需要

1、自定义一个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;
    }
}

2、接着实现一个FilterInvocationSecurityMetadataSource实现类,这个类给出访问保护对象具体需要的哪些权限。

/**
 * 这个类的作用,主要是根据用户传来的请求地址,分析出请求需要的角色
 */
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        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);
            }
        }
        return SecurityConfig.createList("ROLE_LOGIN");
    }

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

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

3、将上面2个对象添加到拦截器中,给FilterSecurityInterceptor重新设置它的这2个属性

http.authorizeRequests()
        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                object.setAccessDecisionManager(customUrlDecisionManager);
                object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                return object;
            }
        })

​ 相信到这里,小伙伴也能根据自己实际项目需要怎样的授权方式去进行实现了,如果是AOP/@secure方式的则需要再看一下文档说明。

全部评论

相关推荐

11-09 14:54
已编辑
华南农业大学 产品经理
大拿老师:这个简历,连手机号码和照片都没打码,那为什么关键要素求职职位就不写呢? 从上往下看,都没看出自己到底是产品经理的简历,还是电子硬件的简历? 这是一个大问题,当然,更大的问题是实习经历的描述是不对的 不要只是去写实习流程,陈平,怎么去开会?怎么去讨论? 面试问的是你的产品功能点,是怎么设计的?也就是要写项目的亮点,有什么功能?这个功能有什么难处?怎么去解决的? 实习流程大家都一样,没什么优势,也没有提问点,没有提问,你就不得分 另外,你要明确你投的是什么职位,如果投的是产品职位,你的项目经历写的全都是跟产品无关的,那你的简历就没用 你的面试官必然是一个资深的产品经理,他不会去问那些计算机类的编程项目 所以这种四不像的简历,在校招是大忌
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务