简答易懂的springsecurity认证流程!

        在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

        市面上存在比较有名的:Shiro,Spring Security !

        Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架。它是确保基于Spring的应用程序的标准。

        Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

        好了废话少说,下面开始了解 Spring Security认证的基本原理

1.快速入门

1.1初识Spring Security

        首先咱先创建一个springboot的项目

        1.引入Spring Security依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

        2.引入依赖之后呢我们发现访问接口需要认证登录了,它会自动跳转到一个Spring Security自带的登录界面

默认用户名为:user

默认密码在控制台:

        3.退出,在路径后输入logout

        虽然我们只是引入了一个Spring Security相关的依赖,代码并不多,但是Spring Security背后为我们默默的做了很多事情:

  1. 开启Spring Security自动化配置。开启后,Spring Security会自动创建一个名为springSecurityFilterChain的过滤器并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecurityFilterChain实际上代理了Spring Security中的过滤器链)

  2. 创建一个InMemoryUserDetailsManager实例(是UserDetailsService接口的实现类),InMemoryUserDetailsManager负责提供用户数据,默认用户数据是基于内存的用户,用户名为user,密码则是随机生成的UUID字符串

     3.给用户生成一个默认的登录页面

        让咱们看到如下图:

        这三个过滤器是登录认证中非常重要的过滤器

  1. 客户端(浏览器)发起请求去访问接口,这个接口默认是需要认证之后才能访问的。

  2. 这个请求走一遍Spring Security中的过滤器链,在最后的FilterSecurityInterceptor过滤器中拦截下来。因为系统中发现用户没有被认证,请求拦截下来之后会抛AccessDeniedException异常。抛出的AccessDeniedException异常在ExceptionTranslationFilter过滤器中被捕获

  3. ExceptionTranslationFilter过滤器通过调用LoginUrlAuthenticationEntryPoint的commence()方法给客户端返回302(重定向的http状态码),要求客户端重定向到/login页面。

  4. 客户端发送/login请求

  5. /login请求被DefaultLoginPageGeneratinFilter过滤器拦截下来,并在该过滤器中返回登录页面。所以用户访问/hello接口会先看到登录页面。
     

 1.2设置用户名和密码

        设置用户名和密码有三种方式

       1.配置文件设置

       2.配置类设置

        (前面两种就不详细说明了,直接来看第三种)

       3.通过实现UserDetailsService接口, 从数据库进行查询

       要想知道这个方式怎么实现我们先得了解UserDetailsService是个啥

  UserDetailsService 接口是 Spring Security 框架中定义的一个核心接口,用于加载用户特定的详细信息。在 Spring Security 中,验证用户身份和权限通常需要从数据库或其他持久存储中加载用户信息。UserDetailsService 接口的作用就是定义了加载用户信息的方法,它有一个核心方法:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

        loadUserByUsername()方法只有一个username参数,这是用户在认证时传入的用户名,最常见的就是表单中输入的用户名。开发者在这里拿到用户名之后,再去数据库中查询用户,最终返回一个UserDetails实例。在实际开发中,一般开发者需要自定义UserDetailsService的实现。如果开发者没有自定义UserDetailsService的实现,Spring Securit也为UserDetailsService提供了默认实现:

  1. UserDetailsManager:这个是一个扩展接口,新增了添加用户、跟新用户、删除用户、修改密码、判断用户是否存在等等方法。

  2. JdbcDaoImpl:实现了通过spring-jdbc冲数据库中查询用户的方法

  3. JdbcUserDetailsManager:继承自JdbcDaoImpl同时又实现了UserDetailsManager接口。这里有一定的局限性,因为操作数据的sql都是写好的不够灵活。因此,在实际开发中JdbcUserDetailsManager使用的并不多。

  4. InMemoryUserDetailsManager:实现了UserDetailsManager中关于用户的增、删、改、查方法,不过都是基于内存操作,数据并没有持久化。

        当我们使用Spring Security时,如果仅仅引入一个Spring Security的依赖,则默认使用的InMemoryUserDetailsManager。

@Service
public class UserDetailsServiceImpl  implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 //模拟数据库, 这里可以使用Mybatis操作数据库
 User user = new User("test2", passwordEncoder.encode("123"), Collections.EMPTY_LIST);
 return user;
}
}

 2.认证流程

        初步了解了Spring Security的认证方式以及修改用户名和密码之后,我们来更深入的了解一下

认证流程:直接附上一张图。

        UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter的子类,是负责处理用户提交的用户名和密码,进行认证的关键过滤器之一,它通过整合 Spring Security 的认证流程,实现安全的用户登录功能。在AbstractAuthenticationProcessingFilter中定义了一个重要的处理方法。

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    // 检查是否需要进行认证
    if (!this.requiresAuthentication(request, response)) {
        // 如果不需要认证,直接将请求传递给下一个过滤器或目标资源
        chain.doFilter(request, response);
    } else {
        try {
            // 尝试进行认证,获取认证结果
            Authentication authenticationResult = this.attemptAuthentication(request, response);
            // 如果认证结果为 null,结束认证过程
            if (authenticationResult == null) {
                return;
            }

            // 在认证成功之后,处理会话策略
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // 如果配置允许在成功认证之前继续调用链,则继续执行
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            // 处理认证成功后的逻辑
            this.successfulAuthentication(request, response, chain, authenticationResult);
        } catch (InternalAuthenticationServiceException var5) {
            // 如果发生内部认证服务异常,记录错误日志
            this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
            // 处理认证失败情况
            this.unsuccessfulAuthentication(request, response, var5);
        } catch (AuthenticationException var6) {
            // 如果发生认证异常,处理认证失败情况
            this.unsuccessfulAuthentication(request, response, var6);
        }
    }
}

        在判断需要认证之后,会调用attemptAuthentication方法,这个方法在此类中定义为抽象方法,通常由其具体子类实现:UsernamePasswordAuthenticationFilter。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    // 检查是否只接受 POST 方法,如果设置为 postOnly 并且请求不是 POST 方法,则抛出异常
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        // 获取用户名
        String username = this.obtainUsername(request);
        // 如果用户名为 null,则设置为空字符串
        username = username != null ? username : "";
        // 去除用户名两端的空白字符
        username = username.trim();
        
        // 获取密码
        String password = this.obtainPassword(request);
        // 如果密码为 null,则设置为空字符串
        password = password != null ? password : "";
        
        // 创建一个包含用户名和密码的 UsernamePasswordAuthenticationToken 对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        
        // 将请求的详细信息设置到 authRequest 中
        this.setDetails(request, authRequest);
        
        // 调用 AuthenticationManager 的 authenticate 方法进行认证,并返回认证后的 Authentication 对象
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

        在这个方法中,它将post请求里的数据(用户名、密码)封装成一个UsernamePasswordAuthenticationToken 对象,UsernamePasswordAuthenticationToken继承于AbstractAuthenticationToken类,AbstractAuthenticationToken是Authentication的实现类,所以最后将此封装结果交给AuthenticationManager管理。

        AuthenticationManager接口有个实现类:ProviderManager是 Spring Security 中实现认证管理的核心组件,管理多个AuthenticationProvider(通常是DaoAuthenticationProvider),在DaoAuthenticationProvider中进行密码等匹配校验

        DaoAtuthenticationProvider的authentic方法会调用retrieveUser方法。retrieveUser方法使用 UserDetailsService加载用户信息。这是通过调用UserDetailsService的loadUserByUsername方法来实现的,返回一个UserDetails 对象。然后由additionalAuthenticationChecks方法判断密码等认证是否通过

/**
 * 从 UserDetailsService 加载用户信息。
 * 
 * @param username 用户名,用于从 UserDetailsService 中检索用户。
 * @param authentication 当前的身份验证令牌,包含凭据等信息。
 * @return 加载的用户详细信息(UserDetails)。
 * @throws AuthenticationException 如果加载用户详细信息时出现问题,则抛出此异常。
 */
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {

    // 准备处理时间攻击保护。此方法用于减少由于时间差异而可能泄露的认证信息。
    this.prepareTimingAttackProtection();

    try {
        // 使用 UserDetailsService 加载用户的详细信息。
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

        // 如果 UserDetailsService 返回 null,则抛出内部认证服务异常。
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
        } else {
            // 返回加载的用户详细信息。
            return loadedUser;
        }
    } catch (UsernameNotFoundException e) {
        // 如果捕获到 UsernameNotFoundException,进行时间攻击保护,并重新抛出异常。
        this.mitigateAgainstTimingAttack(authentication);
        throw e;
    } catch (InternalAuthenticationServiceException e) {
        // 如果捕获到 InternalAuthenticationServiceException,直接抛出。
        throw e;
    } catch (Exception e) {
        // 捕获所有其他异常,并将它们包装在 InternalAuthenticationServiceException 中抛出。
        throw new InternalAuthenticationServiceException(e.getMessage(), e);
    }
}
/**
 * 执行附加的身份验证检查,验证提供的凭据是否与存储的凭据匹配。
 * 
 * @param userDetails 包含用户信息的对象。
 * @param authentication 当前的身份验证令牌,包含用户提交的凭据。
 * @throws AuthenticationException 如果凭据无效或与存储的凭据不匹配,则抛出此异常。
 */
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) 
        throws AuthenticationException {

    // 检查凭据是否为空。如果为空,记录调试日志并抛出 BadCredentialsException 异常。
    if (authentication.getCredentials() == null) {
        this.logger.debug("Failed to authenticate since no credentials provided");
        throw new BadCredentialsException(
            this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
        // 从身份验证令牌中获取提供的密码,并与用户详细信息中的存储密码进行匹配。
        String presentedPassword = authentication.getCredentials().toString();

        // 使用密码编码器验证提供的密码是否与存储的密码匹配。如果不匹配,记录调试日志并抛出 BadCredentialsException 异常。
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(
                this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

        如果所有的检查都通过了,AuthenticationProvider 会返回一个封装了用户认证信息的 Authentication 对象(通常是 UsernamePasswordAuthenticationToken)。该对象会包含用户的权限信息和认证状态。如果任何一个检查失败(如用户名不存在、密码不匹配),AuthenticationProvider 会抛出一个 AuthenticationException(例如 BadCredentialsException)。这个异常会被 AuthenticationManager 捕获并返回给用户,指示认证失败。

        认证成功后,AuthenticationManager 会将认证结果存储到 SecurityContext 中。SecurityContext 是一个线程局部变量,持有当前用户的认证信息。这些信息可以在应用程序的其他部分通过 SecurityContextHolder 访问。

SecurityContextHolder.getContext().setAuthentication(authentication);

完成认证后,用户会被重定向到原请求的页面或默认页面,基于 Spring Security 的配置。

Thanks !!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值