Spring Security 整体架构

    • 前提
    • 整体架构
      • Servlet 整体的过滤器模型
      • Security 过滤器链
      • 自定义过滤器
    • 实际开发解决方案
      • 一个替代cookie认证的filter
      • 其他组件,后续抽时间再整理整理

前提

开源项目一手文档基本都在github,标准文档基本都在官网。
最好的文档就是官网;
在前些年,Apache下还有个比较流行的权限框架 Shiro,但是随着Spring Security (以下简称 Security)的流行,逐渐边缘化。其实这类框架整体设计都大差不差,都是基于一系列的过滤器或者其他拦截手段实现增强。

整体架构

Servlet 整体的过滤器模型


这一块属于 Servlet 基础,过滤器算是Servlet核心功能之一,需重点掌握。

Security 过滤器链

Security 在 Servlet 过滤器上定义了一个 自己的过滤器(这个过滤器使用了委托者设计模式)

可以看到,Servlet 过滤器链上,不仅有 Security 的过滤器还有 Spring Boot 的一些过滤器,但是这里我们重点关注圈起来的那个过滤器(链)
再对比官方的这个图

就更好理解了。

自定义过滤器

我们自定义的过滤器基本都是在 Security 这个过滤器链上面的,通过编排不同的顺序,实现想要的功能。
在这之前,我们先看看Security默认的过滤器链上有哪些过滤器官方列表
这里为了照顾,访问不了官网的,简单截个图。


但不是所有过滤器默认都开启了的。

上面那个链是,Spring 全局的过滤器链,里面第四个过滤器委托给了下面那个 Security 的过滤器链,在 Security 的过滤器链中,可以看到默认有14个过滤器。
其中有几个重点关注的是:

  • SecurityContextPersistenceFilter (即将过时,替代者为 SecurityContextHolderFilter),其功能为,支持默认的基于 Cookie 的登录形式;对于新接触认证授权的开发者,要明白一点,不管什么实现方案,每一次进行鉴权,其本质会进行一次登录认证,只是方式不同而已
  • LogoutFilter 注销登录的过滤器,默认的url为 /logout,要实现自定义退出可以参考
  • UsernamePasswordAuthenticationFilter 大部分开发者,开始接触 Security 都是模仿这个 过滤器实现的自定义登录
  • AnonymousAuthenticationFilter 在 Security 中,认证没有通过或者没有匹配上认证过滤器的请求,最终会被定义为匿名认证,这也是一种认证结果,在权限中也有相关的配置。

其他的过滤器根据自己的需求,调整。

实际开发解决方案

在实际开发中,目前通常采用前后端分离的形式;有的前端对Cookie 不太友好;因此,为了统一,大部分开发者会采用 header 认证方式。
这样实现起码会有两个点需要调整

  1. 调整默认认证成功的处理方式
    默认配置下,认证成功会跳转到一个指定的页面;现在大多会把这个跳转交给前端。
  2. 改为header 传递认证参数后,默认的过滤器就不支持了,需要定义一个 过滤器,解析header,并进行认证;个人推荐将这个认证过滤器放在
    AnonymousAuthenticationFilter 的前面,因为前面还有些其他认证过滤器,如果自定义的过滤器太靠前,可能会被其他认证过滤器(比如上面提到的 SecurityContextPersistenceFilter 以及 SecurityContextHolderFilter)给覆盖掉认证信息,当然这要取决实际的优先级,万一你就想自定义的认证认证过滤器优先级低一点呢

    可以看到,这两个认证过滤器不管你是否已经认证,会直接覆盖掉认证信息。

一个替代cookie认证的filter

提供一个简单实现,具体业务逻辑,开发者自己填充

package authorization.filter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContext;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.util.StringUtils;import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.Collections;public class JwtAuthenticationFilter extends OncePerRequestFilter {private final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {String token = request.getHeader("token");if (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);return;}try {UsernamePasswordAuthenticationToken authenticationToken= UsernamePasswordAuthenticationToken.authenticated("zhangsan", "zs", Collections.emptyList());SecurityContext context = SecurityContextHolder.createEmptyContext();context.setAuthentication(authenticationToken);SecurityContextHolder.setContext(context);filterChain.doFilter(request, response);}finally {SecurityContextHolder.clearContext();}}@Overrideprotected boolean shouldNotFilterErrorDispatch() {return true;}}

其他组件,后续抽时间再整理整理