当前位置:网站首页>Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码
Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码
2022-07-06 04:23:00 【Alascanfu】
本文目录
Redis 实战 —— 实战篇
课程介绍
heima 点评Redis —— 项目
1️⃣ 短信登录 —— Redis 的共享 session 应用
2️⃣ 商户查询缓存 —— 企业的缓存使用技巧| 缓存雪崩、穿透等问题解决
3️⃣ 达人探店 —— 基于 List 点赞链表|基于 SortedSet 的点赞排行榜
4️⃣ 优惠券秒杀 —— Redis 的计数器| Lua 脚本 Redis | 分布式锁 | Redis 的三种消息队列
5️⃣ 好友关注 —— 基于 Set 集合的关注|取关|共同关注|消息推送的功能
6️⃣ 附近的商户 —— Redis 的 GeoHash 的应用
7️⃣ 用户签到 —— Redis 的 BitMap 数据统计功能
8️⃣ UV 统计 —— Redis 的 HyperLogLog 的统计功能
SMS Login —— 基于 Redis 的短信登录功能
导入 黑马点评 项目
导入后端项目
步骤一:首先导入 SQL 文件到数据库当中
其中表的数据结构说明
1️⃣ tb_user: 用户表
2️⃣ tb_user_info : 用户详情表
3️⃣ tb_shop : 商品信息表
4️⃣ tb_shop_type : 商户类型表
5️⃣ tb_blog : 用户日记表(达人探店日记)
6️⃣ tb_follow : 用户关注表
7️⃣ tb_voucher : 优惠券表
8️⃣ tb_voucher_order : 优惠券的订单表
注意点:MySQL 的版本采用 5.7 及其版本之上
项目架构
步骤二:将写好的半成品源码导入到 IDEA 中并测试
步骤三:改写 application.yaml 文件 改写成自己所对应的数据库
server:
port: 8081
spring:
application:
name: hmdp
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.56.1:3306/hmdp?useSSL=false&serverTimezone=UTC
username: root
password: root
redis:
host: 192.168.56.103
port: 6379
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
level:
com.hmdp: debug
测试连接到数据库
步骤四:启动HmDianPingApplication并用浏览器进行访问测试
http://localhost:8081/shop-type/list
测试导入成功~
导入前端项目
步骤一:在 Nginx 所在目录下打开一个 CMD 窗口,输入命令:
start nginx.exe
步骤二:打开 chrome 浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具——并开启手机模式
http://localhost:8080/
如果出现了能正常打开 前端项目 但是对应的图片以及数据并没有显示,则说明你的后端项目没有开启,前端无法通过请求刷新渲染数据到前端,所以务必保证后端应用程序已经启动。
如图所示大功告成啦~
基于 Session 实现登录
发送短信验证码的逻辑思路
首先用户会提交个人的手机号请求发送验证码
服务端会对手机号进行校验:
- 不符合条件:显示不符合的提示给用户,提示用户重新输入合法的手机号
- 符合条件:生成对应的验证码。
拿到生成好的验证码会将其保存到 Session 当中,随后执行 发送验证码 的业务服务
结束
步骤一:到对应的 UserController 层编写对应接口、并对对应接口服务的具体实现impl
/** * 发送手机验证码 */
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// TODO 发送短信验证码并保存验证码
return userService.sendCode(phone , session);
}
IUserService
Result sendCode(String phone, HttpSession session);
UserServiceImpl
/** * 功能描述 * 获取得到用户发送手机发送短信的获取验证码的请求 * @date 2022/7/4 * @author Alascanfu */
@Override
public Result sendCode(String phone, HttpSession session) {
// 判断当前用户提交的 手机是否符合正确的 格式
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果格式匹配失败 则放回错误信息给前端
return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
}
// 如果格式匹配成功 则生成一串随机的验证码
String code = RandomUtil.randomNumbers(6);
// 将生成好的随机验证码 保存到 session 当中
session.setAttribute("code",code);
// 发送短信验证码给用户
log.info("验证码发送成功 , code => {}",code);
// 发送验证码成功 返回成功响应
return Result.ok();
}
步骤三:启动程序并测试是否能在后台查看到我们对应的验证码生成
短信验证码登录、注册逻辑思路
首先用户会将自身收到的验证码和手机号以请求的方式提交
服务端对用户提交的校验码先进行校验:
- 不匹配:则提示用户验证码输入错误,请重试
- 匹配:根据手机号查询用户
根据手机号到数据库中去查找
- 没有该用户:说明当前用户为第一次使用,则自动为其注册一个账号,并将账号信息加入到数据库做持久化,随后将该用户的数据保存到 Session中方便调用数据。
- 有该用户:将该用户信息保存到 Session中即可。
步骤一:到 UserController 中编写好对应的登录服务接口
/** * 登录功能 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
return userService.login(loginForm , session);
}
步骤二:编写对应的服务接口以及服务接口的具体实现
Result login(LoginFormDTO loginForm, HttpSession session);
/** * 功能描述 * 登录功能的具体实现类 登录参数,包含手机号、验证码;或者手机号、密码 * @date 2022/7/4 * @author Alascanfu */
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 首先拿到用户请求发送过来的 验证码 进行和 session 中对应的验证码是否匹配
// 如果不匹配 则直接返回给前端 说明用户验证码 不正确
String cacheCode = (String) session.getAttribute(SystemConstants.SESSION_PHONE_CODE);
String cachePhone = (String) session.getAttribute(SystemConstants.SESSION_PHONE);
String code = loginForm.getCode();
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())){
return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
}
if (!cachePhone.equals(phone)){
return Result.fail("验证码与手机号不匹配,请重试!");
}
if (cacheCode == null || !cacheCode.equals(code)){
return Result.fail("对不起,您输入的验证码有误,请重试!");
}
// 如果匹配则进行验证用户名输入的手机号是否是之前已经注册过的
User user = userMapper.selectUserByPhone(phone);
if (user == null){
// 反之如果没有注册过, 则快速帮用户注册一个 用户账户 填充一些默认信息
user = createUserWithPhone(phone);
}
// 将快速注册好的用户|对应登录成功的用户, 加入到当前 session 当中
session.setAttribute(SystemConstants.SESSION_USER,user);
// 最后登录成功
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
userMapper.insert(user);
return user;
}
步骤三:启动测试
登陆成功后会自动跳转会首页
数据库中也有对应的数据~
校验登录状态逻辑思路
- 首先用户对该网站的访问请求都会携带cookie信息
- 随后服务端可以根据用户提供的 Jsessionid 查看 服务端 Session 是否存在该用户的信息。
- 如果没有,用户请求就会被拦截,返回到登录界面,提示用户重新登录。
- 如果存在,则将用户的信息 保存到 ThreadLocal 这个线程局部变量域当中方便后续操作。然后放行请求。
步骤一:编写拦截器、用于校验 Session 中的 user 信息
LoginInterceptor
/*** * @author: Alascanfu * @date : Created in 2022/7/4 20:21 * @description: LoginInterceptor * @modified By: Alascanfu **/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 先通过 request 获取 session
HttpSession session = request.getSession();
// 然后通过 session 尝试获取 用户 信息
User user = (User) session.getAttribute(SystemConstants.SESSION_USER);
if (user == null){
// 如果 user 不存在 则返回错误信息 对请求拦截 设置返回的状态码为 未授权
response.setStatus(401);
return false ;
}
// 如果存在 user 将其保存到 threadLocal 当中
UserDTO userDTO = userToUserDTO(user);
UserHolder.saveUser(userDTO);
return true;
}
/** User 类型 转换为 UserDTO */
private UserDTO userToUserDTO(User user) {
UserDTO userDTO = new UserDTO();
userDTO.setId(user.getId());
userDTO.setIcon(user.getIcon());
userDTO.setNickName(user.getNickName());
return userDTO;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 当请求完成之后 清除掉对应 threadLoacl 中的 user 数据即可
UserHolder.removeUser();
}
}
步骤二:创建 WebMvcConfig 类 客制化拦截器配置
/*** * @author: Alascanfu * @date : Created in 2022/7/4 20:36 * @description: Web MVC configuration * @modified By: Alascanfu **/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor ;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
步骤三:改写 UserController 中的 /user/me 请求接口
@GetMapping("/me")
public Result me(){
// TODO 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
步骤四:进行测试
按照用户登录的步骤进行登录,查看当前用户数据
集群的 Session 共享问题
分布式之session共享问题 4种解决方案及spring session的使用
基于 Redis 实现共享 Session 登录
需要查看前端代码逻辑 来了解后端是需要获取请求头中的哪个信息来获取 token 信息数据
// request拦截器,将用户token放入头中
let token = sessionStorage.getItem("token");
axios.interceptors.request.use(
config => {
if(token) config.headers['authorization'] = token
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)
很清楚的可以看到
let token = sessionStorage.getItem("token");
从浏览器内存中获取 token 数值
axios.interceptors.request.use
会在每次发送请求时经过该 axios 拦截器。
将请求头中的 authorization 携带 token 数值信息。
步骤一:改写 原 Session 存储验证码 => Redis 存储验证码
1️⃣ 获取用户提交的请求数据
i. 验证手机号是否格式正确
1.不正确 => 返回 错误信息
2.正确 => 进行下步逻辑判断
2️⃣ 手机号码格式正确则生成一串随机的验证码
3️⃣ 将生成好的 验证码 保存到 redis 当中
i. key => 业务简写:业务唯一属性: + phone
ii.value => 验证码
4️⃣ 返回请求成功信息
/** * 功能描述 * 获取得到用户发送手机发送短信的获取验证码的请求 * @date 2022/7/4 * @author Alascanfu */
@Override
public Result sendCode(String phone, HttpSession session) {
// 判断当前用户提交的 手机是否符合正确的 格式
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果格式匹配失败 则放回错误信息给前端
return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
}
// 如果格式匹配成功 则生成一串随机的验证码
String code = RandomUtil.randomNumbers(6);
// 将生成好的随机验证码 保存到 Redis 当中
// k : login:code:18584100561 v : code expireTime 2 min
stringRedisTemplate.opsForValue()
.set(RedisConstants.LOGIN_CODE_KEY + phone ,
code , RedisConstants.LOGIN_CODE_TTL , TimeUnit.MINUTES);
// 发送短信验证码给用户
log.info("验证码发送成功 , code => {}",code);
// 发送验证码成功 返回成功响应
return Result.ok();
}
步骤二:改写登录具体逻辑
1️⃣ 获取用户提交表单的信息 LoginForm
i. 匹配当前提交的手机号码格式是否正确 ? “继续下步逻辑” : “返回错误信息”
ii. 通过用户提交的 手机号码 与 Redis常量 进行拼接 获取得到 key 进行查找
1. 判断当前获取得到的value 是否不为空 ? “继续下步逻辑” : “返回错误信息”
2️⃣ 通过 用户 提交的手机号码 到 数据库中 查找对应数据
i. 如果用户不存在 则 进行快速创建一个用户
ii. 存在 执行下步逻辑
3️⃣ 随机生成 token 作为登录令牌 并将其作为 key 保存到 redis 当中
i. 将之前获取得到的 User 对象转换为不包含敏感数据的 UserDTO 然后再转换为 HashMap 进行存储
ii. 存储 以 Redis常量 + token 为 Key ,以 UserMap 作为数据存储
4️⃣ 防止大量 k-v 长时间占用内存空间 所以需要设置 token 有效期
5️⃣ 返回 token
/** * 功能描述 * 登录功能的具体实现类 登录参数,包含手机号、验证码;或者手机号、密码 * @date 2022/7/4 * @author Alascanfu */
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 首先拿到用户请求发送过来的 验证码 进行和 session 中对应的验证码是否匹配
String code = loginForm.getCode();
String phone = loginForm.getPhone();
// TODO 从 Redis 中获取验证码 并且进行校验
String cacheCode = stringRedisTemplate.opsForValue()
.get(RedisConstants.LOGIN_CODE_KEY + phone);
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())){
return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
}
if (cacheCode == null || !cacheCode.equals(code)){
return Result.fail("对不起,您输入的验证码有误,请重试!");
}
// 如果匹配则进行验证用户名输入的手机号是否是之前已经注册过的
User user = userMapper.selectUserByPhone(phone);
if (user == null){
// 反之如果没有注册过, 则快速帮用户注册一个 用户账户 填充一些默认信息
user = createUserWithPhone(phone);
}
// 将快速注册好的用户|对应登录成功的用户, 加入到当前 session 当中
// session.setAttribute(SystemConstants.SESSION_USER,user);
// TODO 将用户信息 保存到 Redis 中 1=> 随机生成 token
// 2=>将User对象转换为 Hash 进行存储
// 3=>存储数据
// 4=>设置 token 有效期
String token = UUID.randomUUID().toString(true);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL ,TimeUnit.SECONDS);
// 将 token 返回给浏览器
return Result.ok(token);
}
因为我们想要保证用户每次操作之后都会重新更新 token 凭证的时间,为避免出现定时剔除token凭证所以我们需要在拦截器中追加对应的逻辑,保证用户在每次请求之后都会更新 token凭证的存活时间
步骤三:改写登录校验逻辑
1️⃣ 首先从 请求头中获取用户携带的 token 数据信息
2️⃣ 通过获取得到的 token 信息与 Redis常量 拼接 从 Redis 中获取对应的用户数据
3️⃣ 将从 Redis 中获取得到的 Map 数据转换为 UserDTO 类型数据
4️⃣ 将 UserDTO 数据 添加到当前线程局部变量当中
5️⃣ 刷新 对应用户的 token 凭证过期时间
6️⃣ 放行
/*** * @author: Alascanfu * @date : Created in 2022/7/4 20:21 * @description: LoginInterceptor * @modified By: Alascanfu **/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate ;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 先通过 request 获取 session
// 1.获取请求头中的 token
HttpSession session = request.getSession();
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
response.setStatus(401);
return false ;
}
// 然后通过 session 尝试获取 用户 信息
// 2.通过 token 从 Redis 中获取用户信息
// User user = (User) session.getAttribute(SystemConstants.SESSION_USER);
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
if (userMap.isEmpty()){
// 如果 user 不存在 则返回错误信息 对请求拦截 设置返回的状态码为 未授权
response.setStatus(401);
return false ;
}
// 3.将 查询得到的 Hash 数据类型转换为 UserDTO 对象
// 如果存在 user 将其保存到 threadLocal 当中
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
// 4. 保存用户信息到 ThreadLocal 当中
UserHolder.saveUser(userDTO);
// 7. 刷新 token 有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token , RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
// 8. 放行
return true;
}
/** User 类型 转换为 UserDTO */
private UserDTO userToUserDTO(User user) {
UserDTO userDTO = new UserDTO();
userDTO.setId(user.getId());
userDTO.setIcon(user.getIcon());
userDTO.setNickName(user.getNickName());
return userDTO;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 当请求完成之后 清除掉对应 threadLoacl 中的 user 数据即可
UserHolder.removeUser();
}
}
步骤四:进行对应的测试
启动好程序之后我们来到用户登录界面,进行用户对应的登录,然后查看对应的 Redis 库中是否已经 有了 数据。
错误:java.lang.Long cannot be cast to java.lang.String
2022-07-05 00:36:29.592 ERROR 24500 — [nio-8081-exec-5] com.hmdp.config.WebExceptionAdvice : java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:36) ~[spring-data-redis-2.3.9.RELEASE.jar:2.3.9.RELEASE]
=> 这里报出了 类型转化错误,主要是 StringRedisSerializer 这个序列化对象时导致的。
=> 根据提示找到对应出错的代码错误处
解决方案
改写对应的转换逻辑
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->{
return fieldValue.toString();
}));
重新测试
登录拦截器的优化
步骤一:编写新的全局放行拦截器 RefreshTokenInterceptor
/*** * @author: Alascanfu * @date : Created in 2022/7/5 1:07 * @description: RefreshTokenInterceptor * @modified By: Alascanfu **/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate ;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取 token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
return true ;
}
// 2. 查询对应的 Redis 用户
String tokenKey = RedisConstants.LOGIN_USER_KEY + token ;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
if (userMap.isEmpty()){
return true ;
}
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 3. 保存到 ThreadLocal
UserHolder.saveUser(userDTO);
// 4. 刷新 token 有效期
stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
// 5. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
步骤二:改写优化 LoginInterceptor
/*** * @author: Alascanfu * @date : Created in 2022/7/4 20:21 * @description: LoginInterceptor * @modified By: Alascanfu **/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO 判断是否需要拦截 判断 ThreadLocal 是否存在用户
if (UserHolder.getUser() == null){
response.setStatus(401);
return false ;
}
// 有用户就放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 当请求完成之后 清除掉对应 threadLoacl 中的 user 数据即可
UserHolder.removeUser();
}
}
步骤三:改写 WebMvc 配置类进行对应的拦截器配置
/*** * @author: Alascanfu * @date : Created in 2022/7/4 20:36 * @description: Web MVC configuration * @modified By: Alascanfu **/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor ;
@Autowired
private RefreshTokenInterceptor refreshTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(refreshTokenInterceptor)
.addPathPatterns("/**");
registry.addInterceptor(loginInterceptor)
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
步骤四:启动应用程序并打开浏览器进行测试
刷新回到首页页面再来看是否刷新了 token 的凭证时间
课后知识点 —— 所要了解掌握的面试题
边栏推荐
- P3500 [POI2010]TES-Intelligence Test(二分&离线)
- Codeforces Round #770 (Div. 2) B. Fortune Telling
- P2102 floor tile laying (DFS & greed)
- Lora gateway Ethernet transmission
- Hashlimit rate control
- Can Flink SQL read multiple topics at the same time. How to write in with
- Several important classes in unity
- Global and Chinese markets for patent hole oval devices 2022-2028: Research Report on technology, participants, trends, market size and share
- hashlimit速率控制
- Lambda expression learning
猜你喜欢
Patent | subject classification method based on graph convolution neural network fusion of multiple human brain maps
How does computer nail adjust sound
Stack and queue
When debugging after pycharm remote server is connected, trying to add breakpoint to file that does not exist: /data appears_ sda/d:/segmentation
The value of two date types is subtracted and converted to seconds
What is the difference between gateway address and IP address in tcp/ip protocol?
Record an excel xxE vulnerability
View 工作流程
Query the number and size of records in each table in MySQL database
How many of the 10 most common examples of istio traffic management do you know?
随机推荐
QML和QWidget混合开发(初探)
IDEA编译JSP页面生成的class文件路径
Sorting out the latest Android interview points in 2022 to help you easily win the offer - attached is the summary of Android intermediate and advanced interview questions in 2022
[Zhao Yuqiang] deploy kubernetes cluster with binary package
729. 我的日程安排表 I(set or 动态开点线段树)
2327. 知道秘密的人数(递推)
hashlimit速率控制
10個 Istio 流量管理 最常用的例子,你知道幾個?
How to execute an SQL statement in MySQL
Can Flink SQL read multiple topics at the same time. How to write in with
Le compte racine de la base de données MySQL ne peut pas se connecter à distance à la solution
Record an excel xxE vulnerability
Cross domain and jsonp details
Mixed development of QML and QWidget (preliminary exploration)
About some basic DP -- those things about coins (the basic introduction of DP)
解决“C2001:常量中有换行符“编译问题
[tomato assistant installation]
Mysql database storage engine
Understanding of processes, threads, coroutines, synchronization, asynchrony, blocking, non blocking, concurrency, parallelism, and serialization
Unity中几个重要类