• 01_SpringSecurity学习之配置HttpSecurity


    前言

      🙃😁虽然 Spring Security 不是我发明的,相关的配置方法也是整理自网络,但至少整理的工作是我做的,文章是我自己写的,所以也就算是原创吧😂

      本人菜鸟一枚,这篇文章算是我学习 Spring Security 的记录吧,文中的代码都是自己运行过的,所以放心食用🍔

    1 环境

    • Intellij IDEA 2022.2
    • Spring Boot 2.7.2
      • Spring Security 5.7.2

    2 配置

    2.1 空

    @EnableWebSecurity
    public class SecurityConfig {
    }
    
    • 1
    • 2
    • 3

      因为 WebSecurityConfigurerAdapter 被标记了 @Deprecated ,所以,没有通过继承它来配置 Spring Security。

      启动项目,访问接口时,发现需要认证。Spring Security 提供了一个登录页面,用户名是 user,密码在IDEA的控制台里。

    2.2 极简

    import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
    import org.springframework.security.web.SecurityFilterChain;
    
    import static org.springframework.security.config.Customizer.withDefaults;
    
    @EnableWebSecurity
    public class SecurityConfig {
        /**
         * HttpSecurity 相关的设置
         */
        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                    // 如果没有下面的语句, 那么任何请求都可以免认证
                    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    // 使用默认的表单登录
                    .formLogin(withDefaults())
                    // 使用默认的 http basic 登录
                    .httpBasic(withDefaults());
            return http.build();
        }
    
        /**
         * 配置要忽略的路径
         */
        @Bean
        WebSecurityCustomizer webSecurityCustomizer() {
            // 忽略 /error 页面
            return web -> web.ignoring().antMatchers("/error")
                    // 忽略常见的静态资源路径
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

      2.2 和 2.1 的运行效果相同。究其原因是 Spring Security 默认就是 2.2 这样的配置,如下代码所示:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    class SpringBootWebSecurityConfiguration {
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnDefaultWebSecurity
        static class SecurityFilterChainConfiguration {
            @Bean
            @Order(SecurityProperties.BASIC_AUTH_ORDER)
            SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
                http.authorizeRequests().anyRequest().authenticated();
                http.formLogin();
                http.httpBasic();
                return http.build();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.3 HttpSecurity

    2.3.1 简单尝试

     2.3.1.1 启用默认 formLogin & 启用默认 httpBasic

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .formLogin(withDefaults())
                .httpBasic(withDefaults());
        return http.build();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

      此时有2种登录方式——表单登录和httpBasic登录,如下图:

    1. 表单登录:Spring Security 提供了默认登录页面
      表单登录_202207300933

    2. httpBasic 登录:使用 IDEA 自带的 http client 测试

    ### hello 接口测试
    GET http://localhost:8080/hello/say
    Authorization: Basic user 079c38e0-86b3-4e2f-ac65-8429f09a1bff
    
    • 1
    • 2
    • 3

     2.3.1.2 禁用 formLogin & 启用默认 httpBasic

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .formLogin(form -> form.disable())
                .httpBasic(withDefaults());
        return http.build();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

      此时,浏览器地址栏访问 http://localhost:8080/hello/say 不会重定向到 /login 表单登录页面,而是会弹出一个框,让输入用户名和密码。如下图:

    弹出框_202207300943

      点击登录按钮,由F12可知,实际走的是 httpBasic 认证方式,如下图:

    弹出框_202207300946

    2.3.2 formLogin

      Spring Security 的配置:

    @EnableWebSecurity
    public class SecurityConfig {
        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                    // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
                    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
                    .formLogin(form -> form.loginPage("/login").permitAll());
            return http.build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

      配置一个视图控制器: (点击查看 index.html)

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/login").setViewName("login");
            // 还有一个首页的视图, 相关的 html 详见文章末尾
            registry.addViewController("/").setViewName("index");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

      引入 thymeleaf 依赖:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-thymeleafartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

      把 Spring Security 官网示例1src/main/resources/templates/login.html 复制粘贴到自己的项目中,如下:

    DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    
    <head>
        <title>Please Log Intitle>
    head>
    
    <body>
        <h1>Please Log Inh1>
        <div th:if="${param.error}">
            Invalid username and password.div>
        <div th:if="${param.logout}">
            You have been logged out.div>
        <form th:action="@{/login}" method="post">
            <div>
                <input type="text" name="username" placeholder="Username" />
            div>
            <div>
                <input type="password" name="password" placeholder="Password" />
            div>
            <div>
                
                <input type="checkbox" name="remember-me" />Remember Me
            div>
            <input type="submit" value="Log in" />
        form>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

      然后运行效果如下图(图示的 login.html 没有 remember me):
    FORM运行效果图
      由 F12 可知,/login 登录请求,除了传用户名和密码,还传了 csrf token ,如下图:
    FORM运行效果图_登录后

    2.3.3 Remember Me

      Spring Security 的配置:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
                .formLogin(form -> form.loginPage("/login").permitAll())
                // 退出登录的配置
                .logout(logout -> logout.logoutUrl("/my-logout"))
                // 记住我的设置, 注意前端 login.html 把 remember me 的标签加上
                .rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7*24*3600))
        ;
        return http.build();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

      请求的时候,参数里面会有 remember-me
    REMEMBER_ME
      响应头里面会让浏览器设置 remember me 相关的 Cookie
    REMEMBER_ME_2

    2.3.4 登录成功或失败的 Handler

      Spring Security 的配置:

    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.val;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    @EnableWebSecurity
    public class SecurityConfig {
        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                    // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
                    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
                    .formLogin(form -> form.loginPage("/login").permitAll()
                            .successHandler(jsonAuthenticationSuccessHandler())
                            .failureHandler(jsonAuthenticationFailureHandler()))
                    // 退出登录的配置
                    .logout(logout -> logout.logoutUrl("/my-logout"))
                    // 记住我的设置
                    .rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7 * 24 * 3600))
            ;
            return http.build();
        }
    
        /**
         * 认证失败的处理器
         *
         * @return 函数
         */
        private static AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
            return (request, response, exception) -> {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                val objectMapper = new ObjectMapper();
                val data = Map.of("title", "登录失败", "status", "error");
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.getWriter().println(objectMapper.writeValueAsString(data));
            };
        }
    
        /**
         * 认证成功的处理器
         *
         * @return 函数
         */
        private static AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
            return (request, response, authentication) -> {
                response.setStatus(HttpStatus.OK.value());
                val objectMapper = new ObjectMapper();
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.getWriter().println(objectMapper.writeValueAsString(authentication));
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

      登录成功如下图:

    登录成功1

    登录成功2

      登录失败如下图:

    登录失败1

    2.3.5 退出登录成功的 Handler

      Spring Security 的配置:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
                .formLogin(form -> form.loginPage("/login").permitAll()
                        .successHandler(jsonAuthenticationSuccessHandler())
                        .failureHandler(jsonAuthenticationFailureHandler()))
                // 退出登录的配置
                .logout(logout -> logout.logoutUrl("/my-logout")
                        .logoutSuccessHandler(jsonLogoutSuccessHandler()))
                // 记住我的设置
                .rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7 * 24 * 3600))
        ;
        return http.build();
    }
    
    /**
    * 退出登录成功时的处理器
    *
    * @return 函数
    */
    private static LogoutSuccessHandler jsonLogoutSuccessHandler() {
       return (request, response, authentication) -> {
           response.setStatus(HttpStatus.OK.value());
           val objectMapper = new ObjectMapper();
           val data = Map.of("title", "退出登录成功", "status", "success");
           response.setContentType(MediaType.APPLICATION_JSON_VALUE);
           response.setCharacterEncoding(StandardCharsets.UTF_8.name());
           response.getWriter().println(objectMapper.writeValueAsString(data));
       };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

      如下图:
    退出登录成功

    2.3.6 自定义 Filter

      Spring Security 的配置:

    // debug = true 可以看到更多的 spring security 的日志
    @EnableWebSecurity(debug = true)
    @RequiredArgsConstructor
    @Slf4j
    public class SecurityConfig {
        private final ObjectMapper objectMapper;
        private final AuthenticationHandler handler;
        private final ObjectPostProcessor<Object> objectPostProcessor;
    
        /**
         * HttpSecurity 相关的设置
         */
        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    // 用自定义的 RestAuthenticationFilter 替换 UsernamePasswordAuthenticationFilter
                    .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                    // 用户是通过 json 请求登录的, 是无状态的, 可以把 csrf 禁用
                    .csrf(AbstractHttpConfigurer::disable);
            return http.build();
        }
    
        /**
         * 自定义的认证过滤器
         */
        private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
            RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
            // 设置认证成功的处理器
            filter.setAuthenticationSuccessHandler(handler.jsonAuthenticationSuccessHandler());
            // 设置认证失败的处理器
            filter.setAuthenticationFailureHandler(handler.jsonAuthenticationFailureHandler());
            // 设置认证管理器, 如果不设置, 会报错, 说缺少 authenticationManager
            filter.setAuthenticationManager(authenticationManager());
            // 设置自定义过滤器要针对的 URL 路径
            filter.setFilterProcessesUrl("/rest/login");
            return filter;
        }
    
        /**
         * 构造一个认证管理器
         */
        @Bean
        AuthenticationManager authenticationManager() throws Exception {
            // 这里打日志 验证 authenticationManager 是否只初始化了一次, 因为上面有调用 authenticationManager()
            log.info("初始化 authenticationManager");
            // objectPostProcessor 是可以直接使用 spring context 的对象, 这个是参考了已废弃的 WebSecurityConfigurerAdapter 得知的
            AuthenticationManagerBuilder auth = new AuthenticationManagerBuilder(objectPostProcessor);
            // 创建一些内存中的用户, 用作测试
            auth.inMemoryAuthentication()
                    .withUser("user")
                    .password("{bcrypt}" + passwordEncoder().encode("password"))
                    .roles("USER")
                    .and()
                    .withUser("admin")
                    .password("{bcrypt}" + passwordEncoder().encode("password"))
                    .roles("ADMIN", "USER");
            return auth.build();
        }
    
        /**
         * 密码编码器
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            // 也是验证 passwordEncoder 是否只初始化了一次
            log.info("初始化 passwordEncoder");
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 配置要忽略的路径
         */
        @Bean
        WebSecurityCustomizer webSecurityCustomizer() {
            // 忽略 /error 页面
            return web -> web.ignoring().antMatchers("/error")
                    // 忽略常见的静态资源路径
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

      自定义的过滤器类:

    @RequiredArgsConstructor
    public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        private final ObjectMapper objectMapper;
    
        private static final String POST = "POST";
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 只处理 POST 请求
            if (!POST.equalsIgnoreCase(request.getMethod())) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            String username;
            String password;
            try {
                // 从请求体中获取用户名和密码
                ServletInputStream inputStream = request.getInputStream();
                JsonNode jsonNode = objectMapper.readTree(inputStream);
                username = jsonNode.get("username").textValue();
                password = jsonNode.get("password").textValue();
            } catch (IOException e) {
                throw new BadCredentialsException("用户名或密码错误");
            }
            // 参照 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法来组装 token, setDetails(), 进行 authenticat()
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                    password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

      自定义的认证成功失败的处理器类:

    @Component
    @RequiredArgsConstructor
    public class AuthenticationHandler {
        private final ObjectMapper objectMapper;
    
        /**
         * 退出登录成功时的处理器
         *
         * @return 函数
         */
        public LogoutSuccessHandler jsonLogoutSuccessHandler() {
            return (request, response, authentication) -> {
                response.setStatus(HttpStatus.OK.value());
                val data = Map.of("title", "退出登录成功", "status", "success");
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.getWriter().println(objectMapper.writeValueAsString(data));
            };
        }
    
        /**
         * 认证失败的处理器
         *
         * @return 函数
         */
        public AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
            return (request, response, exception) -> {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                val data = Map.of("title", "登录失败", "status", "error");
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.getWriter().println(objectMapper.writeValueAsString(data));
            };
        }
    
        /**
         * 认证成功的处理器
         *
         * @return 函数
         */
        public AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
            return (request, response, authentication) -> {
                response.setStatus(HttpStatus.OK.value());
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.getWriter().println(objectMapper.writeValueAsString(authentication));
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

      在 IDEA 的 http client 中验证:

    ### login rest 登录测试
    POST http://localhost:8080/rest/login
    Content-Type: application/json
    
    {
      "username": "user",
      "password": "password"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    附录

    1. index.html

    DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    
    <head>
        <meta charset="UTF-8">
        <title>首页title>
    head>
    
    <body>
        <h1>欢迎~h1>
        <div>
            <form th:action="@{/my-logout}" method="post">
                <input type="submit" value="退出登录" />
            form>
        div>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2. 注脚


    1. Spring Security 官网示例连接 https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html ↩︎

  • 相关阅读:
    【Vue全家桶】新一代的状态管理--Pinia
    支持注册API类型
    解决计算机视觉模型中的种族和性别偏见问题,Meta开源 FACET工具
    java毕业设计景区管理系统mybatis+源码+调试部署+系统+数据库+lw
    【3D 图像分割】基于 Pytorch 的 VNet 3D 图像分割1(综述篇)
    118.184.158.111德迅云安全浅谈如何避免网络钓鱼攻击
    Spring5完整版详解
    Jmeter的应用
    Ae 效果:CC Overbrights
    Python的输入输出(来自菜鸟教程)
  • 原文地址:https://blog.csdn.net/ShiJunzhiCome/article/details/126064450