SpringSecurity表单认证(一)
SpringSecurity表单认证(一)
新建一个SpringSecutiry项目
创建一个新项目,名称任意
pom文件引入依赖,版本任意。
1 | <dependencies> |
简略设置一下,启动文件
1 |
|
此时访问任意路径都会被拦截到 /login 路径,呈现以下登录页面
到此为止,就已经新建了一个SpringSecutiry最基本的demo
思考
springsecutity作为重量级的安全框架(相对于shiro),搭建起来却并没有很繁琐,其实这归功于springboot自动配置帮我们节省了繁琐的配置,springsecurity提供了很多强大的功能,但最核心的当属两方面
- 认证 Authentication
- 授权 Authorization
通俗来说,认证就是你是谁,授权就是你可以作什么
简述认证
springsecutiry支持多种不同的认证方式
- 表单认证
- OAuth2.0认证
- SAML2.0认证
- CAS2.0认证
- RememberMe 自动认证
- JAAS认证
- OpenID 去中心化认证
- Pre-Authentication Scenarios 认证
- X509 认证
- HTTP Basic认证
- HTTP Digest认证
认证接口
认证信息主要的操作功能由Authentication 定义
1 | public interface Authentication extends Principal, Serializable { |
- getAuthorities 获取用户权限
- getCredentials 获取用户凭证,一般来说就是密码
- getDetails 获取用户携带的详细信息 请求路径等
- getPrincipal 获取当前用户
- isAuthenticated 是否认证成功
大致看一下Authentication 的子类实现,可以看到不同的认证方式会有不同的实现类,这里先不展开说明,大致有个印象
认证信息由Authentication和其子类提供,而认证工作则是由AuthenticationManager接口极其子类实现
AuthenticationManager接口只提供了一个认证方法。
1 | public interface AuthenticationManager { |
不同的子类也代表了不同的认证方式,只需要有个印象就够了,后面碰到会详细说明
过滤器链
在Spring Security中,认证和授权等功能都是基于过滤器完成,Spring Securiy常见的过滤器如下
过滤器 | 过滤器作用 | 预加载 |
---|---|---|
ChannelProcessingFilter | 过滤请求协议,HTTPS/HTTP | NO |
WebAsyncManagerIntegrationFilter | 将WebAsyncManager 与Spring Security 上下文进行集成 | YES |
SecurityContextPersistenceFilter | 在处理请求前,将安全信息加载到SecurityContextHolder中方便后续使用,请求结束后,在擦除掉信息 | YES |
HeaderWriterFilter | 头信息加入到相应中 | YES |
CorsFilter | 处理跨域问题 | NO |
Csffilter | 处理CSRF攻击 | YES |
LogoutFilter | 处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter | 处理OAuth2认证重定向 | NO |
Saml2WebSsoAuthenticationRequestFilter | 处理SAML认证 | NO |
X509AuthenticationFilter | 处理X509认证 | NO |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | NO |
CasAuthenticationFilter | 处理CAS单点登录 | NO |
OAuth2LoginAuthenticationFilter | 处理OAuth2 | NO |
Saml2WebSsoAuthenticationFilter | 处理SAML认证 | NO |
UsenamePasswordAuthenticationFilter | 处理表单登录 | YES |
OpenIDAuthenticationFilter | 处理OpenID认证 | NO |
DefaultLoginPageGenemtingFilter | 配置默认登录页面 | YES |
DefaultLogoutPageGeneratingFilter | 配置默认注销页面 | YES |
ConcurentSessionFilter | 处理Session有效期 | NO |
DigestAuthenticationFilter | 处理HTTP摘要认证 | NO |
BearerTokenAuthenticationFilter | 处理OAuth2认证时的Access Token | NO |
BasicAuthenticationFilter | 处理HttpBasic登录 | YES |
RequestCacheAwareFilter | 处理请求缓存 | YES |
SecurityContextHolderAwareRequestFilter | 包装原始请求 | YES |
JaasApiIntegrationFilter | 处理JAAS认证 | NO |
RememberMeAuthenticationFilter | 处理RememberMe登录 | NO |
AnonymousAuthenticationFilter | 配置匿名认证 | YES |
OAuth2AuthorizationCodeGrantFilter | 处理OAuth2认证中的授权码 | NO |
SessionManagementFilter | 处理Session并发问题 | YES |
ExceptionTranslationFilter | 池异常认证/授权中的情况 | YES |
FilterSecurityInterceptor | 处理授权 | YES |
SwitchUserFilter | 处理账户切换 | NO |
Spring Security提供的功能基本都是由上述过滤器来实现的,这些过滤器按照既定的优先级排列形成过滤器链。当然开发者也可以自定义过滤器的位置,这里暂不讨论。
Spring Security登录认证
上图中新建了一个最基本的Spring Security demo ,在访问/hello的时候,会自动重定向到/login页面,并且需要输入账号密码登录
而在启动时,发现终端上打印了一行
1 | Using generated security password: 535d2c4d-44eb-4808-8ae1-f272b9317b29 |
也就是Spring Security 随机生成的密码(uuid) 默认用户名为user。
流程分析
上述流程以图标的方式展现如下
- 客户端(浏览器)发起请求
/hello
接口,由于引入了Spring Security
,登录需要认证 - 请求会走一遍
Spring Security
中的过滤链,在最后的FilterSecurityInterceptor
过滤其中被拦截下来,因为系统发现用户未认证。请求拦截下拉后会抛出AccessDeniedException
异常 - 抛出的
AccessDeniedException
异常 在ExceptionTranslationFilter
过滤器中被捕获,ExceptionTranslationFilter
过滤器通过调用LoginUrlAuthenticationEntryPoint
中commmence
方法返回客户端302,要求客户端重定向到/login页面 - 客户端重新发送 /login请求
- /login 请求被
DefaultLoginPageGeneratingFilter
过滤器拦截下来,并在该过滤器中返回登录页面。
代码分析
FilterSecurityInterceptor
当过滤链执行到FilterSecurityInterceptor
时,会调用doFilter
方法如下,可以看到主要调用了invoke
方法,FilterInvocation
分装了当前请求信息。
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { |
在invoke()
方法中,它会调用beforeInvocation()
方法,该方法是AbstractSecurityInterceptor
的一个抽象方法,具体的实现由子类提供。beforeInvocation()
方法会调用AccessDecisionManager
来进行访问决策。
1 | public void invoke(FilterInvocation fi) throws IOException, ServletException { |
在beforeInvocation
方法中并不会直接抛出异常。相反,它会委托给AccessDecisionManager
来进行访问决策。如果访问决策失败,AccessDecisionManager
将抛出AccessDeniedException
异常,表示访问被拒绝。主要语句是
1 | try { |
这里,FilterSecurityInterceptor
会检查用户的认证状态和权限,然后调用AccessDecisionManager
的decide
方法来进行访问决策。
这里默认的决策器是AffirmativeBased
,相关代码为
1 | while(var5.hasNext()) { |
AffirmativeBased
: 如果任何一个访问决策器返回ACCESS_GRANTED
(访问允许),则决策通过;否则,如果所有访问决策器都返回ACCESS_ABSTAIN
(弃权)或没有决策器,那么访问也会被允许。
AffirmativeBased
默认传入的构造器只有一个 WebExpressionVoter
,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果,这里返回 -1,deny > 0 直接抛出异常。
ExceptionTranslationFilter
ExceptionTranslationFilter
捕获FilterSecurityInterceptor
抛出的AccessDeniedException
异常,通过调用 LoginUrlAuthenticationEntryPoint
中 commmence
方法返回客户端302,要求客户端重定向到/login页面。
这里走的是catch(Excception var10)
1 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { |
handleSpringSecurityException
方法主要是禁止访问和重定向
1 | private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { |
显而易见这里走的是 this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
而sendStartAuthentication
实际上调用的就是LoginUrlAuthenticationEntryPoint
的commence
方法
1 | protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { |
commence
真正实现了由Spring Security
提供的默认/login
路径的重定向
1 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { |
DefaultLoginPageGeneratingFilter
经过上述重定向操作,客户端重新发起请求/hello,这个请求会在DefaultLoginPageGeneratingFilter
被拦截下拉,并返回默认的登录页面
DefaultLoginPageGeneratingFilter
的操作要简单些,当匹配到登录操作的时候(else),会发送默认生成的页面,也就是我们看到的demo 登录页。
1 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { |
小结
至此,解释了从输入/hello
到 重定向到 /login
代码的原理,由于它的实现过程非常复杂,这里也只是定位到本次请求经过的代码链路操作。
除此之外,还有一个问题,就是生成了默认的登录页,那在哪里生成的默认账号和密码呢?后续将继续介绍
参考资料
《深入浅出Spring Security》王松