引言
在这个没有隐私可言的时代,为了不让我们的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";
}
}
在控制台上,注意到这一行突出的提示,这是什么都不做的情况下默认的登陆密码。
在浏览器上访问/hello,会弹出一个登陆表单
用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()时,则切换成页面的表单认证。如下图
分析
我们先来看一张图
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
重点看标注红框的地方,super.beforeInvocation(fi)表示在正式调用后台的rest服务之前,检查filter是否都校验通过了。fi.getChain().doFilter(fi.getRequest(), fi.getResponse())表示真正的调用后台的服务。
2)ExceptionTranslationFilter
这个ExceptionFilter其实我们看到它只做简单的过滤,但是它真正核心的逻辑再catch(Exception ex)里面,它捕获了Interceptor中抛出的异常,并对这些异常作相关的处理。
3)UsernamePasswordAuthenticationFilter
从它的构造函数我们可以看出只对/login的POST请求做拦截,核心就是获取表单中的username和password做相关校验。
现在开始运行程序
访问我们的/hello
1)进入第一个断点,直接到了守门员FilterSecurity Interceptor
这里为什么会直接到FilterSecurityInterceptor里面呢?第一个UsernamePasswordAuthentication Filter或者BasicAuthentication Filter都没拦截。其实,原因是这样的,我们直接访问的URL,根本就没有输入任何的用户名和密码,所以Filter如果发现没有任何输入信息,它就放过了,什么都不处理。而且我们刚才看了截图,发现这个Filter只关注POST的/login请求。继续往下看
2)第二个断点。在我们使用了Security后,任何的请求都要经过认证,所以我们直接访问/hello,就会被FilterSecurity Interceptor拦截,并抛出一个异常,这时候,ExceptionTranslationFilter就登场了,把这个异常捕获。
3)ExceptionTranslationFilter会根据我们前面自己配置WebSecurityConfigurer来将我们重定向到认证(登陆)界面。输入用户名和密码后便回进入第三个断点,就是我们的UsernamePasswordAuthenticationFilter(注意,在此过程中如果step over的话会经过相当多的Filter,建议直接跳过)
为什么会被拦截,因为我们真正的发起了login的post请求。注意看红色方框中的内容。
4)突破了一层又一层的过滤器后,又来到了FilterSecurityInterceptor这个守门员类中
可以看到我们此时已经获取了Token,通过了认证,红框中的语句即为调用我们的REST API,后面将会由DispatcherServlet来进行控制器的调度。
Finally
Q.E.D.