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 认证方式。
这样实现起码会有两个点需要调整
- 调整默认认证成功的处理方式
默认配置下,认证成功会跳转到一个指定的页面;现在大多会把这个跳转交给前端。 - 改为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);
@Override
protected 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();
}
}
@Override
protected boolean shouldNotFilterErrorDispatch() {
return true;
}
}