1.前言
在上一篇文章:Spring Security 从入门到精通之架构理解 我们熟悉了Spring Security授权、认证基本概念,下面将搭建HelloWorld入门案例。
2. HelloWorld快速入门
由于Spring Security 与 Spring 生态无缝衔接,所以在Spring Boot中只需引入web与security的依赖即可,依赖如下所示:
<!--Spring boot Web容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入SpringSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然后我们添加一个测试类并在类中新增一个方法如下所示:
然后启动项目,当访问此接口时会自动跳转到登陆页面,当我们登陆成功才可以访问到此接口。
默认Spring Security 内置了一个用户账户:user,其默认密码在项目控制台可以看到如下:
当输入用户名和密码后登陆成功后,就可以直接访问接口了。这样一个简单的入门案例就完成了。
3. 入门案例分析
3.1 自动配置分析
只需要引入相关依赖,Spring Security 默认就将我们的接口给保护起来了,必须认证才能继续访问,那么 Spring Security 是如何做到的呢?首先我们需要关注一下 Spring Boot 自动配置Spring Security类 SecurityAutoConfiguration 如下所示:
我们看到这个类默认导入了SpringBootWebSecurityConfiguration(web Security的默认配置类),此类定义内容如下所示:
通过上面defaultSecurityFilterChain 方法我们可以看到其默认对任何请求开启了认证。这也是我们默认在Spring Boot项目中引入Spring Security 后接口需要认证才可以访问的根本原因。3.1.2 章节我们提到了Spring Security 过滤器原理。defaultSecurityFilterChain 默认返回的是一个SecurityFilterChain 实例,我们可以看看其默认配置的filter有如下15个:
过滤器 | 作用 | 说明 |
---|---|---|
过滤器 | 过滤器作用 | 是否默认加载 |
ChannelProcessingFilter | 过滤请求协议如Http与Https | 否 |
WebAsyncManagerIntegrationFilter | 将WebAsyncManager与 Spring Security 进行集成 | 是 |
SecurityContextPersistenceFilter | 在处理请求前,将安全信息加载到SecurityContextHolder中以方便后续使用,请求结束再从SecurityContextHolder删除 | 是 |
HeaderWriterFilter | 头信息加入响应中 | 是 |
CorsFilter | 处理跨域问题 | 否 |
CsrfFilter 处理CSRF攻击 | 是 | |
LogoutFilter | 注销登陆 | 是 |
OAuth2AuthorizationRequestRedirectFilter | 处理Oauth2认证重定向 | 否 |
Saml2WebSsoAuthenticationRequestFilter | 处理SAML认证 | 否 |
X509AuthenticationFilter | 处理X509认证 | 否 |
AbstractPreAuthenticatedProcessingFilter | 处理预认证 | 否 |
CasAuthenticationFilter | 处理CAS单点登录 | 否 |
OAuth2LoginAuthenticationFilter | 处理Oauth2认证 | 否 |
Saml2WebSsoAuthenticationFilter | 处理SAML认证 | 否 |
UsernamePasswordAuthenticationFilter | 处理表单认证 | 是 |
OpenIDAuthenticationFilter | 处理OpenID认证 | 否 |
DefaultLoginPageGeneratingFilter | 配置默认登陆页面 | 是 |
DefaultLogoutPageGeneratingFilter | 配置默认注销页面 | 是 |
ConcurrentSessionFilter | 处理Session有效期 | 否 |
DigestAuthenticationFilter | 处理Http摘要认证 | 否 |
BearerTokenAuthenticationFilter | 处理Oauth2认证时的Access Token | 否 |
BasicAuthenticationFilter | 处理Http Basic认证 | 是 |
RequestCacheAwareFilter | 处理请求缓存 | 是 |
SecurityContextHolderAwareRequestFilter | 包装原始请求 | 是 |
JaasApiIntegrationFilter | 处理JAAS认证 | 否 |
RememberMeAuthenticationFilter | 处理记住我认证 | 否 |
AnonymousAuthenticationFilter | 配置匿名认证 | 是 |
OAuth2AuthorizationCodeGrantFilter | 处理Oauth2中的授权码 | 否 |
SessionManagementFilter | 处理Session并发问题 | 是 |
ExceptionTranslationFilter | 处理异常认证/授权中的情况 | 是 |
FilterSecurityInterceptor | 处理授权 | 是 |
SwitchUserFilter | 处理账户切换 | 否 |
Spring Security 内置了30个过滤器,在我们一个Hello World案例中默认启动了15个,Spring Security正是通过这些过滤器来实现其认证与授权。下面我们大概了解一下 Spring Security 的过滤器实现。
3.2 过滤器原理分析
Spring Security 认证模块、授权模块都是基于过滤器完成的,需要注意的是Spring Security默认并不是直接使用web Servlet容器中的Filter,而是实现了自己的过滤器并提供了一个DelegatingFilterProxy类,这个类会将Spring Security定义的过滤器转化为原生的Servlet容器中的Filter,并将这个Filter注册到原生过滤器链(Servlet容器过滤器链)中,如下图所示:
Spring Security中过滤器不仅仅只有一个,可能会有多个并组成一个滤器链。所以Spring Security 提供了一个SecurityFilterChain 用来管理多个过滤器。而这个过滤器了链又被一个特殊的过滤器 FilterChainProxy统一管理,此过滤器内嵌在DelegatingFilterProxy中,如下图所示:
Spring Security 可以通过不同的请求url 形成多个过滤器链路,多个过滤器需要指定优先级。当请求到达DelegatingFilterProxy中通过FilterChainProxy进行分发,匹配上那些过滤器链就用那些过滤器处理请求。
3.3 登陆页面生成源码分析
首先我们知道Spring Security 的认证与授权都是基于过滤器完成的,当我们在浏览器中输入:http://127.0.0.1:8001/helloWorld 后,这条请求先后都会经过默认启用的15个过滤器。
我们通过Debug 源码可以跟踪其登陆流程,关键跟踪源代码节点如下:
- FilterSecurityInterceptor拦截请求并抛出AccessDeniedException。
首先helloWorld请求经过若干过滤器后,最终到达过滤器FilterSecurityInterceptor#doFilter方法
doFilter 方法调用了本类的invoke方法,接着调用了父类AbstractSecurityInterceptor的beforeInvocation方法的如下所示:
父类beforeInvocation方法调用了自己的attemptAuthorization方法,方法定义如下:
此方法调用了访问决策器接口的decide方法,此方法实现是由类AffirmativeBased实现如下所示:
投票器AccessDecisionVoter 投了反对票,所以此方法抛出了AccessDeniedException异常。
- 抛出AccessDeniedException异常被ExceptionTranslationFilter捕获并交由LoginUrlAuthenticationEntryPoint类处理并重定向到登陆页面。
请求抛出的异常被ExceptionTranslationFilter捕获并调用了handleSpringSecurityException方法进行处理如下:
在这个方法中又调用了handleAccessDeniedException方法如下所示:
然后接着调用了sendStartAuthentication方法如下所示:
接着调用了在sendStartAuthentication方法中继续调用了类的authenticationEntryPoint实例#commence方法如下:
此实例是由LoginUrlAuthenticationEntryPoint类实现,我们关注其commence方法如下所示:
可以清晰看到请求重定向 http://127.0.0.1:8001/login。然后这个请求又要过一次过滤器链直到DefaultLoginPageGeneratingFilter的doFilter方法如下所示:
在这个方法中response输入了一个静态的html代码,所以我们就能看到最开始登陆的页面:
整个流程总结如下所示:
3.4 默认用户与密码生成源码分析
Spring Security中定义了UserDetailsService接口规范开发者自定义的用户对象接口查询,这样很方便用户将系统集成Spring Security 认证体系中,此接口只有一个查询用户详情的方法:
此方法参数只有一个username参数,注意这里的username并不是前端表单输入的用户名而是用户登陆成功后返回的用户名。在实际项目中,需要用户手动实现UserDetailsService。在HelloWorld案例中Spring Security提供了UserDetailsService接口的默认实现。
当我们只引入了Spring Security依赖,UserDetailsService 默认实现为InMemoryUserDetailsManager。此类提供了用户新增、删除、更新方法,这些方法都是基于内存实现(用户信息保存在Map中)。
再回到上面提到的UserDetailsService#loadUserByUsername 返回了一个用户详情对象,此对象接口提供了如下方法:
Spring Security 默认配置UserDetailsService的实现类为UserDetailsServiceAutoConfiguration。
在上面的代码中我们不难发现,InMemoryUserDetailsManager默认实现条件(@ConditionalOnClass、@ConditionalOnBean、@ConditionalOnMissingBean注解),在inMemoryUserDetailsManager方法中我们可以看到默认初始化的用户信息来自于类SecurityProperties,这个类是Spring Security属性配置类,类定义如下所示:
这个类中内部维护一个用户对象,用户类定义如下:
在这里我们就很清晰了解了为什么Spring Security内置的用户名是user,以及密码是长长的UUID字符串原因了。由于SecurityProperties是一个属性配置类,所以我们可以在项目配置文件自定义用户属性(覆盖默认的用户信息)。