SpringBoot集成 SpringSecurity安全框架

文章目录

  • 一、CSRF跨站请求伪造攻击
  • 二、项目准备
  • 三、认识 SpringSecurity
    • 3.1 认证
      • 🎀①直接认证
      • 🎀②使用数据库认证
    • 3.2 授权
      • 🍡①基于角色授权
      • 🍡②基于权限的授权
      • 🍡③使用注解判断权限
    • 3.3 "记住我"
    • 3.4 登录和注销
      • 💥①原生登录界面
      • 💥②自定义登录界面
      • 💥注销
    • 3.4 SecurityContext


提示:以下是本篇文章正文内容,Java 系列学习将会持续更新

一、CSRF跨站请求伪造攻击

我们时常会在 QQ 上收到别人发送的钓鱼网站链接,只要你在登录QQ账号的情况下点击链接,那么不出意外,你的号已经在别人手中了。实际上这一类网站都属于恶意网站,专门用于盗取他人信息,执行非法操作,甚至获取他人账户中的财产,非法转账等。

我们在 JavaWeb 阶段已经了解了 Session 和 Cookie 的机制,在一开始的时候,服务端会给浏览器一个名为 JSESSION 的 Cookie 信息作为会话的唯一凭据,只要用户携带此 Cookie 访问我们的网站,那么我们就可以认定此会话属于哪个浏览器。因此,只要此会话的用户执行了登录操作,那么就可以随意访问个人信息等内容。

在这里插入图片描述

要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1. 登录受信任网站A,并在本地生成 Cookie。
  2. 在不登出A的情况下,访问危险网站B。

确实如此,我们无法保证以下情况不会发生:

  1. 你不能保证你登录了一个网站后,不再打开一个web页面并访问另外的网站。
  2. 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。
  3. 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

显然,我们之前编写的图书管理系统就存在这样的安全漏洞,而SpringSecurity就很好地解决了这样的问题。

二、项目准备

我们还是基于之前的 SpringBoot 项目 - 图书管理系统进行改造,需要实现以下:

  • http://localhost:8080/index.html - 任何人都可以访问,不需要登录
  • http://localhost:8080/book/{bid} - 任何人都可以访问,不需要登录
  • http://localhost:8080/user/{bid} - 只有用户可以访问,必须登录
  • http://localhost:8080/borrow/{uid} - 只有管理员可以访问,必须登录

回到目录…

三、认识 SpringSecurity

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义 Security 策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启 WebSecurity 模式

Spring Security 的两个主要目标是 “认证” 和 “授权”(访问控制)。

  • “认证”(Authentication)
    身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
    身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
  • “授权” (Authorization)
    授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
    这个概念是通用的,而不是只在Spring Security 中存在。

参考官网:https://spring.io/projects/spring-security

相关帮助文档:https://docs.spring.io/spring-security/site/docs/3.0.7.RELEASE/reference

①先引入 SpringSecurity 依赖

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

②实现 SpringSecurity 配置类: 我们可以在配置类中认证和授权

@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}

回到目录…

3.1 认证

🎀①直接认证

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();// 必须加密,使用SpringSecurity提供的BCryptPasswordEncoder
    // 在内存中定义认证用户
    auth.inMemoryAuthentication().passwordEncoder(encoder)
            .withUser("aaa").password(encoder.encode("666")).roles("currentUser")
            .and()
            .withUser("bbb").password(encoder.encode("666")).roles("currentUser")
            .and()
            .withUser("root").password(encoder.encode("123456")).roles("admin");
}

SpringSecurity 的密码校验并不是直接使用原文进行比较,而是使用加密算法将密码进行加密(更准确地说应该进行Hash处理,此过程是不可逆的,无法解密),最后将用户提供的密码以同样的方式加密后与密文进行比较。
对于我们来说,用户提供的密码属于隐私信息,直接明文存储并不好,而且如果数据库内容被窃取,那么所有用户的密码将全部泄露,这是我们不希望看到的结果,我们需要一种既能隐藏用户密码也能完成认证的机制,而Hash处理就是一种很好的解决方案,通过将用户的密码进行Hash值计算,计算出来的结果无法还原为原文,如果需要验证是否与此密码一致,那么需要以同样的方式加密再比较两个Hash值是否一致,这样就很好的保证了用户密码的安全性。

在这里插入图片描述
此时,我们就可以成功登录了!

回到目录…

🎀②使用数据库认证

a. 首先,我们必须保证数据库中的 user.password 是通过 BCryptPasswordEncoder 加密过的,否则验证不通过。我们可以将加密后的密码插入到数据库中:

@Test
public void toEncoder() {
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    System.out.println(encoder.encode("123456"));
}

b. 编写 UserMapper 中获取用户密码的 SQL

@Select("select password from user where name = #{name}")
String getPasswordByUsername(String name);

c. 然后我们需要创建一个 Service 实现,实现的是 UserDetailsService,它支持我们自己返回一个 UserDetails 对象,我们只需直接返回一个包含数据库中的用户名、密码等信息的 UserDetails 即可, SpringSecurity 会自动进行比对。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        String password = userMapper.getPasswordByUsername(s);  //从数据库根据用户名获取密码
        if(password == null) {
            throw new UsernameNotFoundException("登录失败,用户名或密码错误!");
        }
        return User   // 这里需要返回 UserDetails,SpringSecurity 会根据给定的信息进行比对
                .withUsername(s)
                .password(password)   // 直接从数据库取的密码
                .roles("currentUser")   // 用户角色
                .build();
    }
}

d. 修改一下 Security 配置类:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserDetailsServiceImpl userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 从数据库中认证
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

在这里插入图片描述
此时,我们就可以使用数据库信息登录成功了!

回到目录…

3.2 授权

🍡①基于角色授权

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 定制请求的授权规则
    http.authorizeRequests()
   	        .antMatchers("/index.html", "/book/*").permitAll() // 所有人都可以访问
  		    .antMatchers("/user/*").hasRole("currentUser") // 某个角色可以访问的页面
  		    .antMatchers("/borrow/*").hasRole("admin");
}

🍡②基于权限的授权

基于权限的授权与角色类似,需要以 hasAnyAuthorityhasAuthority 进行判断:

.anyRequest().hasAnyAuthority("page:index")
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    String password = mapper.getPasswordByUsername(s);
    if(password == null)
        throw new UsernameNotFoundException("登录失败,用户名或密码错误!");
    return User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .authorities("page:index") // 权限
            .build();
}

🍡③使用注解判断权限

我们可以直接在需要添加权限验证的请求映射上添加注解:

@PreAuthorize("hasRole('currentUser')")   //判断是否为 currentUser 角色,只有此角色才可以访问
@RequestMapping("/hello")
public String index(){
    return "hello,world";
}

通过添加 @PreAuthorize 注解,在执行之前判断判断权限,如果没有对应的权限或是对应的角色,将无法访问页面。

同样的还有 @PostAuthorize 注解,但是它是在方法执行之后再进行拦截:

@PostAuthorize("hasRole('currentUser')")
@RequestMapping("/test")
public String index(){
    System.out.println("先执行,再拦截);
    return "test";
}

回到目录…

3.3 “记住我”

<input type="checkbox" name="remember"> 记住我
@Override
protected void configure(HttpSecurity http) throws Exception {
	// ........................
    http.rememberMe()  // 记住我
    	.rememberMeParameter("remember"); // 自定义页面的参数!
}

登录成功后,将 cookie 发送给浏览器保存,以后登录带上这个 cookie,只要通过检查就可以免登录了。如果点击注销,springsecurity 帮我们自动删除了这个 cookie。

3.4 登录和注销

💥①原生登录界面

在这里插入图片描述
首先我们要了解一下 SpringSecurity 是如何进行登陆验证的,我们可以观察一下默认的登陆界面中,表单内有哪些内容:

<div class="container">
      <form class="form-signin" method="post" action="/book_manager/login">
        <h2 class="form-signin-heading">Please sign in</h2>
        <p>
          <label for="username" class="sr-only">Username</label>
          <input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
        </p>
        <p>
          <label for="password" class="sr-only">Password</label>
          <input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
        </p>
<input name="_csrf" type="hidden" value="83421936-b84b-44e3-be47-58bb2c14571a">
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>
</div>

我们发现,首先有一个用户名的输入框和一个密码的输入框,我们需要在其中填写用户名和密码,但是我们发现,除了这两个输入框以外,还有一个 input 标签,它是隐藏的,并且它存储了一串类似于 Hash 值的东西,名称为 "_csrf",其实看名字就知道,这玩意八成都是为了防止 CSRF 攻击而存在的

  • 从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法的请求(不仅仅只是登录请求,这里指的是任何请求路径)进行防护。
  • 而这里的登录表单正好是一个 POST 类型的请求。在默认配置下,无论是否登录,页面中只要发起了 PATCH,POST,PUT 和 DELETE 请求 一定会被拒绝,并返回 403 错误 (注意,这里是个究极大坑)
  • 方案一:我们可以在配置类中加入 http.csrf().disable(); // 关闭csrf功能,我们采取此方案。
  • 方案二:需要在请求的时候加入 csrfToken 才行,也就是"83421936-b84b-44e3-be47-58bb2c14571a"。如果提交的是表单类型的数据,那么表单中必须包含此 Token 字符串,键名称为 "_csrf";如果是 JSON 数据格式发送的,那么就需要在请求头中包含此 Token 字符串。

综上所述,我们最后提交的登录表单,除了必须的用户名和密码,还包含了一个 csrfToken 字符串用于验证,防止攻击。

回到目录…

💥②自定义登录界面

a. 先写一个登录页面:index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h1>用户登录</h1>
    <form action="/doLogin" method="post">
        用户名: <input type="text" name="username"><br>
        密码: <input type="password" name="password"><br>
        <input type="checkbox" name="remember"> 记住我<br>
        <button>登录</button>
    </form>
</body>
</html>

b. 编写 LoginController 登录相关的接口:

@Controller
public class LoginController {
    @GetMapping("/success") // 登录成功后跳转的页面
    public String loginSuccess() {
        return "redirect:/user/1";
    }
    @GetMapping("/failure") // 登录失败后跳转的页面
    public String loginFailure() {
        return "redirect:/index.html";
    }
}

c. 在配置类中设置:

@Override
protected void configure(HttpSecurity http) throws Exception {
	// .............................
    http.csrf().disable();

    http
            .formLogin()
            .loginPage("/index.html")  // 当用户未登录时,跳转到该自定义登录页面
            .loginProcessingUrl("/doLogin") // form表单提交地址(POST),不需要在控制层写 /doLogin
            .defaultSuccessUrl("/success")  // 登录成功后的页面
            .failureUrl("/failure"); // 登录失败后的页面
}

重启服务器就可以发现,使用了我们的自定义登录页面:
在这里插入图片描述

💥注销

注销接口:http://localhost:8080/logout ,同样可以自定义注销页面,这里就不做演示了。

http.logout().logoutSuccessUrl("/index.html");

回到目录…

3.4 SecurityContext

用户登录之后,怎么获取当前已经登录用户的信息呢?

方法一:通过使用 SecurityContextHolder 就可以很方便地得到 SecurityContext 对象了,我们可以直接使用 SecurityContext 对象来获取当前的认证信息:

@RequestMapping("/index")
public String index(){
    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();
    // org.springframework.security.core.userdetails.User
    User user = (User) authentication.getPrincipal();
    System.out.println(user.getUsername());
    System.out.println(user.getAuthorities());
    return "index";
}

方法二:除了这种方式以外,我们还可以直接通过 @SessionAttribute 从 Session 中获取:

@RequestMapping("/index")
public String index(@SessionAttribute("SPRING_SECURITY_CONTEXT") SecurityContext context){
    Authentication authentication = context.getAuthentication();
    User user = (User) authentication.getPrincipal();
    System.out.println(user.getUsername());
    System.out.println(user.getAuthorities());
    return "index";
}

注意:SecurityContextHolder 默认的存储策略是 MODE_THREADLOCAL,它是基于 ThreadLocal 实现的,getContext() 方法本质上调用的是对应的存储策略实现的方法。如果我们这样编写,那么在默认情况下是无法获取到认证信息的:

@RequestMapping("/index")
public String index(){
    new Thread(() -> {   //创建一个子线程去获取
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        User user = (User) authentication.getPrincipal();   // 失败,无法获取认证信息
        System.out.println(user.getUsername());
        System.out.println(user.getAuthorities()); 
    });
    return "index";
}

SecurityContextHolderStrategy 有三个实现类:

  • GlobalSecurityContextHolderStrategy:全局模式,不常用
  • ThreadLocalSecurityContextHolderStrategy:基于ThreadLocal实现,线程内可见
  • InheritableThreadLocalSecurityContextHolderStrategy:基于InheritableThreadLocal实现,线程和子线程可见

因此,如果上述情况需要在子线程中获取,那么需要修改 SecurityContextHolder 的存储策略,在初始化的时候设置:

@PostConstruct
public void init(){
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

这样在子线程中也可以获取认证信息了。


因为用户的验证信息是基于 SecurityContext 进行判断的,我们可以直接修改 SecurityContext 的内容,来手动为用户进行登录:

@RequestMapping("/auth")
@ResponseBody
public String auth(){
	// 获取SecurityContext对象(当前会话肯定是没有登陆的)
    SecurityContext context = SecurityContextHolder.getContext();
    // 手动创建一个UsernamePasswordAuthenticationToken对象,也就是用户的认证信息,角色需要添加ROLE_前缀,权限直接写
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", null,
            AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_user"));
    		context.setAuthentication(token);  // 手动为SecurityContext设定认证信息
    return "Login success!";
}

在未登录的情况下,访问此地址将直接进行手动登录,再次访问 /index 页面,可以直接访问,说明手动设置认证信息成功。

疑惑:SecurityContext 这玩意不是默认线程独占吗,那每次请求都是一个新的线程,按理说上一次的 SecurityContext 对象应该没了才对啊,为什么再次请求依然能够继续使用上一次 SecurityContext 中的认证信息呢?

SecurityContext 的生命周期:请求到来时从 Session 中取出,放入 SecurityContextHolder 中,请求结束时从 SecurityContextHolder 取出,并放到 Session 中,实际上就是依靠 Session 来存储的,一旦会话过期验证信息也跟着消失。

回到目录…


总结:
提示:这里对文章进行总结:
本文是对SpringSecurity的学习,学习了它的两大功能:认证和授权,以及如何使用数据库进行认证,如何使用自定义的登录页面,最后也学习了使用SecurityContext获取认证用户的信息。之后的学习内容将持续更新!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/1410.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JavaEE】如何将JavaWeb项目部署到Linux云服务器?

写在前面 大家好&#xff0c;我是黄小黄。不久前&#xff0c;我们基于 servlet 和 jdbc 完善了博客系统。本文将以该系统为例&#xff0c;演示如何将博客系统部署到 Linux 云服务器。 博客系统传送门&#xff1a; 【JavaEE】前后端分离实现博客系统&#xff08;页面构建&#…

arcpy基础篇(3)-处理空间数据

ArcPy的数据访问模块arcpy.da&#xff0c;可以控制会话、编辑操作、游标、表或要素类与NumPy数组之间相互转换的函数以及对版本化和复本工作流的支持。 1.使用游标访问数据 游标是一个数据库术语&#xff0c;它主要用于访问表格中的每一行记录或者向表中插入新的记录。在ArcG…

【数据结构】Java实现单链表

目录 1. ArrayList的缺陷 2. 链表 2.1 链表的概念及结构 2.2 接口的实现 3. 动手实现单链表 3.1 重写SeqList接口方法 3.2 在当前链表头部添加节点&#xff08;头插&#xff09; 3.3 在 第index位置添加节点&#xff08;任意位置&#xff09; 3.4 在当前链表尾部添加…

Python|蓝桥杯进阶第四卷——图论

欢迎交流学习~~ 专栏&#xff1a; 蓝桥杯Python组刷题日寄 蓝桥杯进阶系列&#xff1a; &#x1f3c6; Python | 蓝桥杯进阶第一卷——字符串 &#x1f50e; Python | 蓝桥杯进阶第二卷——贪心 &#x1f49d; Python | 蓝桥杯进阶第三卷——动态规划 ✈️ Python | 蓝桥杯进阶…

四、快速上手 ODM 操作 Mongodb

文章目录一、ODM 的选择和安装二、MongoEngine 模型介绍三、文档的嵌套模型四、使用 ODM 查询数据4.1 查询一个文档4.2 条件查询4.3 统计、排序和分页五、使用 ODM 新增数据六、使用 ODM 修改和删除数据一、ODM 的选择和安装 MongoEngine&#xff1a; 使用最为广泛的 ODM。htt…

【C++】命名空间

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;C的学习之路 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言一、命名空间产生的背景二、命名空…

基础篇:07-Nacos注册中心

1.Nacos安装部署 1.1 下载安装 nacos官网提供了安装部署教程&#xff0c;其下载链接指向github官网&#xff0c;选择合适版本即可。如访问受阻可直接使用以下最新稳定版压缩包&#xff1a;&#x1f4ce;nacos-server-2.1.0.zip&#xff0c;后续我们也可能会更改为其他版本做更…

图论学习(五)

极图 l部图的概念与特征 定义&#xff1a;若简单图G的点集V有一个划分&#xff1a; 且所有的Vi非空&#xff0c;Vi内的点均不邻接&#xff0c;设G是一个l部图。 如果l2&#xff0c;则G就是偶图。n阶无环图必是n部图。若l1<l2≤n&#xff0c;则任意的l1部图也是l2部图。…

【毕业设计】基于SpringBoot+Vue论坛管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】

您好&#xff0c;我是码农飞哥&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通 &#x1f601; 2. 毕业设计专栏&…

JavaScript学习笔记(7.0)

<!--* Author: RealRoad1083425287qq.com* Date: 2023-03-13 14:50:18* LastEditors: Mei* LastEditTime: 2023-03-13 15:08:54* FilePath: \vscode\鼠标跟随.html* Description: * * Copyright (c) 2023 by ${git_name_email}, All Rights Reserved. --> <!DOCTYPE …

Vue3(递归组件) + 原生Table 实现树结构复杂表格

一、递归组件 什么是递归&#xff0c;Javascript中经常能接触到递归函数。也就是函数自己调用自己。那对于组件来说也是一样的逻辑。平时工作中见得最多应该就是菜单组件&#xff0c;大部分系统里面的都是递归组件。文章中我做了按需引入的配置&#xff0c;所以看不到我引用组…

什么是让ChatGPT爆火的大语言模型(LLM)

什么是让ChatGPT爆火的大语言模型(LLM) 更多精彩内容: https://www.nvidia.cn/gtc-global/?ncidref-dev-876561 文章目录什么是让ChatGPT爆火的大语言模型(LLM)大型语言模型有什么用&#xff1f;大型语言模型如何工作&#xff1f;大型语言模型的热门应用在哪里可以找到大型语言…

西安石油大学C语言期末真题实战

很简单的一道程序阅读题&#xff0c;pa’默认为a【0】&#xff0c;接下来会进行3次循环 0 1 2 输出结果即可 前3题就是一些基础定义&#xff0c;在此不多赘述 要注意不同的数据类型的字节数不同 a<<2 b>>1&#xff08;b>>1;就是说b自身右位移一位&#xff08…

支付系统设计:消息重试组件封装

文章目录前言一、重试场景分析一、如何实现重试1. 扫表2. 基于中间件自身特性3. 基于框架4. 根据公司业务特性自己实现的重试二、重试组件封装1. 需求分析2. 模块设计2.1 持久化模块1. 表定义2. 持久化接口定义3. 持久化配置类2.2 重试模块1.启动2.重试3. 业务端使用1. 引入依赖…

Linux基础(3) Vim编辑器与Shell命令脚本

1、VIM文本编辑器 VIM编辑器的三大模式 命令模式&#xff1a; 控制光标移动&#xff0c;可对文本进行复制、粘贴和查找等工作输入模式&#xff1a; 正常的文本录入。末行模式&#xff1a; 保存或退出文档&#xff0c;以及设置编辑环境三种模式的切换&#xff1a; ​注意&…

app自动化测试——Android studio安装与配置

文章目录一、Appium框架介绍二、Appium 生态工具三、环境安装四、安装Android studio五、配置环境变量六、创建模拟器查看设备启动模拟器一、Appium框架介绍 1、跨语言&#xff1a;java、python等 2、跨平台&#xff1a;Android、IOS、Windows、Mac 3、底层多引擎切换 4、生态…

(待补充)小蒟蒻的刷题成长之路-------2023年中国高校计算机大赛-团队程序设计天梯赛(GPLT)上海理工大学校内选拔赛(同步赛)

小蒟蒻的刷题成长之路 蓝桥杯的比赛流程和必考点_蓝桥杯省赛考点_时雨h的博客-CSDN博客 大一学生一周十万字爆肝版C语言总结笔记_大一c语言笔记_时雨h的博客-CSDN博客 程序设计与 C 语言期末复习_时雨h的博客-CSDN博客 P8597 [蓝桥杯 2013 省 B] 翻硬币个人思考总结第五届传智杯…

西瓜视频登录页面

题目 代码 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>登录页面</title><style>td{width: 160px;height: 25px;}img{width: 20px;height: 20px;}.number, .password{background: rgba(0,0,0,.05);}.numbe…

指针进阶(上)

内容小复习&#x1f431;&#xff1a; 字符指针:存放字符的数组 char arr1[10]; 整型数组:存放整型的数组 int arr2[5]; 指针数组:存放的是指针的数组 存放字符指针的数组(字符指针数组) char* arr3[5]; 存放整型指针的数组(整型指针数组) int* arr[6]; 下面进入学习了哦~&…

【二分查找】

二分查找704. 二分查找35. 搜索插入位置34. 在排序数组中查找元素的第一个和最后一个位置结语704. 二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在…