spring security 认证源码跟踪

​ 在跟踪认证源码之前,我们先根据官网说明一下security的内部原理,主要是依据一系列的filter来实现,大家可以根据https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#servlet-hello 查看相关的文档说明,英文不好的可以配合使用google翻译。

security 原理说明

​ 在上图中,红色方框圈出来的是security 的filter,每一个http request都会经过上图的每一个指定的过滤器。请求其中:

DelegatingFilterProxy:主要负责在servlet容器的生命周期和Spring上下文进行衔接,也就是说security的所有过滤器都委托给它进行代理。

FilterChainProxy:是一个特殊的过滤器,被包装在DelegatingFilterProxy内部。它代理代理了SecurityFilterChain

SecurityFilterChain:SecurityFilterChain 确定应为此请求调用哪些 Spring 安全过滤器。

DelegatingFilterProxy

​ 这是一个过滤器,所以肯定会有doFilter方法,我们主要查看内部的2个方法,首先从doFilter方法看起:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException { // Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
// 拿到Spring Web上下文
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
// 初始化委托filter
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
} // Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
} // 初始化委托filter
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
// 众多filter中,会有一个是FilterChainProxy
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}

FilterChainProxy

​ 它也是一个过滤器,那一定也会有doFilter方法,我们查看该方法

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 当前request是否已经清除了上下文,因为每一个请求都会经过这个过滤器
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 内部的filter方法,我们看到该方法
doFilterInternal(request, response, chain);
}
catch (RequestRejectedException ex) {
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
} private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 拿到防火墙配置,对于这里不重要
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
// 这里可以看到,FilterChainProxy在这里拿到了这次请求request具体还要经过的一系列过滤器链,其中包括CsrfFilter、UsernamePasswordAuthenticationFilter等过滤器,包含了SecurityFilterChain 涉及的filter
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}

认证源码跟踪

​ 回到认证这里,在网上随便搜一搜就能搜到spring scurity认证的几种方式,这次我们主要跟踪第三种认证方式:数据库认证,也是我们平时在用的方式。先给大家说明一下数据库认证的知识点,有个大概印象:

  1. UsernamePasswordAuthenticationFilter
  2. 实现UserDetailsService接口并注入到spring管理

这三种认证方式分为为:

1、在xml中配置账号密码

spring.security.user.name=user
spring.security.user.password=123456

2、在代码中将账号、密码加载到内存中

@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}

3、从数据库中读取账号进行认证校验

public class MyUserDetailsService implements UserDetailsService {

    @Autowired
private UserMapper userMapper; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库尝试读取该用户
User user = userMapper.findByUserName(username);
// 用户不存在,抛出异常
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 将数据库形式的roles解析为UserDetails的权限集
// AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
//提供的用于将逗号隔开的权限集字符串切割成可用权限对象列表的方法
// 当然也可以自己实现,如用分号来隔开等,参考generateAuthorities
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}

​ 在这个例子中,我们会有一个自定义WebSecurityConfig类,其中定义了哪些Url路径需要拦截,以及需要哪些权限才能够访问,同时在这个配置中,注入一个一个密码编码类,默认是不采用加密方式NoOpPasswordEncoder

@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login-error")
.permitAll();
} @Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}

​ 我们先不去实现UserDetailsService接口,看看spring security是怎么去实现认证的?

UsernamePasswordAuthenticationFilter

​ 首先找到UsernamePasswordAuthenticationFilter类,发现它继承了AbstractAuthenticationProcessingFilter类,那我们就先看一下AbstractAuthenticationProcessingFilter类,发现这个类中主要有四个方法,分别是:

  1. doFilter(reqeust,response,chain):每个filter都会有的方法,最重要的一个。
  2. attemptAuthentication(request,response); 是个抽象方法,交给具体的实现类去实现认证的逻辑
  3. successfulAuthentication(request,response,chain,authenticationResult); 认证成功后的处理逻辑,通过不同的策略实现
  4. unsuccessfulAuthentication(request,resonse,failed);认证失败后的处理逻辑,通过不同的策略实现
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 具体的认证方法,是个抽象方法,交给具体的实现类去实现认证的逻辑
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功后的处理逻辑
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
// 认证失败后的处理逻辑
unsuccessfulAuthentication(request, response, ex);
}
}

​ 接着我们看回UsernamePasswordAuthenticationFilter类,发现它主要是重写了AbstractAuthenticationProcessingFilter类的attemptAuthentication(request,response)认证方法。具体如下:

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 获取请求的用户名
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
// 获取请求输入的密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 构造带有用户名、密码的UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
// 设置认证的对象
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}

接着我们运行程序,直接访问http://localhost:8080/admin/api/hello地址,重定向到登录页后,随意输入账号、密码后,在UsernamePasswordAuthenticationFilter类的attemptAuthentication方法上打断点进行跟踪,跟踪到DaoAuthenticationProvider类的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法,

​ 在AbstractUserDetailsAuthenticationProvider抽象中的需要指定一个实现UserDetailsService 接口的实现类,如果我们没有指定,就是会去加载默认的InMemoryUserDetailManager类。

​ 因为采用的是上面提过的第二种方式:在代码中将账号、密码加载到内存中,然后我们并没有在内存中预先加载我们输入的账号、密码,所以自然是认证不通过的。

UserDetailsService 接口

​ 想要通过自定义的认证方式,也就是上面提到的第三种认证方式:从数据库中读取账号进行认证校验。所以需要自己去实现UserDetailsService 接口。刚才我们在跟踪代码的过程中,发现AbstractUserDetailsAuthenticationProvider类是需要一个实现了UserDetailsService接口的对象,于是我们就自定义一个实现该接口的实现类,并注入到spring容器中。

@Service
public class MyUserDetailsService implements UserDetailsService { @Autowired
private UserMapper userMapper; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库尝试读取该用户
User user = userMapper.findByUserName(username);
// 用户不存在,抛出异常
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 将数据库形式的roles解析为UserDetails的权限集
// AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
//提供的用于将逗号隔开的权限集字符串切割成可用权限对象列表的方法
// 当然也可以自己实现,如用分号来隔开等,参考generateAuthorities
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}

​ 如上图,我们重写了UserDetailsService接口的loadUserByUsername(String username)方法,从而实现我们的自定义认证逻辑。然后我们再重启服务,重新访问http://localhost:8080/admin/api/hello,再次登录,并进行代码跟踪,

这个时候就发现DaoAuthenticationProvider从自己的userDetailsService拿到了我们自定义的对象,接着就会走我们的自定义认证逻辑。

​ 认证源码跟踪就到这里,接下来是授权的源码跟踪,跟踪文章较短,但大家了解一下还是有些收获的。加油!

spring security 认证源码跟踪的更多相关文章

  1. Spring Security 解析(七) —— Spring Security Oauth2 源码解析

    Spring Security 解析(七) -- Spring Security Oauth2 源码解析   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因 ...

  2. Spring Security 访问控制 源码解析

    上篇 Spring Security 登录校验 源码解析  分析了使用Spring Security时用户登录时验证并返回token过程,本篇分析下用户带token访问时,如何验证用户登录状态及权限问 ...

  3. spring security 实践 + 源码分析

    前言 本文将从示例.原理.应用3个方面介绍 spring data jpa. 以下分析基于spring boot 2.0 + spring 5.0.4版本源码 概述 Spring Security 是 ...

  4. Spring Security OAuth2 源码分析

    Spring Security OAuth2 主要两部分功能:1.生成token,2.验证token,最大概的流程进行了一次梳理 1.Server端生成token (post /oauth/token ...

  5. Spring组件扫描--源码跟踪

    看这篇文章之前可以先了解之前的跟踪流程,https://www.jianshu.com/p/4934233f0ead 代码过宽,可以shift + 鼠标滚轮 左右滑动查看 这篇文章主要跟踪spring ...

  6. spring-security-4 (4)spring security 认证和授权原理

    在上一节我们讨论了spring security过滤器的创建和注册原理.请记住springSecurityFilterChain(类型为FilterChainProxy)是实际起作用的过滤器链,Del ...

  7. spring security之 默认登录页源码跟踪

    spring security之 默认登录页源码跟踪 ​ 2021年的最后2个月,立个flag,要把Spring Security和Spring Security OAuth2的应用及主流程源码研究透 ...

  8. spring security 之自定义表单登录源码跟踪

    ​ 上一节我们跟踪了security的默认登录页的源码,可以参考这里:https://www.cnblogs.com/process-h/p/15522267.html 这节我们来看看如何自定义单表认 ...

  9. spring security 授权方式(自定义)及源码跟踪

    spring security 授权方式(自定义)及源码跟踪 ​ 这节我们来看看spring security的几种授权方式,及简要的源码跟踪.在初步接触spring security时,为了实现它的 ...

随机推荐

  1. C# datagridview、datagrid、GridControl增加行号

    01 - WinForm中datagridview增加行号 在界面上拖一个控件dataGridView1,在datagridview添加行事件中添加如下代码: private void dataGri ...

  2. 8086存储器组织和IO组织 奇偶分体

    8086的存储器组织 存储器的基本存储单位是字节,每个字节用唯一的地址码表示. 若存放的信息是8位的字节数据,将按顺序存放: 若存放的信息是16位的字数据,则将字的高位字节放在高地址中,低位字节放在低 ...

  3. Postman实现SHA256withRSA签名

    @ 目录 获取pmlib 引入依赖bundle.js,有以下两种方式: 使用Pre-request Script对请求进行加签(具体加签字段请看自己项目) 获取pmlib 引入依赖bundle.js, ...

  4. Java---String和StringBuffer类

    Java---String和StringBuffer类 Java String 类 字符串在Java中属于对象,Java提供String类来创建和操作字符串. 创建字符串 创建字符串常用的方法如下: ...

  5. Django Model字段加密的优雅实现

    早前的一篇文章Django开发密码管理表实例有写我们写了个密码管理工具来实现对密码的管理,当时加密解密的功能在view层实现,一直运行稳定所以也没有过多关注实现是否优雅的问题.最近要多加几个密码表再次 ...

  6. [CSP-S 2021] 回文

    题目描述: 给定正整数 n 和整数序列 a1, a2,-,a2n,在这 2n 个数中,1, 2,-,n 分别各出现恰好 2 次.现在进行 2n 次操作,目标是创建一个长度同样为 2n 的序列 b 1, ...

  7. WiFi模块选型参考

    经常会碰到一些关于wifi模块的咨询,很多刚接触wifi模块的设计人员或者用户,只知道提wifi模块,很难提具体的模块要求!希望通过文章的介绍,会做到有的放矢!咨询时一定要搞清楚自己希望使用什么主芯片 ...

  8. 转载:使用Xilinx IP核进行PCIE开发学习笔记(一)简介篇

    https://zhuanlan.zhihu.com/p/32786076 最近接触到一个项目,需要使用PCIE协议,项目要求完成一个pcie板卡,最终可以通过电脑进行通信,完成电脑发送的指令.这当中 ...

  9. hdu 1501 Zipper(DP)

    题意: 给三个字符串str1.str2.str3 问str1和str2能否拼接成str3.(拼接的意思可以互相穿插) 能输出YES否则输出NO. 思路: 如果str3是由str1和str2拼接而成,s ...

  10. hdu 5083 Instruction (稍比较复杂的模拟题)

    题意: 二进制指令转汇编指令,汇编指令转二进制指令. 思路: 额,条理分好,想全,思维不能乱. 代码: int findyu(char yu[50],char c){ int l=strlen(yu) ...