Spring Security之基本原理

Spring Security之基本原理

Scroll Down

引言

在这个没有隐私可言的时代,为了不让我们的REST API裸奔。便引出了SpringSecurity的学习。

依赖配置

1)依赖

<dependencies>
    <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
      <!--引入security相关核心jar包,如oauth2 -->
    </dependency>

    <!-- 第三方登陆依赖  -->
    <dependency>
      <groupId>org.springframework.social</groupId>
      <artifactId>spring-social-config</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.social</groupId>
      <artifactId>spring-social-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.social</groupId>
      <artifactId>spring-social-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.social</groupId>
      <artifactId>spring-social-web</artifactId>
    </dependency>

   <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>1.3.3.RELEASE</version>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
    <finalName>demo</finalName>
  </build>
  </dependencies>
  • 这里为啥没有指定版本号呢,因为这是一个子模块,依赖了父模块,在父模块中声明了spring-cloud-dependencies以及io.spring.platform的相关依赖,这俩东西的的好处是可以帮助我们在引入各种jar包的时候可以不用考虑版本不兼容的问题,它里面的所有jar包都是经过测试兼容的。
  • spring-boot-maven-plugin,让我们打包成可运行的jar包,如果不加后面的build,可以看到你打出来的jar包是没有把相关的依赖放进去的,是没法直接执行的。

父模块pom.xml如下

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.spring.platform</groupId>
        <artifactId>platform-bom</artifactId>
        <version>Brussels-SR4</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Dalston.SR2</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

<build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>

注意dependencyManagement的作用是声明子模块可能会引入的jar包,但其本身并不会真正引入jar包。

启动

默认什么都不做的情况下

启动类

@SpringBootApplication
@RestController
public class DemoApplication {
    public static void main(String[] args){
        SpringApplication.run(DemoApplication.class,args);
    }

    @GetMapping("/hello")
    public String Hello(){
        return "Hello spring security";
    }
}

在控制台上,注意到这一行突出的提示,这是什么都不做的情况下默认的登陆密码。
20200130221315

在浏览器上访问/hello,会弹出一个登陆表单
20200130222347

用postman发一个HTTP请求,会出现401错误,error是未认证。

{
    "timestamp": 1580394323603,
    "status": 401,
    "error": "Unauthorized",
    "message": "Full authentication is required to access this resource",
    "path": "/hello"
}

用户名可以随便输入(因为调用的是loadByUserName这个方法,是通过用户名来匹配对应的密码的),但密码一定要输入一开始主程序启动时出现的那串提示的密码。输入后,便可访问到我们的REST服务了。

修改

新建一个BrowserSecurityConfig配置类,继承WebSecurityConfigurerAdapter这个抽象类,该类提供了一个标准,可以帮助我们方便的构建自己的WebSecurityConfigurer,通过重写该类的方法来实现客制化。

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.httpBasic()  //or http.formLogin()
                .and()
                .authorizeRequests() //认证请求配置
                .anyRequest()   //任何请求
                .authenticated();  //都需要身份认证

    }
}
  • @Configuration声明这是一个配置类,在这里我们重写了WebSecurityConfigurerAdapter的configure(HttpSecurity http)方法,该,这要是对Http请求做一些安全处理。
  • 在什么都不做的情况下弹出来的窗口是基于httpBasic的,而当修改为http.formLogin()时,则切换成页面的表单认证。如下图
    20200131111857

分析

我们先来看一张图
security
SpringSecurity有如下的核心功能:

  • 认证(你是谁)
  • 授权(你能干什么)
  • 攻击防护(防止伪造身份)

在没有Spring Security的时候,我们直接访问REST APi可以得到结果,但是当我们的应用加入了Spring security之后,相当于加上了过滤器,其实Spring Security本身就是一个过滤器链,所有的请求在访问REST API时都要经过Spring Security的过滤器链,当返回应答的时候,也会走一遍这个过滤器链,然后返回给用户。

  • 在图中我们可以看到第一个绿色的过滤器链Username Password Authentication Filter,这个就是http.formLogin()这个方法所对应的过滤器。
  • 图中的BasicAuthenticationFilter是弹出登录框供用户输入的情况,过滤的是登录框中的信息,对应http.httpBasic()这个方法。

这里只讲解了2个绿色框,关于其他的,比如微信登录,第三方认证登录,其实就是配置在绿色方框中的Filter,绿色方框的Filter可以有很多,顺序也可以有变动,而且可有可无。但是后面蓝色的Exception Translation Filter和Filter Security Interceptor是Spring Security过滤器链中顺序是固定的而且是一定存在的。

我们先讲讲最后一个FilterSecurity Interceptor,它是Spring Security的守门员,也决定了我们的请求究竟能不能访问后面的REST API。在以下代码中我们配置的信息都被放到了FilterSecurity Interceptor中,所以请求来了之后它会看我们有没有做用户登录认证,如果没有认证,Interceptor就会抛出异常。当然我们还可以在我们的代码里面配置只有VIP用户才可以访问相关的信息。这样,即使我们登录认证了,但是不是VIP身份,仍然不可以访问后台的API,FilterSecurity Interceptor依然抛异常。

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.httpBasic()  //or http.formLogin()
                .and()
                .authorizeRequests() //认证请求配置
                .anyRequest()   //任何请求
                .authenticated();  //都需要身份认证

    }
}

既然FilterSecurity Interceptor要抛出异常,那么这些异常被谁捕获?显然是由Exception Translation Filter来处理的。它就是专门负责捕获异常并且做出相关翻译的过滤器。如果说FilterSecurity Interceptor抛出没有身份认证的异常,Exception Translation Filter会看看前面究竟配置的是什么样的Filter,然后做出相应的处理。比如说前面配置的是Username Password Authentication Filter,那么就会跳转到带表单的登录页面。但如果说前面配置的是BasicAuthenticationFilter,那么就会弹出登录框等待用户输入信息。

局部代码分析

为了验证这个流程,我们在三个类中打断点
1)FilterSecurityInterceptor
20200131114115
重点看标注红框的地方,super.beforeInvocation(fi)表示在正式调用后台的rest服务之前,检查filter是否都校验通过了。fi.getChain().doFilter(fi.getRequest(), fi.getResponse())表示真正的调用后台的服务。

2)ExceptionTranslationFilter
20200131114408
这个ExceptionFilter其实我们看到它只做简单的过滤,但是它真正核心的逻辑再catch(Exception ex)里面,它捕获了Interceptor中抛出的异常,并对这些异常作相关的处理。

3)UsernamePasswordAuthenticationFilter
20200131121638
从它的构造函数我们可以看出只对/login的POST请求做拦截,核心就是获取表单中的username和password做相关校验。

现在开始运行程序

访问我们的/hello
1)进入第一个断点,直接到了守门员FilterSecurity Interceptor
20200131125455
这里为什么会直接到FilterSecurityInterceptor里面呢?第一个UsernamePasswordAuthentication Filter或者BasicAuthentication Filter都没拦截。其实,原因是这样的,我们直接访问的URL,根本就没有输入任何的用户名和密码,所以Filter如果发现没有任何输入信息,它就放过了,什么都不处理。而且我们刚才看了截图,发现这个Filter只关注POST的/login请求。继续往下看

2)第二个断点。在我们使用了Security后,任何的请求都要经过认证,所以我们直接访问/hello,就会被FilterSecurity Interceptor拦截,并抛出一个异常,这时候,ExceptionTranslationFilter就登场了,把这个异常捕获。
20200131125838

3)ExceptionTranslationFilter会根据我们前面自己配置WebSecurityConfigurer来将我们重定向到认证(登陆)界面。输入用户名和密码后便回进入第三个断点,就是我们的UsernamePasswordAuthenticationFilter(注意,在此过程中如果step over的话会经过相当多的Filter,建议直接跳过)
20200131133113
为什么会被拦截,因为我们真正的发起了login的post请求。注意看红色方框中的内容。

4)突破了一层又一层的过滤器后,又来到了FilterSecurityInterceptor这个守门员类中
20200131134947
可以看到我们此时已经获取了Token,通过了认证,红框中的语句即为调用我们的REST API,后面将会由DispatcherServlet来进行控制器的调度。

Finally

20200131135634