手把手带你撸一把springsecurity框架源码中的认证流程

提springsecurity之前,不得不说一下另外一个轻量级的安全框架Shiro,在springboot未出世之前,Shiro可谓是颇有统一J2EE的安全领域的趋势。

有关shiro的技术点

1、shiro之权限管理的概念
2、shiro之第一个程序认证
3、shiro之自定义realm
4、shiro认证+授权(使用MD5+salt加密)
5、shiro+springboot 图解分析思路
6、Shiro+springboot+mybatis(md5+salt+散列)认证与授权-01
7、Shiro+springboot+mybatis(md5+salt+散列)认证与授权-02
8、Shiro+springboot+mybatis+EhCache(md5+salt+散列)认证与授权-03



一、springsecurity因springboot而火

1.1.

Spring Security 并非一个新生的事物,它最早不叫 Spring Security ,叫 Acegi Security,叫 Acegi Security 并不是说它和 Spring 就没有关系了,它依然是为 Spring 框架提供安全支持的。事实上,Java 领域的框架,很少有框架能够脱离 Spring 框架独立存在。(spring真的是碉堡了呀)

1.2.

当 Spring Security 还叫 Acegi Security 的时候,流传着这样一句话,“每当有人要使用 Acegi Security,就会有一个精灵死去”,从这你就可以感觉到,其中的配置是多繁琐;

1.3.

之后Acegi Security投入了spring的窝里,(呵,这波操作可以呀),然后你懂的,研发团队的小凸凸们开始精简繁杂的xml配置;虽然比之前简化了很多,但一直没火起来,(这一波spring只能在心里默默的说:你是真的带不动,啥也不是;)

1.4.

直到有一天,springboot这个二愣子突然出现在了封建社会中,彻底颠覆了J2EE的世界,“约定大于配置 ”成为springboot的代名词。一人得道,鸡犬升天,连带着把spring家族的产品都带了一把,springsecurity就是其中之一。

1.5.

当前springboot/springcloud是J2EE中主流的技术栈(springboot不是一个新的框架,他是对spring的扩展,是为了高效开发而生)
spring是对Java代码的封装,springboot可以说又对spring进行了封装,屏蔽了内部的细节,让开发人员专注于业务逻辑;缺点就是封装太深,学习成本高。

推荐两种搭配
1.springboot+springcloud+springsecurity
2.SSM+shiro



二、代码准备工作


2.1.使用初始化向导快速搭建springboot项目
手把手带你撸一把springsecurity框架源码中的认证流程_第1张图片

2.2.编写mapper、dao、文件

UserMapper.xml



<mapper namespace="com.itz.security.mapper.UserMapper">
    <select id="findPasswordByUsername" resultType="Users">
        select * from users where username=#{usernaem}
    select>
mapper>

UserMapper接口

public interface UserMapper {
     
    Users findPasswordByUsername(String username);
}

2.3.service层编写

至于为什么要实现UserDetailService这个接口,后面会详细说

package com.itz.security.service;

import com.itz.security.entity.Users;
import com.itz.security.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * @author:抱着鱼睡觉的喵喵
 * @date:2021/3/24
 * @description:
 */
@Service(value = "userDetailService")
@Slf4j
public class MyUserDetailService implements UserDetailsService {
     

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
     
        log.info("用户名:"+username);
        Users user = userMapper.findPasswordByUsername(username);
        if (user == null) {
     
            throw new UsernameNotFoundException("没有该账户!");
        }
        String password = passwordEncoder.encode(user.getPassword());
        log.info("加密后的密码为:"+password);
        return new User(user.getUsername(),password, AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole()));
    }
}

2.4.实体类编写

只是简单的模拟,就没必要太复杂。
注意:不要使用User这个名字,security中有这个类,防止冲突。

@Data
public class Users {
     
    private Integer id;
    private String username;
    private String password;
    private String role;
}

2.5.controller层编写

@RestController
public class HelloController {
     

    @GetMapping("/test")
    public String hello() {
     
        return "HELLO";
    }
}

2.6.login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="login.html" method="post">
        username: <input type="text" name="username"> <br>
        password: <input type="password" name="password"> <br>
                  <input type="submit" value="提交">
    </form>
</body>
</html>

2.7.编写SecurityConfig文件

至于为什么是这样,后面源码分析时会说

package com.itz.security.controller;

import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author:抱着鱼睡觉的喵喵
 * @date:2021/3/24
 * @description:
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     

    @Autowired
    private UserDetailsService userDetailsService;
    @Bean
    public PasswordEncoder getPasswordEncoder() {
     
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     

        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        http.formLogin().loginPage("/login.html")
        	
                .and()
                .csrf().disable();//关闭csrf(一种web攻击手段)
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
     
        web.ignoring().antMatchers("/js/**","/css/**","/images/**");
    }
}

2.8.application.yml编写

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: hao20001010
    username: root
    url: jdbc:mysql://localhost:3306/crud?serverTimezone=UTC

mybatis:
  mapper-locations: /mapper/**
  type-aliases-package: com.itz.security.entity

2.9.数据库表设计
手把手带你撸一把springsecurity框架源码中的认证流程_第2张图片

注意扫描mapper接口
手把手带你撸一把springsecurity框架源码中的认证流程_第3张图片



三、源码认证流程分析


首先会使用debug调试,最基本的F7和F8以及F9,当然鼠标点击也一样
F7:调试的时候遇到方法体会进入到方法体内部执行
F8:遇到方法体不会进入到方法体内部,只会依次执行
F9:只会执行打断点的地方

3.1. ctrl+N 查看UsernamePasswordAuthenticationFilter类的源码,在attemptAuthentication方法上打上断点,bebug模式下启动
手把手带你撸一把springsecurity框架源码中的认证流程_第4张图片
手把手带你撸一把springsecurity框架源码中的认证流程_第5张图片
当走到82行,这个令牌类UsernamePasswordAuthenticationToken
手把手带你撸一把springsecurity框架源码中的认证流程_第6张图片
点击F7 debug进入该类中查看执行情况
手把手带你撸一把springsecurity框架源码中的认证流程_第7张图片

你可以把这个令牌类UsernamePasswordAuthenticationToken当作一个实体类,用来将前端传来的变量赋值给本地变量;简单了说就是将其封装

继续F8之后,就会返回到UsernamePasswordAuthenticationFilter类中
手把手带你撸一把springsecurity框架源码中的认证流程_第8张图片

点击F7,进入查看setDetails方法的源码
手把手带你撸一把springsecurity框架源码中的认证流程_第9张图片
击F7查看buildDetails方法
手把手带你撸一把springsecurity框架源码中的认证流程_第10张图片
击F7查看WebAuthenticationDetails类的具体细节
手把手带你撸一把springsecurity框架源码中的认证流程_第11张图片

原来这个setDetails方法主要是为了将请求中的额外信息保存起来。


下面要进入AbstractUserDetailsAuthenticationProvider类了

手把手带你撸一把springsecurity框架源码中的认证流程_第12张图片
点击F7查看authenticate方法的具体实现细节
手把手带你撸一把springsecurity框架源码中的认证流程_第13张图片

手把手带你撸一把springsecurity框架源码中的认证流程_第14张图片

通过AuthenticationProvicer接口的实现类获取用户的登录方式,然后通过for循环,查看是否支持该登录方式;(一般的登录方式有vx,qq,表单等)

如果不支持支持该登录方式你会发现
手把手带你撸一把springsecurity框架源码中的认证流程_第15张图片

parentResult = this.parent.authenticate(authentication);

从表面意思我们也可以猜到,调用父级提供Provider,重新执行该authenticate方法,看是否支持该登录方式


当支持该登录方式之后,
手把手带你撸一把springsecurity框架源码中的认证流程_第16张图片
然后父类会调用authenticate对用户的身份进行认证(也就是那个支持登录方式的父类)


F7查看具体的认证细节

手把手带你撸一把springsecurity框架源码中的认证流程_第17张图片

手把手带你撸一把springsecurity框架源码中的认证流程_第18张图片


重点来了

手把手带你撸一把springsecurity框架源码中的认证流程_第19张图片

user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

retrieveUser是AbstractUserDetailsAuthenticationProvider抽象类的继承类DaoAuthenticationProvider类中的方法

作用是从数据库或者缓存中获取用户信息

F7查看源码
手把手带你撸一把springsecurity框架源码中的认证流程_第20张图片
手把手带你撸一把springsecurity框架源码中的认证流程_第21张图片

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

可以发现它加载了我自定义的MyUserDetailService类,为什么呢?当然是因为我实现了UserDetailService接口,接下来的debug到哪个类你也因该明白了(多态)

没错就是我自定义的MyUserDetailService类
手把手带你撸一把springsecurity框架源码中的认证流程_第22张图片
还是自己写的代码香哈

在39行F7进入查看源码,然后一直F8返回到上一级的调用处


手把手带你撸一把springsecurity框架源码中的认证流程_第23张图片

return loadedUser执行之后就会返回到AbstractUserDetailsAuthenticationProvider类中的authenticate方法中手把手带你撸一把springsecurity框架源码中的认证流程_第24张图片

this.preAuthenticationChecks.check(user);

这个方法主要是对用户状态进行检测,看是否可用,过期,锁定等

下面是它的方法
手把手带你撸一把springsecurity框架源码中的认证流程_第25张图片
手把手带你撸一把springsecurity框架源码中的认证流程_第26张图片
手把手带你撸一把springsecurity框架源码中的认证流程_第27张图片
matches方法具体源码如下,其中BCrypt.checkpw中的方法不再展示,主要是干嘛的,下面已说明
手把手带你撸一把springsecurity框架源码中的认证流程_第28张图片
手把手带你撸一把springsecurity框架源码中的认证流程_第29张图片
手把手带你撸一把springsecurity框架源码中的认证流程_第30张图片
放行
手把手带你撸一把springsecurity框架源码中的认证流程_第31张图片



自此认证流程就分析结束了

总结认证流程

1.首先进入到UsernamePasswordAuthenticationFilter类中的attemptAuthentication方法

1.1.将用户登录信息封装到UsernamePasswordToken令牌中
1.2.setDetails将请求中额外的信息封装到WebAuthenticaitonDetails
1.3.调用AuthenticationManager接口的实现类ProviderManager中的authenticate方法

2.在ProviderManager类中的authenticate进行执行

2.1.匹配支持的登录方式
2.2.匹配成功后,进入到AuthenticationProvider接口的实现类AbstractUserDetailsAuthenticationProvider中的authenticate方法
2.3.在authenticate方法中,获取用户认证信息,然后进行校验是否过期,最后进行密码的匹配

简单了说就上面两点,但是细节还是有很多的

希望有服务端大佬指点迷津

你可能感兴趣的:(SpringSecurity,spring,java,springsecurity)