【Redis】2、Redis应用之【根据 Session 和 Redis 进行登录校验和发送短信验证码】

目录

  • 一、基于 Session 实现登录
    • (1) 发送短信验证码
      • ① 手机号格式后端校验
      • ② 生成短信验证码
    • (2) 短信验证码登录、注册
    • (3) 登录验证
      • ① 通过 SpringMVC 定义拦截器
      • ② ThreadLocal
    • (4) 集群 Session 不共享问题
  • 二、基于 Redis 实现共享 session 登录
    • (1) 登录之后,缓存 token 到客户端
    • (2) 每次请求都携带 token
    • (3) 短信验证码
    • (4) 短信验证码登录、注册
    • (5) 免登录
    • (6) 刷新登录有效期

🌼 文章基于 B 站黑马程序员视频教程编写
🌼 做笔记便于日后复习

一、基于 Session 实现登录

在这里插入图片描述

(1) 发送短信验证码

在这里插入图片描述

① 手机号格式后端校验

手机号校验的正则表达式

/*** 正则表达式*/
public abstract class RegexPatterns {/*** 手机号正则*/public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";/*** 邮箱正则*/public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";/*** 密码正则:4~32 位的字母、数字、下划线*/public static final String PASSWORD_REGEX = "^\\w{4,32}$";/*** 验证码正则, 6位数字或字母*/public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";}

校验工具类:

public class RegexUtils {/*** 是否是无效手机格式** @param phone 要校验的手机号* @return true:符合,false:不符合*/public static boolean isPhoneInvalid(String phone) {return mismatch(phone, RegexPatterns.PHONE_REGEX);}/*** 是否是无效邮箱格式** @param email 要校验的邮箱* @return true:符合,false:不符合*/public static boolean isEmailInvalid(String email) {return mismatch(email, RegexPatterns.EMAIL_REGEX);}/*** 是否是无效验证码格式** @param code 要校验的验证码* @return true:符合,false:不符合*/public static boolean isCodeInvalid(String code) {return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);}// 校验是否不符合正则格式private static boolean mismatch(String str, String regex) {if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);}
}

② 生成短信验证码

     <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.17</version></dependency>

🌼 hutool 工具的详细使用: https://doc.hutool.cn/pages/index/


    /*** 发送短信验证码** @param phone   手机号* @param session 用户缓存验证码*/@Overridepublic Result sendCode(String phone, HttpSession session) {// 校验手机号格式是否符合手机号的规范if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}// 生成 6 位数字短信验证码String code = RandomUtil.randomNumbers(6);// 把短信验证码保存到服务端 session 中session.setAttribute("code", code);// 发送短信验证码给手机号 phonelog.info("向 {} 手机号发送了验证码:{}", phone, code);return Result.ok("发送验证码成功");}

(2) 短信验证码登录、注册

在这里插入图片描述

    @Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {Result result = requestParamsValidate(loginForm, session);if (!result.getSuccess()) { // 手机号或验证码校验失败return result;}// 根据手机号查询用户String phone = loginForm.getPhone();User user = query().eq("phone", phone).one();// 根据手机号在数据中没有查询到用户信息if (user == null) {// 注册用户Result saveResult = saveUserByPhone(phone);if (saveResult.getSuccess()) { // 注册用户成功user = (User) saveResult.getData();} else {return saveResult;}}// 保存用户信息到 sessionsession.setAttribute("user", user);return Result.ok("登录成功");}private Result saveUserByPhone(String phone) {User newUser = new User();newUser.setNickName("USER_" + RandomUtil.randomString(9));newUser.setPhone(phone);if (save(newUser)) {return Result.ok(newUser);}return Result.fail("服务器忙, 用户保存到数据库失败");}private Result requestParamsValidate(LoginFormDTO loginForm, HttpSession session) {// 校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}// 校验验证码是否存在Object cacheCode = session.getAttribute("code");String paramCode = loginForm.getCode();if (RegexUtils.isCodeInvalid(paramCode) || !paramCode.equals(cacheCode)) {return Result.fail("验证码错误");}return Result.ok();}

(3) 登录验证

在这里插入图片描述

在这里插入图片描述

🌿 根据 Cookie 中的 JSESSIONID 获取到 Session
🌿 然后从 Session 中获取到信息


🌿 登录校验需要在拦截器(Interceptor)中完成
🌿 SpringMVC 的 Interceptor 可以拦截 Controller,在请求到达 Controller 之前做一些事情

① 通过 SpringMVC 定义拦截器

  • 实现(implements)HandlerInterceptor 接口
  • 可覆盖该接口中的三个默认方法

🌼 preHandle:在 Controller 的处理方法之前调用(当该方法的返回值为 true 的时候 才执行 Controller 里面的内容)
🌱通常在 preHandle 中进行初始化、请求预处理等操作(可进行登录验证
🌱preHandle 返回 true 才会执行后面的调用。若返回 false,不会调用 Controller 处理方法、postHandle 和 afterCompletion
🌱当有多个拦截器时,preHandle 按照正序执行

🌼 postHandle:在 Controller 的处理方法之DispatcherServlet 进行视图渲染之调用
🌱可在 postHandle 中进行请求后续加工处理操作
🌱当有多个拦截器时,postHandle 按照序执行

🌼 afterCompletion:在 DispatcherServlet 进行视图渲染之后调用
🌱一般在这里进行资源回收操作
🌱当有多个拦截器时,afterCompletion 按照逆序执行


配置拦截器:

@Configuration
public class MvcConfig implements WebMvcConfigurer {/*** 配置拦截器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( // 这些请求不可拦截"/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}

② ThreadLocal

🌼 ThreadLocal 可以解释成线程的局部变量
🌼一个 ThreadLocal 的变量只有当前自身线程可以访问,别的线程都访问不了,那么自然就避免了线程竞争
🌼ThreadLocal 提供了一种与众不同的线程安全方式,它不是在发生线程冲突时想办法解决冲突,而是彻底避免了冲突的发生

/*** 用于保存 UserDTO 的 ThreadLocal 的封装*/
public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user) {tl.set(user);}public static UserDTO getUser() {return tl.get();}public static void removeUser() {tl.remove();}
}

登录拦截器:

/*** 登录校验拦截器*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {HttpSession session = request.getSession();Object user = session.getAttribute("user");if (null == user) {response.setStatus(401);return false; // 拦截}UserDTO userDTO = user2UserDto((User) user);// 保存用户信息到 ThreadLocal 中UserHolder.saveUser(userDTO);return true;}private UserDTO user2UserDto(User user) {UserDTO userDTO = new UserDTO();userDTO.setIcon(user.getIcon());userDTO.setId(user.getId());userDTO.setNickName(user.getNickName());return userDTO;}/*** 资源释放*/@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) throws Exception {UserHolder.removeUser();}@Overridepublic void postHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView) throws Exception {}
}

    @GetMapping("/me")public Result me() {// 获取当前登录的用户并返回UserDTO userDto = UserHolder.getUser();return Result.ok(userDto);}

🌼 /me 这个 Controller 执行完毕后,LoginInterceptor 的 afterCompletion 会被调用

(4) 集群 Session 不共享问题

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

二、基于 Redis 实现共享 session 登录

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

🎉 登录之后,每次发起请求都要携带 token(用户登录凭证)

(1) 登录之后,缓存 token 到客户端

     login() {const {radio, phone, code} = this.formif (!radio) {this.$message.error("请先确认阅读用户协议!");return}if (!phone || !code) {this.$message.error("手机号和验证码不能为空!");return}if (phone.length !== 11) {this.$message.error("手机号格式错误!");return}axios.post("/user/login", this.form).then(({data}) => {if (data) {// 保存用户信息到 sessionsessionStorage.setItem("token", data);}// 跳转到首页location.href = "/info.html"}).catch(err => {console.log(err)this.$message.error(err)})},

(2) 每次请求都携带 token

let commonURL = "/api";
// 设置后台服务地址
axios.defaults.baseURL = commonURL
axios.defaults.timeout = 2000// request 拦截器,将用户 token 放入头中
let token = sessionStorage.getItem("token")// 请求拦截器
axios.interceptors.request.use(config => {// 如果 token 存在, 将用户 token 放入头中(key 是 authorization)if (token) config.headers['authorization'] = tokenreturn config},error => {console.log(error)return Promise.reject(error)}
)// 响应拦截器
axios.interceptors.response.use(function (response) {// 判断执行结果if (!response.data.success) {return Promise.reject(response.data.errorMsg)}return response.data;
}, function (error) {// 一般是服务端异常或者网络异常console.log(error)if (error.response.status == 401) {// 未登录,跳转setTimeout(() => {location.href = "/login.html"}, 200);return Promise.reject("请先登录");}return Promise.reject("服务器异常");
});// 请求参数序列化
axios.defaults.paramsSerializer = function (params) {let p = "";Object.keys(params).forEach(k => {if (params[k]) {p = p + "&" + k + "=" + params[k]}})return p;
}

🎶在 axios 的请求拦截器中配置 token,每次发起请求该请求会首先被拦截
🎶被拦截之后,往该请求的请求头中设置 token【key: authorization;value: token 值】

(3) 短信验证码

Redis 相关常量:

public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:phone";public static final Long LOGIN_CODE_TTL = 2L; // 验证码有效期public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 36000L;public static final Long CACHE_NULL_TTL = 2L;public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L;public static final String SECKILL_STOCK_KEY = "seckill:stock:";public static final String BLOG_LIKED_KEY = "blog:liked:";public static final String FEED_KEY = "feed:";public static final String SHOP_GEO_KEY = "shop:geo:";public static final String USER_SIGN_KEY = "sign:";
}
   public Result sendCode(String phone, HttpSession session) {// 校验手机号格式是否符合手机号的规范if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}// 生成 6 位数字短信验证码String code = RandomUtil.randomNumbers(6);// 把短信验证码保存到 Redis 中stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone,code,RedisConstants.LOGIN_CODE_TTL,TimeUnit.MINUTES);// 发送短信验证码给手机号 phonelog.info("向 {} 手机号发送了验证码:{}", phone, code);return Result.ok("发送验证码成功");}

(4) 短信验证码登录、注册

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

在这里插入图片描述

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {Result result = requestParamsValidate(loginForm, session);if (!result.getSuccess()) { // 手机号或验证码校验失败return result;}// 根据手机号查询用户String phone = loginForm.getPhone();User user = query().eq("phone", phone).one();// 根据手机号在数据中没有查询到用户信息if (user == null) {// 注册用户Result saveResult = saveUserByPhone(phone);if (saveResult.getSuccess()) { // 注册用户成功user = (User) saveResult.getData();} else {return saveResult;}}// 保存用户信息到 Redis// 生成随机 token 串String token = RedisConstants.LOGIN_USER_KEY + UUID.randomUUID().toString(true);// 把 User 转换为 UserDTO(过滤敏感数据)UserDTO userDTO = user2UserDto(user);// 把 UserDTO 转换为 HashMap(便于往 Redis 中存储)Map<String, Object> userDtoMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 把用户信息保存到 Redis// stringRedisTemplate 要求存储的 key 和 value 都必须是 String 类型// 否则会报类型转换错误stringRedisTemplate.opsForHash().putAll(token, userDtoMap);// 设置登录有效期stringRedisTemplate.expire(token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);session.setAttribute("user", user);return Result.ok(token);}private UserDTO user2UserDto(User user) {UserDTO userDTO = new UserDTO();userDTO.setIcon(user.getIcon());userDTO.setId(user.getId());userDTO.setNickName(user.getNickName());return userDTO;}private Result saveUserByPhone(String phone) {User newUser = new User();newUser.setNickName("USER_" + RandomUtil.randomString(9));newUser.setPhone(phone);if (save(newUser)) {return Result.ok(newUser);}return Result.fail("服务器忙, 用户保存到数据库失败");}private Result requestParamsValidate(LoginFormDTO loginForm, HttpSession session) {// 校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}// 通过手机号获取验证码String redisCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);// 校验验证码是否存在String paramCode = loginForm.getCode();if (RegexUtils.isCodeInvalid(paramCode) || !paramCode.equals(redisCode)) {return Result.fail("验证码错误");}return Result.ok();}}

(5) 免登录

🎄 需要实现一个效果:用户只有在使用该应用,哪么该用户的登录有效期就延长七天
🎄 而不是到达七天就退出登录

🎄 在 LoginInterceptor 中,若用户有发请求,就把该用户的登录有些时间的缓存更新为七天

/*** 登录校验拦截器*/
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {// 获取请求头中的 token 串String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {response.setStatus(401);return false; // 拦截}// 通过前端传过来的 token 串从 Redis 中获取用户信息Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);if (userMap.isEmpty()) {response.setStatus(401);return false; // 拦截}// 保存用户信息到 ThreadLocal 中// 把 HashMap 类型转换为 UserDto 类型UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);// 刷新登录有效期(只要用户是活跃的, 登录有效期就延长七天)stringRedisTemplate.expire(token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}/*** 资源释放*/@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) throws Exception {UserHolder.removeUser();}@Overridepublic void postHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView) throws Exception {}
}

🎄 LoginInterceptor 拦截器并没有被 IoC 管理,所以不能在 LoginInterceptor 中使用 @Resource@Autowired

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 配置拦截器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns( // 这些请求不可拦截"/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}

(6) 刷新登录有效期

在这里插入图片描述

🎄 RefreshInterceptor 会拦截每一个请求,然后进行登录有效期刷新
🎄 上一节中的 LoginInterceptor 只有当访问需要登录校验的请求的时候才会刷新

/*** 刷新登录拦截器(每个请求都要来到这里)*/
public class RefreshLoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshLoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {// 获取请求头中的 token 串String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true; // 拦截(进入 LoginInterceptor)}// 通过前端传过来的 token 串从 Redis 中获取用户信息Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);if (userMap.isEmpty()) {return true; // 拦截(进入 LoginInterceptor)}// 保存用户信息到 ThreadLocal 中// 把 HashMap 类型转换为 UserDto 类型UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);// 刷新登录有效期(只要用户是活跃的, 登录有效期就延长七天)stringRedisTemplate.expire(token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}/*** 资源释放*/@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) throws Exception {UserHolder.removeUser();}@Overridepublic void postHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView) throws Exception {}
}
/*** 登录校验拦截器*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {if (UserHolder.getUser() == null) {response.setStatus(401);return false;}return true;}}
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 配置拦截器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( // 这些请求不可拦截"/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");// order 值越小优先级越高// order 值默认是 0registry.addInterceptor(new RefreshLoginInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(-1);}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java版电子招标采购系统源码之电子招标采购实践与展望-招标采购管理系统

统一供应商门户 便捷动态、呈现丰富 供应商门户具备内外协同的能力&#xff0c;为外部供应商集中推送展示与其相关的所有采购业务信息&#xff08;历史合作、考察整改&#xff0c;绩效评价等&#xff09;&#xff0c;支持供应商信息的自助维护&#xff0c;实时风险自动提示。…

Prometheus - Concept

一 Prometheus 是什么 Prometheus 是一个开源的 监控和报警系统 。该系统内置和基于时间序列地抓取、存储、查询、绘图数据、报警。 现在是一个开源项目&#xff0c;继 K8S 后的第二个云原生计算基金会的托管项目&#xff0c;可见其火爆程度。 二 Prometheus 的特征 Promet…

Mysql (insert,update操作)

1.创建表&#xff1a; 创建员工表employee&#xff0c;字段如下&#xff1a; id&#xff08;员工编号&#xff09;&#xff0c;name&#xff08;员工名字&#xff09;&#xff0c;gender&#xff08;员工性别&#xff09;&#xff0c;salary&#xff08;员工薪资&#xff09; …

【网络编程】应用层协议——HTTP协议

文章目录 一、HTTP协议基本认识二、URL的认识2.1 urlencode和urldecode 三、HTTP协议格式3.1 HTTP请求与响应格式3.2 如何保证请求和响应被应用层完整读取&#xff1f;3.3 请求和响应如何做到序列化和反序列化&#xff1f;3.4 代码验证请求格式3.5 代码验证响应格式3.5.1 telne…

OpenGl纹理贴图

给图形赋予颜色时&#xff0c;采用纹理贴图的方式。 每个顶点关联一个纹理坐标(Texture Coordinate),然后在图形的其他片段上进行片段插值(Fragment Interpolation) 顶点坐标如下&#xff1a; float vertices[] { // positions // colors // texture coords 0.2f, 0.2f, 0.0f,…

创造一款安卓自定义控件_裁剪原理介绍

1、新增功能&#xff0c;旋转&#xff1a; 效果如图&#xff0c;点击旋转&#xff0c;可以将控件画面本身进行90度倍数的旋转&#xff0c;并进行宽高比例适配&#xff0c;旋转之后裁剪依然正常。 功能实现原理&#xff1a; 1、通过调用view的setRotation功能进行以View为中心…

Docker 中的 .NET 异常了怎么抓 Dump (转载)

一、背景 1. 讲故事 有很多朋友跟我说&#xff0c;在 Windows 上看过你文章知道了怎么抓 Crash, CPU爆高&#xff0c;内存暴涨 等各种Dump&#xff0c;为什么你没有写在 Docker 中如何抓的相关文章呢&#xff1f;瞧不上吗&#xff1f; 哈哈&#xff0c;在DUMP的分析旅程中&a…

[MySQL]MySQL库的操作

[MySQL]MySQL库的操作 文章目录 [MySQL]MySQL库的操作1. 创建数据库2. 字符集和校验规则2.1. 基本概念2.2. 查看系统默认字符集以及校验规则2.3. 查看数据库支持的字符集2.4 查看数据库支持的校验规则2.5 指明字符集和校验规则创建数据库2.6 校验规则对数据库的影响 3. 删除数据…

我爱学QT-制作一个最简单的QT界面

1.qt基础 qt的移植性非常强&#xff0c;一套代码不用我们改太多&#xff0c;直接通用所有平台。不久的将来&#xff0c;qt会被用到MCU上&#xff0c;学习QT还是非常有意义的。 2.做一个简单的QT界面 首先新建工程 注意这个不一样 工程文件分析&#xff1a; #--------------…

【优选算法】—— 双指针问题

从今天开始&#xff0c;整个暑假期间。我将不定期给大家带来有关各种算法的题目&#xff0c;帮助大家攻克面试过程中可能会遇到的算法这一道难关。 目录 &#xff08;一&#xff09; 基本概念 &#xff08;二&#xff09;题目讲解 1、难度&#xff1a;easy 1️⃣移动零 2️…

nginx的优化

目录 一 隐藏版本号在网页上面有nginx的版本号会让别人攻击你的服务器 二 nginx的优化之日志分割 三 nginx的优化之页面压缩 四 连接超时 五 nginx的并发设置 七总结:nginx的优化 一 隐藏版本号在网页上面有nginx的版本号会让别人攻击你的服务器 如图所示 第一种方法是关…

数据结构与算法——数据结构有哪些,常用数据结构详解

数据结构是学习数据存储方式的一门学科&#xff0c;那么&#xff0c;数据存储方式有哪几种呢&#xff1f;下面将对数据结构的学习内容做一个简要的总结。 数据结构大致包含以下几种存储结构&#xff1a; 线性表&#xff0c;还可细分为顺序表、链表、栈和队列&#xff1b;树结…