SpringSecurity 基础

什么是安全框架

安全框架顾名思义,就是解决系统安全问题的框架。任何应用开发的计划阶段都应该确定一组特定的安全需求,如身份验证、授权和加密方式。不使用安全框架之前,我们需要手动处理每个资源的访问控制,针对不同的项目都需要做不同对处理,此时就会显得非常麻烦,并且低效率引起的额外开销会延缓开发周期。使用安全框架,使开发团队能够选择最适合这些需求的框架,可以通过配置的方式实现对资源的访问限制,使得开发更加的高效。

常用的安全框架

Spring Security: Spring 家族一员,是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(控制反转)、DI(依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理功能。使用 Shiro 的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

SpringSecurity 简介

Spring Security 是一个高度自定义的安全框架,利用 Spring loC、DI 和 AOP 功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大星重复代码的工作。

使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE 的 Servlet 规范或 EJ8 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR 或 EAR 级别无法移植,因此如果你更换服务器环境,还有大星工作去重新配置你的应用程序,使用 Spring Security 解决了这些问题,并且提供了可定制的安全功能,比如认证和授权。

SpringBoot 没有发布之前,Shiro 应用更加广泛,因为 Shiro 是一个强大且易用的 Java 安全框架,能够非常清晰的处理身份验证、授权、管理会话以及密码加密。利用其易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。但是 Shiro 只是一个框架而已,其中的内容需要自己的去构建,前后是自己的,中间是Shiro帮我们去搭建和配置好的。

SpringBoot 发布后,随着其快速发展,Spring Security 重新进入人们的视野。SpringBoot 解决了 Spring Security 各种复杂的配置,Spring Security 在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器来控制权限的访问,从而实现安全,也就是说 Spring Security 除了不能脱离 Spring,Shiro 的功能它都有。

  • 在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。

  • 在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

SpringSecurity 入门

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

导入配置

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 禁用csrf(跨站请求伪造)
                .cors().and().csrf().disable()
                // 设置表单登陆以及登录页面,自动开启登录页,如果没有登录,没有权限就会来到登录页面
                .formLogin().loginPage("/login.html").and()
                // 过滤请求
                .authorizeRequests()
                // 访问此地址不需要进行身份认证,允许直接访问,防止重定向死循环
                .antMatchers("/login.html").permitAll()
                // 除上面外的所有请求全部需要鉴权认证,访问任何资源都需要身份认证
                .anyRequest().authenticated();
    }
}

登录测试

启动服务之后,如果只实现一个 WebSecurityConfigurerAdapter 然后重写一下 configure 方法,效果会默认使用Spring Security 的登录页 ,同时项目启动时后台会打印出一个默认的密码,然后使用任意账号就可以进行登录访问指定的资源。

SpringSecurity 核心

基本原理

Spring Security 所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源,并且采用的是责任链的设计模式。

Spring Security 对 Web 资源的保护是靠过滤器链(Filter Chain)实现的。当初始化 Spring Security 时,会创建一个名为 springSecurityFilterChain 的 Servlet 过滤器链,类型 FilterChainProxy,它实现 Filter,因此外部的请求会经过此类,下图是 Spring Security 过虑器链结构图:

image

FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain 所包含的各个 Filter,同时这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理。
Spring Security 功能的实现主要是由一系列过滤器链相互配合完成,如下图:

image

下面介绍过滤器链中主要的几个过滤器及其作用:

  • WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

  • SecurityContextPersistenceFilter :每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除。

  • UsernamePasswordAuthenticationFilter :用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变。

  • HeaderWriterFilter:用于将头信息加入响应中。

  • CsrfFilter:用于处理跨站请求伪造。

  • LogoutFilter:用于处理退出登录。

  • DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

  • BasicAuthenticationFilter:检测和处理 http basic 认证。

  • RequestCacheAwareFilter:用来处理请求的缓存。

  • SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。

  • AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

  • SessionManagementFilter:管理 session 的过滤器

  • ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。

  • FilterSecurityInterceptor: 是用于保护web资源的,使用 AccessDecisionManager 对当前用户进行授权访问。

  • RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

下面看一下 Spring Security 整个执行流程图,只要把 Spring Security 的执行过程弄明白了,这个框架就会变得很简单:

image

流程说明:

  1. 客户端发起一个请求,进入 Security 过滤器链。

  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。

  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

  4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。

核心配置

系统集成 Spring Security 后,通过创建配置类并继承 WebSecurityConfigurerAdapter 类,这个类里面可以完成上述流程图的所有配置,也就是认证及授权。接下来我们通过一个完整的配置类来进行详细认识:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 资源请求配置
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // ...
    }

    /**
     * 全局安全性配置
     */
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(new String[]{"/v3/api-docs", "/swagger-resources/configuration/ui", 
            "/swagger-resources", "/swagger-ui.html", "/swagger-ui/*", "/modeler/**", "/**/doc.html",
             "/favicon.ico", "/definition/**", "/activiti/**", "/**/*.css", "/**/*.js", "/**/*.png", 
             "/**/*.gif", "/swagger-resources/**", "/**/*.ttf", "/upload/**", "/process/read-resource/**",
              "/ueditor/**", "/**/export/**", "/**importGdCache/**", "/**/sysGlobalConfig/**", "/OAuth/**", "/v1/**"});
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

configure(AuthenticationManagerBuilder auth):身份认证接口

AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让 Spring Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);

如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。

如果重写了该方法,Spring Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。

configure(WebSecurity web):全局安全性配置

此方法用于配置影响全局安全性的配置,比如配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求,一般用于配置全局的某些通用事物,比如静态资源等。

configure(HttpSecurity http):资源请求配置

这个方法是整个 Spring Security 的核心,也是最复杂的部分,通过案例简单的说明一些常用配置,详细说明会在权限控制中说明。

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin().loginPage("/login_page") // 自定义登录页
            .passwordParameter("username") // 用户名属性名
            .passwordParameter("password") // 密码属性名
            .loginProcessingUrl("/sign_in") // 登录请求路径
            .permitAll(); // 代表任意用户可访问
            .cors().and()).csrf().disable()) // 禁用csrf(跨站请求伪造)
            .authorizeRequests() // 过滤请求
            .antMatchers(new String[]{"/upload/**", "/definition/**", "/activiti/**"}).permitAll() // 放行匹配请求
            .anyRequest().authenticated().and() //除上面外的所有请求全部需要鉴权认证
            .exceptionHandling().accessDeniedHandler((AccessDeniedHandler) this.accessDeniedHandler); // 异常解析器
        http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 退出登录
    }
}

权限控制

匹配规则

  • anyRequest():表示匹配所有的请求,一般情况都会使用此方法,设置全部内容都需要进行认证,比如anyRequest().authenticated();

  • antMatcher(String… antPatterns): 表示匹配指定的请求,参数是不定向参数,每个参数是一个 ant 表达式,? 表示匹配一个字符,* 表示匹配 0~N 个字符,** 表示匹配 0~N 个目录。例如 antMatchers( "/**/*.js").permitAll()

  • regexMatchers(String… regexPatterns):使用正则表达式进行匹配,与 antMatchers() 主要的区别就是参数,antMatchers()参数是 ant 表达式,二 regexMatchers() 参数是正则表达式。例如 .regexMatchers( ".+[.]js").permitAll()

  • mvcMatchers():适用于配置了 servletPath 的情况。.servletPath() 是 mvcMatchers() 返回值特有的方法,例如 .mvcMatchers( "demo").servletPath( "/bjsxt").permitAll() 等价于 antMatchers( "/bjsxt/demo").permitAll()

访问控制

  • permitAll():表示所匹配的 URL 任何人都允许访问,也就是不需要认证,随意访问。

  • anonymous():表示可以匿名访问匹配的 URL,只是设置为 anonymous() 的 URL 会执行 filter 链中,比如说浏览商城时。

  • authenticated():表示所匹配的 URL 都需要被认证才能访问,也就是用户登录后可访问。

  • denyAll():表示所匹配的 URL 都不允许被访问。

  • rememberMe():只有被 remember me 的用户才能访问。

  • fullyAuthenticated():如果用户不是被 remember me 的,才可以访问。

角色控制

  • hasRole():用户具备某个角色即可访问资源,此方法会自动给传入的字符串加上 ROLE_ 前缀,例如 hasRole(“list”),表示拥有 ROLE_list 权限即可访问。

  • hasAnyRole():用户具备多个角色中的任意一个即可访问资源,例如 hasAnyRole(“admin”, “save”),只要具备其中一个角色,即可访问资源。

  • hasAuthority():类似于 hasRole,但是不会添加 ROLE_ 前缀,也就是说,使用 hasAuthority 更具有一致性,你不用考虑要不要加 ROLE_ 前缀,数据库什么样这里就是什么样!

  • hasAnyAuthority():类似于 hasAnyRole,只是没有前缀。

注解控制

当我们使用注解之前,必须通过 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 开启注解。

  • @PreAuthorize:方法执行前进行权限检查,允许使用 SpEL(pring表达式语言)。

  • @PostAuthorize:方法执行后进行权限检查,基本不用。

  • @Secured:类似于 @PreAuthorize,但是不允许使用 SpEL(pring表达式语言)。

例如:

@Controller
public class HelloController {

    // 只有当前登录用户名为 javaboy 的用户才可以访问该方法。
    @PreAuthorize("principal.username.equals('javaboy')")
    public String hello() {
        return "hello";
    }

    // 表示访问该方法的用户必须具备 admin 角色
    @PreAuthorize("hasRole('admin')")
    public String admin() {
        return "admin";
    }

    // 表示访问该方法的 age 参数必须大于 98,否则请求不予通过。
    @PreAuthorize("#age>98")
    public String getAge(Integer age) {
        return String.valueOf(age);
    }

    // 表示该方法的用户必须具备 user 角色,但是注意 user 角色需要加上 ROLE_ 前缀。
    @Secured({"ROLE_user"})
    public String user() {
        return "user";
    }
}

请求认证

image

让我们仔细分析认证过程:

  1. 当用户发送登录请求的时候,首先进入到 UsernamePasswordAuthenticationFilter 中进行校验。

  2. UsernamePasswordAuthenticationFilter 通过 attemptAuthentication 方法会获取用户的username以及password参数的信息,封装为 UsernamePasswordAuthenticationToken 对象,最后会进入 AuthenticationManager 接口的实现类 ProviderManager 中。AuthenticationManager(认证管理器) 本身不包含验证的逻辑,它的作用是用来管理 AuthenticationProvider。

  3. 进入 ProviderManager 类调用 authenticate() 方法,通过循环遍历判断它是否支持这种登录方式,具体的登录方式有表单登录,qq登录,微信登录等。如果支持则会进入A uthenticationProvider 接口的抽象实现类 AbstractUserDetailsAuthenticationProvider 中调用 authenticate() 方法对用户的身份进入校验。

  4. 进入 AbstractUserDetailsAuthenticationProvider 的 authenticate方法之后,UserDetail 的 user 对象是否为空,如果为空,表示还没有认证,就需要调用 DaoAuthenticationProvider 类的 retrieveUser 方法去获取用户的信息。

  5. 该扩展类的 retrieveUser 方法中调用 UserDetailsService 这个接口的实现类的 loadUserByUsername 方法去获取用户信息。如果需要自定义实现,则可以实现 UserDetails 接口,编写自己的逻辑,从数据库中获取用户密码等权限信息返回。

  6. 获取到用户信息之后,返回到 AbstractUserDetailsAuthenticationProvider 类中调用 createSuccessAuthentication() 方法,通过 PasswordEncoder 对比用户信息是否与 AuthenticationManager 一直,如果一直,则认证通过(setAuthenticated(true))。

  7. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。

请求授权

image

让我们仔细分析授权过程:

  1. 已认证用户访问受保护的 web 资源将被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子类拦截。

  2. FilterSecurityInterceptor 会从 SecurityMetadataSource 的 getAttributes() 方法,获取要访问当前资源所需要的权限。其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则。

  3. FilterSecurityInterceptor 会调用 AccessDecisionManager(授权决策器)的 decide() 方法进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

版权声明:本文为mmgmj原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/mmgmj/p/15191927.html