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. 一个简单的单例模式Demo

    /** * @author :nx014924 * @date :Created in 5/30/2021 1:09 PM * @description: * @modified By: * @ver ...

  2. 混合开发框架Flutter

    Flutter开发简介与其他的混合开发的对比 为什么要使用Flutter? 跨平台技术简介 Hybrid技术简介 QT简介 Flutter简介 为什么要使用Flutter? Flutter有什么优势? ...

  3. spring cloud hystrix的隔离策略和dashboard

    随着服务的拆分,各个服务有着明确的职责,服务之间通过轻量级的协议进行通讯.但有时候我们完成一个功能需要同时调用多个微服务,比如完成订单的创建,那么获取用户信息需要调用用户微服务,获取商品信息需要调用商 ...

  4. 21.10.9 test

    T1 购票方案 \(\color{green}{100}\) 对于每个时间节点维护它作为每种票所能包含的最后一个点时,这种票的起始点位置,由于这个位置是单调的,所以类似双指针维护,\(O(KN)\) ...

  5. 数组中出现次数超过一半的数字 牛客网 剑指Offer

    数组中出现次数超过一半的数字 牛客网 剑指Offer 题目描述 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字.例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}.由于数字 ...

  6. 前端面试手写代码——call、apply、bind

    1 call.apply.bind 用法及对比 1.1 Function.prototype 三者都是Function原型上的方法,所有函数都能调用它们 Function.prototype.call ...

  7. SpirngBoot整合Mybatis Plus多数据源

    导读 有一个这样子的需求,线上正在跑的业务,由于业务发展需要,需重新开发一套新系统,等新系统开发完成后,需要无缝对接切换,当初具体设计见草图. 添加依赖 <!--lombok--> < ...

  8. request/response解决中文乱码!!!

    Request中文乱码问题以及解决方案 补充三个知识点: Get是URL解码方式.默认解码格式是Tomcat编码格式.所以URL解码是UTF-8,覆盖掉了request容器解码格式 Post是实体内容 ...

  9. SimpleNVR流媒体服务系统录像功能解析

    录像的回放与观看是许多人在使用视频监控时必不可少的需求.人不可能每时每刻都观看视频,而录像能对摄像机的视频信息进行存储,方便用户的后期回放查看,因此,SimpleNVR的录像功能应运而生.       ...

  10. leetcode 剪绳子系列

    ### 剪绳子一 利用动态规划 状态转移方程 为啥是这个样子?首先  代表 长度为i的绳子被剪去j,且继续剪(子问题)  表示长度为i的绳子被剪去j,不剪了的乘积 注意初始化: n<2 f=0 ...