IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> SpringSecurity<4> -> 正文阅读

[开发测试]SpringSecurity<4>

对于Springsecuriy 中
呢么我们该如何给我们的web前后端不分离
或者前后端分离加上验证码验证呢~~~~

对于传统web中 
//
我们的验证码
是通过res相应流响应的~  我们先加入 谷歌的 kaptcha 这个 jar 用于 图片的生成
<dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
我们第一步需要 生成图片 存入session, 当用户输入 的时候进行比对~~
网上找一个 KaptchaConfig 进行配置

在这里插入图片描述

  传统web 登录验证码~~~~~~~  我此时加入的是 内存数据源~
  
/*
 * 自定义springsecurity的 相关配置
 *
 * */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /* 自定义数据源*/
    @Bean
    public UserDetailsService userDetailsService() {
        // 返回内存的userDetailService
        InMemoryUserDetailsManager userDetailsService =
                new InMemoryUserDetailsManager();
        userDetailsService.createUser
                (User.withUsername("user")
                        .password("{noop}123").roles("admin").build());
//        /*我们就吧内存的用户信息进行覆盖*/
        return userDetailsService;

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置数据源
        auth.userDetailsService(userDetailsService());//
    }

    /* http 用来去控制http 请求的*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.

                authorizeRequests() // 开启认证   所有的Http请求开启认证
                //  login  页面 就放行
                .mvcMatchers("/login.html").permitAll() // 当时login.html  放行
                /*验证码的请求 必须的放行*/
                .mvcMatchers("/vc.jpg").permitAll() // 当时login.html  放行
                .anyRequest()   // 任何请求都要求认证
                .authenticated() // 所有请求都的认证
                .and()
                .formLogin() // 表单认证
                .loginPage("/login.html")// 指定自定义登录页面
                /*拦截doLogin 请求*/
//    .loginProcessingUrl("/doLogin") // 请求url
//    .usernameParameter("uname")
//    .passwordParameter("passwd") -------->>>>  替换为 KapterFilter  因为我们现在 用的不再是UsernamePasswordAuthenticationFilter    用UsernamePasswordAuthenticationFilter 有默认的实现
//                .defaultSuccessUrl("/index.html", true)/*重定向处理*/
//                .failureUrl("/login.html") /*重定向到我我们的登录页面  吧信息存储在session 作用域中*/
                .and()
                .logout()/*开启退出登录*/
                /**/
                .logoutUrl("/logout")/*路径 get 请求*/
                .logoutSuccessUrl("/login.html")/* 退出之后回到login.html*/
                /* 退出成功的页面*/
                .and()
                .csrf()
                .disable();/* csrf漏洞保护给关闭了*/
//
//                .failureUrl("/login.html")//重定向到登录页面
//                .and()
//                .logout()//开启退出登录
//                .logoutUrl("/logout")
//                .logoutSuccessUrl("/login.html")
//                .and()
//                .csrf().disable();//csrf 关闭

        //  验证码的Filter 替换UsernamePasswordAuthenticationFilter
        // 替换 UsernamePasswordAuthenticationFilter
        // 这样以后在form表单认证的时候 首先进入kapterFilter 所在的过滤器
        // 然后在
        http.addFilterAt(kapterFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /*现在的认证交给KapteFilter来处理了
     *
     * 1. 认证的url
     *
     *
     * */
    @Bean
    public KapterFilter kapterFilter() throws Exception {
        // 自定义Filter  替换掉 UsernamePasswordAuthenticationFilter
        /*
            loginProcessingUrl
            usernameParameter
            passwordParameter
        *
        * */
        KapterFilter kapterFilter = new KapterFilter();
        kapterFilter.setFilterProcessesUrl("/doLogin");
        kapterFilter.setUsernameParameter("uname");
        kapterFilter.setPasswordParameter("passwd");
        // set 方法注入 有的就用有的 没有用默认的
        kapterFilter.setKapttchaParameter("kaptcha");
        /* 指定认证管理器  */
        kapterFilter.setAuthenticationManager( authenticationManagerBean() );
        /*认证成功的处理 -->defaultSuccessUrl 自定义了 覆盖他  */
       /* 我认证通过我还是希望 重定向到   index.html*/
// 认证成功 认证失败只是简单的做一个页面的跳转以及响应        kapterFilter.setAuthenticationSuccessHandler((req,resp,auth)->{
          /* 成功时候的处理*/
            resp.sendRedirect("/index.html");
        });
        /*认证失败的处理---??覆盖他 failureUrl  */
        kapterFilter.setAuthenticationFailureHandler((req,resp,ex)->{
        
            resp.sendRedirect("/login.html");
        });
        return kapterFilter;
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

  我此时 开发一个vc.vc.jpg  的接口 并且通过response响应出去 ~ 再之后加入到springsecurity 的配置中 标记 可以放行通过~
@Controller
public class VerifyCodeController {
    @Autowired
    Producer producer;

@RequestMapping("vc.jpg")
    public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
        // 生成验证码
        String text = producer.createText();
        // 保存到session中
        session.setAttribute("kaptcha", text);
        //3.  生成图片
        BufferedImage b1 = producer.createImage(text);
        // 设置响应图片
        response.setContentType("image/png");
        //4. 响应图片

        ServletOutputStream os = response.getOutputStream();
        ImageIO.write(b1, "jpg", os);
    }
}

/*自定义验证码的Filter*/
public class KapterFilter extends UsernamePasswordAuthenticationFilter {

    public static final String FORM_KAPTCHA_KEY = "kaptcha";

    /*可配置 这个就相当于我们定义一个默认的*/
    public String kapttchaParameter = FORM_KAPTCHA_KEY;

    public String getKapttchaParameter() {
        return kapttchaParameter;
    }

    public void setKapttchaParameter(String kapttchaParameter) {
        this.kapttchaParameter = kapttchaParameter;
    }

    /*  先调用子类 再调用父类*/
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 从请求中获取验证码   如果你没有赋值 拿的就是默认值
        //  如果赋值拿的就不是默认值 通过set 方法赋值了 就是拿的是set方法赋值之后的
        String kaptcha = request.getParameter(getKapttchaParameter());
        // 与session中code 取出来  kaptcha

        String sessionCode = (String) request.getSession().getAttribute("kaptcha");
        if (!ObjectUtils.isEmpty(sessionCode)
                && !ObjectUtils.isEmpty(kaptcha)
                && sessionCode.equals(kaptcha)
        ) {
            //   放行请求去掉父类的认证 调用父类的 完成账号密码登录
            return super.attemptAuthentication(request, response);
        }
        ///
        throw new KaptchaException("验证码不匹配!");

    }
}

在这里插入图片描述

只是在传统web 开发中引入
  验证码:  <input name="kaptcha" type="text"> <img th:src="@{/vc.jpg}" alt=""> <br>
  这个验证码的选择框
--------------------------------------------------------------------------------------------
呢么前后端分离的时候如何加入验证码~校验,同样我们需要引入pom 然后生成验证码响应到前端  但是前后端分离的时候 就不是用流来响应了 ,因为此时是2个系统了, 的要用base64 相映成json 给前端  ,因为前后端分离此时用的是json做数据的交互

@RestController
public class VerifyCodeController {
    @Autowired
    Producer producer;


    @GetMapping("vc.jpg")
    public String verifyCode(HttpSession session) throws IOException {
        // 生成验证码
        String text = producer.createText();
        // 保存到session中
        session.setAttribute("kaptcha", text);
        //3.  生成图片
        BufferedImage b1 = producer.createImage(text);
        // 吧图片转成byte数组  吧图片转成内存中的byte数组
        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
        ImageIO.write(b1, "jpg", fos);
        // 输出到fos 中  吧 b1--->以jpg 输出到fos中  内存中的以字节的方式
        // 4. 返回base64_____>>> 转为base64
        String s = Base64Utils.encodeToString(fos.toByteArray());
        return s;


    }
}

  自定义LoginKapterFilter

/*自定义Filter*/
public class LoginKapterFilter extends UsernamePasswordAuthenticationFilter {

    public static final String FORM_KAPTCHA_KEY = "kaptcha";

    /*可配置 这个就相当于我们定义一个默认的*/
    public String kapttchaParameter = FORM_KAPTCHA_KEY;

    public String getKapttchaParameter() {
        return kapttchaParameter;
    }

    public void setKapttchaParameter(String kapttchaParameter) {
        this.kapttchaParameter = kapttchaParameter;
    }

    /*  先调用子类 再调用父类  覆盖视图认证的方法
     *
     *  如果验证码认证通过之后 再调用我的manager
     *
     * */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        //1. 获取请求验证码
        Map<String, String> userInfo = new HashMap<>();
        try {
            userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 设置一个默认值  get/set方法  可以改变
        String kaptcha = userInfo.get(getKapttchaParameter()); // 获取数据中的验证码
        String username = userInfo.get(getUsernameParameter()); // 获取数据中的账号
        String passwd = userInfo.get(getPasswordParameter()); // 获取数据中的密码

        //2. 获取session中验证码
        String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
        //3.获取用户名和密码认证
        if (!ObjectUtils.isEmpty(sessionVerifyCode)
                && !ObjectUtils.isEmpty(kaptcha)
                && kaptcha.equals(sessionVerifyCode)) {
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, passwd);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        throw new KaptchaException("验证码异常 !!");

    }
}

 1. 放行 /vc.jpg 2. 使用内存型的数据源~ 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                /*放行验证码*/
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                /*配置异常处理*/
                .exceptionHandling()
                /*当出现认证异常时 我们执行认证的 EntryPoint*/
                .authenticationEntryPoint((req, response, ex) -> {
                    /*指定响应编码*/
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    /*未被认证的  当你没有认证的时候返回状态码 未被授权的*/
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    /*打印一句话*/
                    response.getWriter().println("请认证 ~之后再做处理");
                })

                /*怎么认证  formlogin 认证 [默认是不行的  进行替换  我需要自定义filter]*/
                .and()
                .formLogin()
                .and()
                .logout()
                .and()
                .csrf().disable()
        ; // 所有请求开启认证
          让自定义Filter 替换 UsernamePasswordAuthenticationFilter
          让我的http 请求添加 loginKapterFilter 然后替换 UsernamePasswordAuthenticationFilter
        //  一旦替换以后很多特性就没有了 需要指定了
        http.addFilterAt(loginKapterFilter(), UsernamePasswordAuthenticationFilter.class);

    }

    /*authenticationManagerBean */
    /*自定义mananger  但是不能被外部使用  如果我们要暴露出来的化 我们必须要覆盖这个方法*/
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /*配置自定义LoginKapterFilter*/
    @Bean
    public LoginKapterFilter loginKapterFilter() throws Exception {
        LoginKapterFilter loginKapterFilter = new LoginKapterFilter();
        //认证的url
        loginKapterFilter.setFilterProcessesUrl("/doLogin");

        //认证接受的参数
        loginKapterFilter.setUsernameParameter("uname");
        loginKapterFilter.setPasswordParameter("passwd");
        loginKapterFilter.setKapttchaParameter("kaptcha");
        // 注入authenticationManager【】指定认证管理器
        loginKapterFilter.setAuthenticationManager(authenticationManagerBean());
        /// 成功 认证成功的处理  --------->>>> 自定义成功的处理
        loginKapterFilter.setAuthenticationSuccessHandler((req, rep, authentication) -> {
            HashMap<Object, Object> resMap = new HashMap<>();
            resMap.put("msg", "登录成功");
            //  强转成用户对象
            resMap.put("userInfo", authentication.getAuthorities());
            rep.setStatus(HttpStatus.OK.value());
            rep.setContentType("application/json; charset=UTF-8");
            String stringRsult = new ObjectMapper().writeValueAsString(resMap);
            rep.getWriter().println(stringRsult);
        });
        /// 失败的 认证成功的处理  一般我们会改变响应的状态码
        /  自定义失败的处理
        loginKapterFilter.setAuthenticationFailureHandler((req, rep, exception) -> {
            HashMap<Object, Object> resultMap = new HashMap<>();
            /*失败信息*/
            resultMap.put("msg", "登录失败" + exception.getMessage());
            rep.setContentType("application/json; charset=UTF-8");
            // 改变响应码
            rep.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            String stringRsult = new ObjectMapper().writeValueAsString(resultMap);
            rep.getWriter().println(stringRsult);
        });
        return loginKapterFilter;
    }

    /*自定义mananger  但是不能被外部使用  如果我们要暴露出来的化 我们必须要覆盖这个方法*/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    /* UserDetailsService 使用内存数据库*/
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails serDetails = User.withUsername("user")
                .password("{noop}123").roles("admin").build();
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(serDetails);
        return inMemoryUserDetailsManager;

    }
}

// 写一个自定义异常~~~~~~~~~
public class KaptchaException extends AuthenticationException {

    public KaptchaException(String explanation) {
        super(explanation);
    }

    public KaptchaException(String explanation,Throwable cause) {
        super(explanation, cause);
    }
}

-------------------------------------->>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Springsecurity 中的密码加密
	凡是涉及密码的地方,我们都采用明文存储,在实际项目中这肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加``,最大程度地保证密码安全。
	我们一般会吧盐存储在 数据库中 因为加盐的拼接位置不同 也会增加密码的安全

在这里插入图片描述

我们之前只是  做账号的匹配,我们看下源码 看下密码的匹配在哪里做的

在这里插入图片描述

DelegatingPasswordEncoder  
根据前缀选择不同的	PasswordEncoder
再之后不同的	PasswordEncoder  进行不同的处理~比对密码

在这里插入图片描述

默认的用的是他 也就是 noop是明文的比对

在这里插入图片描述

明文比对就会做equals() ..判断

在这里插入图片描述

默认是有这么多加密方式~

在这里插入图片描述

他里面的方法就在这里了

在这里插入图片描述

他在比对密码的时候会用你的 密码的前缀来选择不同的  不同的策略  然后再进行不一样的策略

这样就能保证我一个系统有多种策略方式 ~
允许同一个系统中存储多个不同中密码加密方案
        /*passwordEncoder   指定版本号  盐 长度*/
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(16);
      /* 16次散列 默认是10次*/
        String encode = bCryptPasswordEncoder.encode("123");
        System.out.println(encode);
/  这个就是使用123进行加密的  而且每次加密都不一样  而且运行的时候很慢 因为他会占用系统大量资源

// 

{bycrpt}$2a$16$.SAgkod45suAcQEkyLNtZeMekhZDXXXOE17XHqzFQSDjvxwPHZwNm
我们可以用一下代码生成一个密码~ 然后 我们在set密码的时候前面加一个
{bycrpt}
这样他在密码解密的时候 就会用bycrpt 这种 PasswordEncoder 进行解密了

在这里插入图片描述

我们也可以让整个系统用这么一种密码匹配器 这样的话 前面就不用加前缀了~

在这里插入图片描述

推荐使用DelegatingPasswordEncoder 的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用。

比如说当前数据库密码是明文{noop}  然后springsecurity 这个版本改成最新的了  之后我们需要认证后更新数据库db

在这里插入图片描述

在认证成功以后 ,做完全密码匹配以后  这里如果我们实现了 UserDetailsPasswordService 这个接口 会自动给我们做update 为最新密码的  此时会查询最新的策略 进行密码update

在这里插入图片描述


在这里插入图片描述
在这里插入图片描述

然后我root 密码转成 bcrypt 了

在这里插入图片描述

~~~~~~~>>>>>>>>RememberMe<---------- 

在这里插入图片描述

Remember就像 这里的复选框一样一样~,
我们这里说的Remember是服务端的一种行为  传统登录方式会基于session会话,一旦
用户的session超时过期 就需要再次登录,

在这里插入图片描述

 比如说我此时设置的
 # 默认修改服务端的会话
server.servlet.session.timeout=11 分钟 默认的是,如果客户端不操作的话 默认1分钟之后session 过期就得要重新登录

在这里插入图片描述
在这里插入图片描述

RememberMe是 响应一个JsessoinId的同时还会响应一个用户的加密信息的cookie



在这里插入图片描述

Remember在Springsecurity中开启很容易

在这里插入图片描述

当我们开启RememberMe之后 这里就弹出来一个窗口

在这里插入图片描述

当我们点了以后会回应一个  Remember-me的 cookie  然后我们再过1分钟之后  追一下源码看看
他根据这个cookie 进行的一个[Remember]登录

在这里插入图片描述

 我们看看RememberMeAuthenticationFilter  中的 doFilter 是如何做处理的请求

在这里插入图片描述

如果
SecurityContextHolder.getContext().getAuthentication() 没有值
代表session过期了~ 

	Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response); 
	我们需要重新去认证

在这里插入图片描述

当他重新认证的时候,他会解析请求中的cookie是否有 remember-me

之后将 remember-me 的值进行一个Base64解码,然后再按照: 切割成数组
// 各种校验之后 就可以通过.loadUserByUsername 再进行加载用户了
UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);

		String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
		根据这些封装一个签名 进行md5解密~ 如果解密成功就正常响应了

在这里插入图片描述

successfulAuthentication(request, response, chain, authenticationResult);

在这里插入图片描述

在这里认证成功后 如果我们勾选了~ remember的话  就会进行在resp中写remember
这样 的话他在session过期之后就会根据这个remember-me的cookie进行解密了~
 
但是有个问题就是 安全性问题 如果别人拿着这个remember-me的cookie 貌似也可以登录  因为此时他的cookie是不变的
所以我们可以 
 

在这里插入图片描述

onLoginSuccess  生成token 回写response
processAutoLoginCookie  处理token
我们刚刚使用的 TokenBasedRememberMeServices  来做这些的 这个是最简单的所以叫做tokenBase 


我们可以使用 PersistentTokenBasedRememberMeServices 
这个每次 验证完token的时候会回写一个新的token
根据  token.getSeries()

在这里插入图片描述

但是我们上述的token 每次刷新只是在内存中用map存储的

我们如何做 才能吧这个token 存储在jdbc 中呢~~~
/*
 * 自定义springsecurity的 相关配置
 *
 * */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//
//    /* 让所有请求都认证 开启表单认证  关闭csrf*/
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.authorizeRequests()
//                .anyRequest()
//                .authenticated()
//                .and()
//                .formLogin()
//                .and()
//                /* 开启rememberMe 开启记住我的功能  有个勾选框 */
//                .rememberMe()
                [如果没有勾选他的时候他就会对你的请求进行一个判断了]
                .alwaysRemember(true)// 前端勾选不勾选都是rememberme 的状态
         .rememberMeParameter("aa")// 接受的是a  用来接受请求的参数 用来记录开启记住我的参数
//
//                /*key 是什么*/
//                .key(UUID.randomUUID().toString())
//                .and()
//                .csrf().disable();
//    }

    /* 让所有请求都认证 开启表单认证  关闭csrf*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .and()
                /* 开启rememberMe 开启记住我的功能  有个勾选框 */
                .rememberMe()
                .rememberMeServices(rememberMeServices()) /*指定RememberService的实现*/
                .key(UUID.randomUUID().toString())
                .and()
                .csrf().disable();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        // 返回内存的userDetailService
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser
                (User.withUsername("user")
                        /*明文123*/
                        .password("{noop}123").roles("admin").build());
        /*我们就吧内存的用户信息进行覆盖*/
        return userDetailsService;

    }

    //    userDetailsService  做一个全局的设置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置数据源
        auth.userDetailsService(userDetailsService());//
    }
、、、、、  @Bean标注一下使用那个RememberService  但是 只是内存的map而已
    @Bean
    public RememberMeServices rememberMeServices() {
        // 生成令牌时候的key
        String key = UUID.randomUUID().toString();//*或者 固定的Key*/
    默认是基于内存的
        return new PersistentTokenBasedRememberMeServices(key,userDetailsService(),new InMemoryTokenRepositoryImpl());
    }
}


  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2022-04-01 00:24:13  更:2022-04-01 00:24:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 0:35:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码