面向微服务的Spring Cloud Gateway的集成解决方案:用户登录认证与访问控制
🎯导读:本文档详细描述了一个基于Spring Cloud Gateway的微服务网关及Admin服务的实现。网关通过定义路由规则,利用负载均衡将请求转发至不同的后端服务,并集成了Token验证过滤器以确保API的安全访问,同时支持白名单路径免验证。Admin服务负责用户管理,包括注册、登录、登出等功能,采用布隆过滤器优化用户名存在性检查,使用Redis存储会话信息并结合JWT进行身份验证。此外,文档还介绍了ShardingSphere的数据分片与加密配置,以及用户上下文在请求链路中的传递机制,确保了跨服务调用时用户信息的一致性和安全性。
文章目录
- 工具类
- Jwt工具
- 网关服务
- 依赖
- common
- 网关访问状态码
- 获取白名单url
- token校验过滤器
- 启动类
- 配置文件
- Admin服务
- 数据库表
- 依赖
- 枚举类
- 配置类
- 用户名布隆过滤器
- 用户上下文过滤器
- controller
- service
- impl
- 用户上下文封装
- 配置文件
- 数据源 (Data Sources)
- 分片规则 (Sharding Rules)
- 加密规则 (Encrypt Rules)
- 属性 (Props)
- 其他服务
工具类
Jwt工具
package com.vrs.utils;import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;import java.util.Date;/*** 生成JSON Web Token的工具类*/
public class JwtUtil {/*** JWT的默认过期时间,单位为毫秒。这里设定为30天*/private static final long tokenExpiration = 30L * 24L * 60L * 60L * 1000L;/*** 在实际应用中,应使用随机生成的字符串*/private static String tokenSignKey = "dsahdashoiduasguiewu23114";/*** 从给定的JWT令牌中提取指定参数名对应的值。** @param token 需要解析的JWT令牌字符串* @param paramName 要提取的参数名* @return 参数值(字符串形式),如果令牌为空、解析失败或参数不存在,则返回null*/public static String getParam(String token, String paramName) {try {if (StringUtils.isEmpty(token)) {return null;}// 使用提供的密钥解析并验证JWTJws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);// 获取JWT的有效载荷(claims),其中包含了所有声明(参数)Claims claims = claimsJws.getBody();// 提取指定参数名对应的值Object param = claims.get(paramName);// 如果参数值为空,则返回null;否则将其转换为字符串并返回return param == null ? null : param.toString();} catch (ExpiredJwtException e) {throw new RuntimeException("token过期了,需要重新登录");}}/*** 根据用户信息生成一个新的JWT令牌。** @param userId* @param username* @return*/public static String createToken(Long userId, String username, int userType) {// 使用Jwts.builder()构建JWTDate expiration = new Date(System.currentTimeMillis() + tokenExpiration);String token = Jwts.builder()// 设置JWT的主题(subject),此处为常量"AUTH-USER".setSubject("AUTH-USER")// 设置过期时间,当前时间加上预设的过期时间(tokenExpiration).setExpiration(expiration)// 有效载荷.claim("userId", userId).claim("userName", username).claim("userType", userType)// 使用HS512算法和指定密钥对JWT进行加密.signWith(SignatureAlgorithm.HS512, tokenSignKey)// 使用GZIP压缩算法压缩JWT字符串,将字符串变成一行来显示.compressWith(CompressionCodecs.GZIP)// 完成构建并生成紧凑格式的JWT字符串.compact();return token;}public static String getUserId(String token) {return getParam(token, "userId");}public static String getUsername(String token) {return getParam(token, "userName");}public static int getUserType(String token) {return Integer.parseInt(getParam(token, "userType"));}}
网关服务
依赖
<dependency><groupId>com.vrs</groupId><artifactId>vrs-common</artifactId>
</dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-loadbalancer</artifactId>
</dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
common
网关访问状态码
package com.vrs.common;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @Author dam* @create 2024/11/16 18:15*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GatewayResult {/*** HTTP 状态码*/private Integer status;/*** 返回信息*/private String message;
}
获取白名单url
package com.vrs.common;import lombok.Data;import java.util.List;/*** @Author dam* @create 2024/11/16 18:13*/
@Data
public class WhitePathConfig {/*** 白名单前置路径*/private List<String> whitePathList;
}
token校验过滤器
该过滤器的作用是:
- 路径白名单检查:
- 在处理请求之前,它会检查请求的 URL 路径是否在配置的白名单路径列表中。如果请求路径匹配白名单中的任意路径,则直接放行,不需要进行后续的令牌验证。
- JWT 验证:
- 对于不在白名单中的请求路径,它会从请求头中提取名为
token
的 JWT。 - 使用
JwtUtil.getUsername(token)
方法从 JWT 中解析出用户名(假设 JWT 中包含用户名信息)。 - 如果用户名和令牌都存在,它会尝试从 Redis 缓存中查找与用户名和令牌对应的用户信息。
- 对于不在白名单中的请求路径,它会从请求头中提取名为
- 用户信息添加到请求头:
- 如果用户信息存在于 Redis 中,它会将用户的 ID、类型和名称等信息添加到原始请求头中。这样做的目的是为了简化下游服务的逻辑,使得它们可以直接从请求头中获取用户信息,而无需再次查询数据库或缓存。
- 错误响应:
- 如果用户名为空,或者提供的令牌无效(即不存在于 Redis 中),则返回 401 未授权状态码,并附带一条消息提示用户需要先登录。
- 请求继续:
- 如果一切验证通过,它会使用修改后的请求(带有额外的用户信息头)继续向下游服务转发。
package com.vrs.filter;import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.vrs.common.GatewayResult;
import com.vrs.common.WhitePathConfig;
import com.vrs.constant.RedisCacheConstant;
import com.vrs.utils.JwtUtil;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;/*** @Author dam* @create 2024/11/16 18:14*/
@Component
public class TokenValidateGatewayFilterFactory extends AbstractGatewayFilterFactory<WhitePathConfig> {private final StringRedisTemplate stringRedisTemplate;public TokenValidateGatewayFilterFactory(StringRedisTemplate stringRedisTemplate) {super(WhitePathConfig.class);this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic GatewayFilter apply(WhitePathConfig whitePathConfig) {return (exchange, chain) -> {ServerHttpRequest request = exchange.getRequest();// 获取请求路径String requestPath = request.getPath().toString();if (!isPathInWhiteList(requestPath, whitePathConfig.getWhitePathList())) {// --if-- 当前请求路径不在白名单中String token = request.getHeaders().getFirst("token");// 用户名为空,或者不存在于Redis中,返回错误提示ServerHttpResponse response = exchange.getResponse();String userName = "";try {userName = JwtUtil.getUsername(token);} catch (Exception e) {return writeResult(response, e.getMessage());}Object userInfo;if (StringUtils.hasText(userName) && StringUtils.hasText(token) &&(userInfo = stringRedisTemplate.opsForHash().get(RedisCacheConstant.USER_LOGIN_KEY + userName, token)) != null) {JSONObject userInfoJsonObject = JSON.parseObject(userInfo.toString());// 将解析出来的信息放到请求头中,避免上下文封装的时候还需要去查询一遍ServerHttpRequest.Builder builder = exchange.getRequest().mutate().headers(httpHeaders -> {httpHeaders.set("userId", userInfoJsonObject.getString("id"));httpHeaders.set("userType", userInfoJsonObject.getString("userType"));httpHeaders.set("userName", URLEncoder.encode(userInfoJsonObject.getString("userName"), StandardCharsets.UTF_8));});return chain.filter(exchange.mutate().request(builder.build()).build());}response.setStatusCode(HttpStatus.UNAUTHORIZED);return writeResult(response, "没有通过登录校验,请先登录");}return chain.filter(exchange);};}/*** 返回结果给前端* @param response* @param e* @return*/private static Mono<Void> writeResult(ServerHttpResponse response, String e) {return response.writeWith(Mono.fromSupplier(() -> {DataBufferFactory bufferFactory = response.bufferFactory();GatewayResult resultMessage = GatewayResult.builder().status(HttpStatus.UNAUTHORIZED.value()).message(e).build();;return bufferFactory.wrap(JSON.toJSONString(resultMessage).getBytes());}));}/*** 判断请求路径是否存在于白名单中** @param requestPath* @param whitePathList* @return*/private boolean isPathInWhiteList(String requestPath, List<String> whitePathList) {if (whitePathList.isEmpty()) {return false;}for (String whitePath : whitePathList) {if (isPathMatched(whitePath, requestPath) == true) {return true;}}return false;}/*** 检查给定的路径是否与whitePath模式匹配。** @param whitePath 定义的白名单路径模式* @param testPath 要校验的具体路径* @return 如果testPath匹配whitePath,则返回true;否则返回false。*/public boolean isPathMatched(String whitePath, String testPath) {// 去除路径两边的空白字符whitePath = whitePath.trim();testPath = testPath.trim();// 如果whitePath是以'**'结尾,则只检查前面的部分是否匹配if (whitePath.endsWith("/**")) {// 获取whitePath中除了'/**'之外的部分String prefix = whitePath.substring(0, whitePath.length() - 3);// 检查testPath是否以prefix开头return testPath.startsWith(prefix);}// 对于其他类型的模式,这里可以扩展更多的匹配规则// 但在这个例子中我们只处理'/webjars/**'这种简单的情况// 默认情况下,直接比较字符串是否完全相等return whitePath.equals(testPath);}}
启动类
package com.vrs;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;/*** @Author dam* @create 2024/11/15 16:22*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class VrsGatewayApplication {public static void main(String[] args) {SpringApplication.run(VrsGatewayApplication.class, args);}
}
配置文件
路由定义 (routes
)
配置文件中定义了三个路由规则,每个规则都有一个唯一的 id
、目标 URI、匹配条件 (predicates
) 和过滤器 (filters
)。
vrs-admin
路由uri: lb://vrs-admin
:使用负载均衡(lb://
)转发到名为vrs-admin
的服务。predicates: Path=/admin/**
:只有当请求路径以/admin/
开头时,才会匹配此路由。filters: TokenValidate
:应用名为TokenValidate
的过滤器,用于验证请求中的令牌。对于特定的白名单路径(如登录、注册等),不需要进行令牌验证。whitePathList
:定义一组白名单url,如果访问的是admin的这些接口,不需要经过token校验
vrs-venue
路由uri: lb://vrs-venue/venue/**
:转发到名为vrs-venue
的服务。predicates: Path=/venue/**
:匹配以/venue/
开头的请求路径。filters: TokenValidate
:同样应用TokenValidate
过滤器进行令牌验证。
vrs-order
路由uri: lb://vrs-order/order/**
:转发到名为vrs-order
的服务。predicates: Path=/order/**
:匹配以/order/
开头的请求路径。filters: TokenValidate
:应用TokenValidate
过滤器进行令牌验证。
server:port: 7049
spring:profiles:active: damMacapplication:name: vrs-gatewaycloud:gateway:routes:- id: vrs-adminuri: lb://vrs-adminpredicates:- Path=/admin/**filters:- name: TokenValidateargs:whitePathList:- /admin/user/v1/login- /admin/user/v1/has-username- /admin/user/v1/register- /admin/pic/- id: vrs-venueuri: lb://vrs-venuepredicates:- Path=/venue/**filters:- name: TokenValidate- id: vrs-orderuri: lb://vrs-orderpredicates:- Path=/order/**filters:- name: TokenValidate
【application-damMac.yml】
spring:data:redis:host: 127.0.0.1port: 6379password: 12345678cloud:nacos:discovery:server-addr: 127.0.0.1:8848
Admin服务
数据库表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` bigint NOT NULL COMMENT 'ID',`create_time` datetime,`update_time` datetime,`is_deleted` tinyint default 0 COMMENT '逻辑删除 0:没删除 1:已删除',`user_name` varchar(30) NOT NULL COMMENT '用户账号',`nick_name` varchar(30) NOT NULL COMMENT '用户昵称',`user_type` tinyint NULL DEFAULT 2 COMMENT '用户类型 0:系统管理员 1:机构管理员 2:普通用户',`email` varchar(50) NULL DEFAULT '' COMMENT '用户邮箱',`phone_number` varchar(11) NULL DEFAULT '' COMMENT '手机号码',`gender` tinyint NULL DEFAULT 2 COMMENT '用户性别(0男 1女 2未知)',`avatar` varchar(100) NULL DEFAULT '' COMMENT '头像地址',`password` varchar(100) NULL DEFAULT '' COMMENT '密码',`status` tinyint NULL DEFAULT 0 COMMENT '帐号状态(0正常 1停用)',`login_ip` varchar(128) NULL DEFAULT '' COMMENT '最后登录IP',`login_date` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间',`point` int NULL DEFAULT NULL COMMENT '积分',`organization_id` bigint COMMENT '机构id,如果是机构管理员,必须填写;用户如果归属于某个机构,也要填写',PRIMARY KEY (`id`) USING BTREE
);-- 添加唯一约束
ALTER TABLE `user` ADD CONSTRAINT `uk_user_name` UNIQUE (`user_name`);
ALTER TABLE `user` ADD CONSTRAINT `uk_phone_number` UNIQUE (`phone_number`);
ALTER TABLE `user` ADD CONSTRAINT `uk_email` UNIQUE (`email`);
依赖
<dependency><groupId>com.vrs</groupId><artifactId>vrs-common</artifactId>
</dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId>
</dependency><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc-core</artifactId>
</dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId>
</dependency>
枚举类
package com.vrs.common.enums;import com.vrs.convention.errorcode.IErrorCode;/*** 用户错误码*/
public enum ErrorCodeEnum implements IErrorCode {// ------------------- 用户相关 -------------------USER_NULL("200", "用户记录不存在"),USER_NAME_EXIST("201", "用户名已存在"),USER_EXIST("202", "用户记录已存在"),USER_SAVE_ERROR("203", "用户记录新增失败"),USER_TOKEN_EXPIRE("204", "用户登录状态过期,请重新登录"),USER_TOKEN_FAIL("205", "用户token异常,请重新登录"),;private final String code;private final String message;ErrorCodeEnum(String code, String message) {this.code = code;this.message = message;}@Overridepublic String code() {return code;}@Overridepublic String message() {return message;}
}
配置类
用户名布隆过滤器
配置一个用户名的布隆过滤器
package com.vrs.config;import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @Author dam* @create 2024/11/16 11:58*/
@Configuration(value = "rBloomFilterConfigurationByAdmin")
public class RBloomFilterConfiguration {/*** 防止用户注册查询数据库的布隆过滤器*/@Beanpublic RBloomFilter<String> userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) {RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("vrs:userRegisterCachePenetrationBloomFilter");// 参数1:预估布隆过滤器里面要存放多少个元素// 参数2:误判率(误判率越低,散列函数数量越多)cachePenetrationBloomFilter.tryInit(100000000L, 0.001);return cachePenetrationBloomFilter;}
}
用户上下文过滤器
将UserTransmitFilter的优先级设置为最高
package com.vrs.config;import com.vrs.common.context.UserTransmitFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;/*** 配置使用用户过滤器* @Author dam* @create 2024/11/16 16:09*/
@Configuration
public class UserConfiguration {/*** 用户信息传递过滤器*/@Beanpublic FilterRegistrationBean<UserTransmitFilter> globalUserTransmitFilter(StringRedisTemplate stringRedisTemplate) {FilterRegistrationBean<UserTransmitFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new UserTransmitFilter());registration.addUrlPatterns("/*");registration.setOrder(0);return registration;}}
controller
package com.vrs.controller;import com.vrs.convention.result.Result;
import com.vrs.convention.result.Results;
import com.vrs.domain.dto.req.UserLoginReqDTO;
import com.vrs.domain.dto.req.UserRegisterReqDTO;
import com.vrs.domain.dto.resp.UserLoginRespDTO;
import com.vrs.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;/*** 用户管理控制层*/
@RestController
@RequestMapping("/user/")
@RequiredArgsConstructor
public class UserController {private final UserService userService;/*** 查询用户名是否存在*/@GetMapping("/v1/has-username")public Result<Boolean> hasUsername(@RequestParam("username") String username) {return Results.success(userService.hasUsername(username));}/*** 注册用户*/@PostMapping("/v1/register")public Result<Void> register(@RequestBody UserRegisterReqDTO requestParam) {userService.register(requestParam);return Results.success();}/*** 用户登录*/@PostMapping("/v1/login")public Result<UserLoginRespDTO> login(@RequestBody UserLoginReqDTO requestParam) {return Results.success(userService.login(requestParam));}/*** 用户退出登录** @return*/@DeleteMapping("/v1/logout")public Result<Void> logout(HttpServletRequest request) {String token = request.getHeader("token");userService.logout(token);return Results.success();}}
service
package com.vrs.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.vrs.domain.dto.req.UserLoginReqDTO;
import com.vrs.domain.dto.req.UserRegisterReqDTO;
import com.vrs.domain.dto.resp.UserLoginRespDTO;
import com.vrs.domain.entity.UserDO;/**
* @author dam
* @description 针对表【user】的数据库操作Service
* @createDate 2024-11-15 16:52:24
*/
public interface UserService extends IService<UserDO> {/*** 注册用户** @param requestParam 注册用户请求参数*/void register(UserRegisterReqDTO requestParam);/*** 用户登录** @param requestParam 用户登录请求参数* @return 用户登录返回参数 Token*/UserLoginRespDTO login(UserLoginReqDTO requestParam);/*** 查询用户名是否存在** @param username 用户名* @return 用户名存在返回 True,不存在返回 False*/Boolean hasUsername(String username);/*** 注销用户登录* @param token*/void logout(String token);
}
impl
- 用户注册 (
register
方法):- 使用布隆过滤器快速检查用户名是否已存在,以防止缓存穿透。
- 如果用户名不存在,则尝试获取一个基于用户名的分布式锁来确保并发环境下的数据一致性。
- 将用户信息保存到数据库中,并在成功后更新布隆过滤器。
- 如果数据库插入过程中发生唯一索引冲突(即用户名重复),则抛出异常通知客户端用户名已存在。
- 如果未能获取锁,假设其他进程正在处理相同用户名的注册请求,并认为注册将会成功,因此直接告知客户端用户名已存在。
注意
:这个注册只是最简单的注册,建议加上手机验证码或者邮箱验证码,然后给用户进行绑定,否则一个人可以注册大量的账号
- 检查用户名是否存在 (
hasUsername
方法):- 利用布隆过滤器快速判断用户名是否存在。由于布隆过滤器可能会产生误判(即假阳性),所以这里的逻辑是:如果布隆过滤器返回不存在,则可以确定用户名确实不存在;如果返回存在,虽然可能是误判,但是不管了,大不了这个用户名不让用户用。
- 用户登录 (
login
方法):- 根据提供的用户名和密码查询数据库,验证用户的身份。
- 检查用户状态,如果账户被停用,则拒绝登录。
- 检查用户是否已经登录,如果是,则刷新会话的有效时间并返回现有的 token。
- 如果用户未登录,则生成一个新的 JWT token 并将其存储在 Redis 中,同时设置过期时间。
- 返回包含 token 的响应给客户端。
- 用户登出 (
logout
方法):- 从 Redis 中删除与指定 token 相关的用户会话信息,实现用户的登出操作。
package com.vrs.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vrs.convention.exception.ClientException;
import com.vrs.domain.dto.req.UserLoginReqDTO;
import com.vrs.domain.dto.req.UserRegisterReqDTO;
import com.vrs.domain.dto.resp.UserLoginRespDTO;
import com.vrs.domain.entity.UserDO;
import com.vrs.mapper.UserMapper;
import com.vrs.service.UserService;
import com.vrs.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.vrs.common.enums.ErrorCodeEnum.*;
import static com.vrs.constant.RedisCacheConstant.LOCK_USER_REGISTER_KEY;
import static com.vrs.constant.RedisCacheConstant.USER_LOGIN_KEY;/*** @author dam* @description 针对表【user】的数据库操作Service实现* @createDate 2024-11-15 16:52:24*/
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO>implements UserService {private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter;private final RedissonClient redissonClient;private final StringRedisTemplate stringRedisTemplate;private static final long EXPIRE_TIME = 300L;private static final TimeUnit EXPIRE_TIME_UNIT = TimeUnit.HOURS;@Overridepublic void register(UserRegisterReqDTO requestParam) {// 开始注册之前,判断用户名有没有被注册if (hasUsername(requestParam.getUserName())) {// --if-- 用户名已经存在了,抛异常throw new ClientException(USER_NAME_EXIST);}// 使用Redisson的分布式锁,有看门狗机制,底层使用Netty来实现,网络通讯更加高效// LOCK_USER_REGISTER_KEY + requestParam.getUsername():只锁注册的用户名RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUserName());try {if (lock.tryLock()) {try {// 将用户数据保存到数据库UserDO userDO = BeanUtil.toBean(requestParam, UserDO.class);userDO.setNickName(userDO.getUserName());int inserted = baseMapper.insert(userDO);if (inserted < 1) {throw new ClientException(USER_SAVE_ERROR);}// 保存成功,将注册成功的用户名保存到布隆过滤器userRegisterCachePenetrationBloomFilter.add(requestParam.getUserName());} catch (DuplicateKeyException ex) {// 数据库唯一索引异常(按理说这个是不会执行)throw new ClientException(USER_EXIST);}} else {// --if-- 没有获取到锁,说明有其他用户正在注册,大概率注册都会成功的,返回用户名已经存在throw new ClientException(USER_NAME_EXIST);}} finally {lock.unlock();}}/*** 直接用布隆过滤器判断用户名是否存在* - 布隆过滤器不存在,说明肯定不存在* - 布隆过滤器存在,可能产生误判,但是问题不大,部分用户名用不了也没啥关系** @param username 用户名* @return*/@Overridepublic Boolean hasUsername(String username) {return userRegisterCachePenetrationBloomFilter.contains(username);}@Overridepublic UserLoginRespDTO login(UserLoginReqDTO requestParam) { 根据用户名密码查询,看看有没有匹配的用户LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class).eq(UserDO::getUserName, requestParam.getUserName()).eq(UserDO::getPassword, requestParam.getPassword()).eq(UserDO::getIsDeleted, 0);UserDO userDO = baseMapper.selectOne(queryWrapper);if (userDO == null) {throw new ClientException("用户不存在或者密码错误");}if (userDO.getStatus() != 0) {throw new ClientException("该账号已经停用");} 判断用户之前有没有登录,如果登录了直接返回token即可,防止有人一直刷接口Map<Object, Object> hasLoginMap = stringRedisTemplate.opsForHash().entries(USER_LOGIN_KEY + requestParam.getUserName());if (CollUtil.isNotEmpty(hasLoginMap)) {// 用户又登录了,刷新过期时间stringRedisTemplate.expire(USER_LOGIN_KEY + requestParam.getUserName(), EXPIRE_TIME, EXPIRE_TIME_UNIT);// 如果已经登录,返回缓存的tokenString token = hasLoginMap.keySet().stream().findFirst().map(Object::toString).orElseThrow(() ->// token为空new ClientException("用户登录错误"));try {JwtUtil.getUserId(token);return new UserLoginRespDTO(token);} catch (Exception e) {// --if-- 如果抛异常,说明token有问题,可能过期了,需要继续执行下面的流程来重新生成token}} 存储用户信息// 使用jwt创建tokenString token = JwtUtil.createToken(userDO.getId(), userDO.getUserName(), userDO.getUserType());// 将生成的token和用户信息存储到redis里面stringRedisTemplate.opsForHash().put(USER_LOGIN_KEY + requestParam.getUserName(), token, JSON.toJSONString(userDO));// 设置过期时间stringRedisTemplate.expire(USER_LOGIN_KEY + requestParam.getUserName(), EXPIRE_TIME, EXPIRE_TIME_UNIT);return new UserLoginRespDTO(token);}/*** 用户退出登录* @param token*/@Overridepublic void logout(String token) {// 拦截器已经帮我验证了token的有效性,直接删除缓存即可String username = JwtUtil.getUsername(token);stringRedisTemplate.delete(USER_LOGIN_KEY + username);}
}
当然这里实现的是最简单的登录、注册功能,如果说需要添加验证码验证,请改写实现方法,这里提供一个简单的思路
- 前端首先发起一个请求,获取验证码,后台可以用uuid分发一个编号给前端,并将编号和验证码存储到Redis中,设置一个过期时间,例如60s
- 前端输入验证码之后进行登录或注册,同时需要带上的上面的随机编号,方便后台核验验证码是否正确
- 后台验证
- 如果前端上传的验证码编号在Redis中找不到,返回验证码失效
- 如果前端输入的验证码与后台存储的验证码不匹配,返回验证码错误,让用户重新输入
后面还可以进一步限制用户获取验证码的频率,防范恶意攻击
用户上下文封装
【UserInfoDTO】
package com.vrs.common.context;import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @Author dam* @create 2024/11/16 16:02*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserInfoDTO {/*** 用户 ID*/@JSONField(name = "id")private String userId;/*** 用户名*/private String userName;/*** 用户类型 0:系统管理员 1:机构管理员 2:普通用户*/private Integer userType;
}
【UserContext】 这个类使用了 TransmittableThreadLocal
类来存储用户信息,这允许用户信息不仅可以在同一个线程中传递,而且可以跨线程传递(例如,在异步任务或新线程中仍然可以访问到原始线程的用户信息)。TransmittableThreadLocal
是阿里巴巴开源的一个库,旨在解决 Java 中 ThreadLocal
无法在线程切换时传递数据的问题。
package com.vrs.common.context;import com.alibaba.ttl.TransmittableThreadLocal;import java.util.Optional;/*** @Author dam* @create 2024/11/16 16:01*/
public final class UserContext {/*** <a href="https://github.com/alibaba/transmittable-thread-local" />*/private static final ThreadLocal<UserInfoDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();/*** 设置用户至上下文** @param user 用户详情信息*/public static void setUser(UserInfoDTO user) {USER_THREAD_LOCAL.set(user);}/*** 获取上下文中用户 ID** @return 用户 ID*/public static String getUserId() {UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserId).orElse(null);}/*** 获取上下文中用户名称** @return 用户名称*/public static String getUsername() {UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserName).orElse(null);}/*** 获取上下文中用户名称** @return 用户名称*/public static Integer getUserType() {UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserType).orElse(null);}/*** 清理用户上下文*/public static void removeUser() {USER_THREAD_LOCAL.remove();}
}
【UserTransmitFilter】
过滤器,用来获取网关服务放行之后的请求的请求头上面的信息,并设置到用户上下文中
package com.vrs.common.context;import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;/*** @Author dam* @create 2024/11/16 16:10*/
@RequiredArgsConstructor
public class UserTransmitFilter implements Filter {@SneakyThrows@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;String username = httpServletRequest.getHeader("userName");if (StringUtil.isNotBlank(username)) {// --if-- username不为空,说明是经过了过滤器的String userId = httpServletRequest.getHeader("userId");Integer userType = Integer.parseInt(httpServletRequest.getHeader("userType"));UserContext.setUser(new UserInfoDTO(userId, username, userType));}try {filterChain.doFilter(servletRequest, servletResponse);} finally {// 不移除,会有内存泄漏风险UserContext.removeUser();}}
}
配置文件
【application.yml】
server:port: 7050servlet:context-path: /admin
spring:profiles:active: damMacapplication:name: vrs-admin
logging:level:org.springframework.web: DEBUGorg.springframework.web.servlet: DEBUG
【application-damMac.yml】
spring:cloud:nacos:discovery:server-addr: 127.0.0.1:8848datasource:# ShardingSphere 对 Driver 自定义,实现分库分表等隐藏逻辑driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver# ShardingSphere 配置文件路径url: jdbc:shardingsphere:classpath:shardingsphere-config-damMac.yamldata:redis:host: 127.0.0.1port: 6379password: 12345678database: 0timeout: 1800000jedis:pool:max-active: 20 #最大连接数max-wait: -1 #最大阻塞等待时间(负数表示没限制)max-idle: 5 #最大空闲min-idle: 0 #最小空闲
【shardingsphere-config-damMac.yaml】
这段配置是用于设置一个数据分片(Sharding)和加密的规则,它定义了如何在多个数据库表之间分配数据以及如何对特定字段的数据进行加密存储。以下是配置文件的详细解释:
数据源 (Data Sources)
ds_0
:定义了一个名为ds_0
的数据源,使用的是 HikariCP 连接池,连接到本地 MySQL 数据库venue-reservation
,端口为 3308。设置了字符编码、允许批量语句重写、允许多查询以及时区。
分片规则 (Sharding Rules)
- 分片策略 (
tableStrategy
):- 定义了一个分片策略,针对
user
表。根据user_name
字段进行哈希取模运算来决定数据应该插入到哪个物理表中。 actualDataNodes
指定了实际存在的数据节点,这里指出了user
表被分成了 16 个物理表,即user_0
到user_15
。shardingAlgorithmName
引用了分片算法user_table_hash_mod
。
- 定义了一个分片策略,针对
- 分片算法 (
shardingAlgorithms
):user_table_hash_mod
是一种基于哈希取模的分片算法,它会根据user_name
字段的哈希值对 16 取模,结果决定了该条记录应该存放在哪一个user_${0..15}
表中。
加密规则 (Encrypt Rules)
- 加密表 (
tables
):- 对
user
表中的phone_number
,email
, 和password
字段进行了加密配置。每个字段都指定了一个cipherColumn
,这是存储加密后数据的实际字段名,同时指定了相同的加密器common_encryptor
。 queryWithCipherColumn
设置为true
表示在查询时也可以使用加密后的字段。
- 对
- 加密器 (
encryptors
):common_encryptor
使用的是 AES 加密算法,并提供了一个 AES 密钥aes-key-value
用于加密和解密操作。
属性 (Props)
sql-show: true
表示开启 SQL 显示功能,当执行 SQL 语句时,框架会打印出实际执行的 SQL 语句,这有助于调试。
# 数据源集合
dataSources:ds_0:dataSourceClassName: com.zaxxer.hikari.HikariDataSourcedriverClassName: com.mysql.cj.jdbc.DriverjdbcUrl: jdbc:mysql://127.0.0.1:3308/venue-reservation?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: 12345678 rules:- !SHARDINGtables:user:# 真实数据节点,比如数据库源以及数据库在数据库中真实存在的actualDataNodes: ds_0.user_${0..15}# 分表策略tableStrategy:# 用于单分片键的标准分片场景standard:# 分片键shardingColumn: user_name# 分片算法,对应 rules[0].shardingAlgorithmsshardingAlgorithmName: user_table_hash_mod# 分片算法shardingAlgorithms:# 数据表分片算法 使用的分片算法,根据数据的hashcode来进行取模(根据上面的配置知道是mod 16),值是多少就被分配到哪个表中user_table_hash_mod:# 根据分片键 Hash 分片type: HASH_MOD# 分片数量props:sharding-count: 16# 展现逻辑 SQL & 真实 SQL# 逻辑SQL:select * from t_user where username = 'admin'# 真实SQL:select * from t_user_0 where username = 'admin'# 数据加密存储规则- !ENCRYPT# 需要加密的表集合tables:# 用户表user:# 用户表中哪些字段需要进行加密columns:# 手机号字段,逻辑字段,不一定是在数据库中真实存在phone_number:# 手机号字段存储的密文字段,这个是数据库中真实存在的字段cipherColumn: phone_number# 身份证字段加密算法encryptorName: common_encryptoremail:cipherColumn: emailencryptorName: common_encryptorpassword:cipherColumn: passwordencryptorName: common_encryptor# 是否按照密文字段查询queryWithCipherColumn: true# 加密算法encryptors:# 自定义加密算法名称common_encryptor:# 加密算法类型# AES 可逆type: AESprops:# AES 加密密钥,密钥千万不能泄露,不然拿到数据就可以破解了aes-key-value: dadh423h343hg
props:sql-show: true
其他服务
其他如果需要使用到用户上下文,也需要添加以下几个类
相关文章:
面向微服务的Spring Cloud Gateway的集成解决方案:用户登录认证与访问控制
🎯导读:本文档详细描述了一个基于Spring Cloud Gateway的微服务网关及Admin服务的实现。网关通过定义路由规则,利用负载均衡将请求转发至不同的后端服务,并集成了Token验证过滤器以确保API的安全访问,同时支持白名单路…...
MongoDB 更新文档
关于MongoDB更新文档的操作,可以通过多种方法实现。以下是一些常用的方法: updateOne() 方法:用于更新匹配过滤器的单个文档。其语法为 db.collection.updateOne(filter, update, options)。其中,filter 用于查找文档的查询条件&a…...
静态路由与动态路由
静态路由和动态路由是网络中两种不同的路由配置方式,它们在网络中的运作方式、配置方法以及适用场景等方面存在显著差异。以下是对两者的详细比较: 一、定义与配置方式 静态路由 定义:静态路由是由网络管理员手动配置的固定路径,…...
leetcode hot二叉树的层序遍历
102. 二叉树的层序遍历 已解答 中等 相关标签 相关企业 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点) # Definition for a binary tree node. # class TreeNode(object): # …...
Windows下ESP32-IDF开发环境搭建
Windows下ESP32-IDF开发环境搭建 文章目录 Windows下ESP32-IDF开发环境搭建一、软件安装二、搭建IDF开发环境2.1 安装VS Code插件:2.2 配置ESP-IDF插件:2.3 下载例程源码: 三、编译和烧录代码四、Windows下使用命令行编译和烧录程序4.1 配置环…...
基于高云GW5AT-15 FPGA的SLVS-EC桥MIPI设计方案分享
作者:Hello,Panda 一、设计需求 设计一个4Lanes SLVS-EC桥接到2组4lanes MIPI DPHY接口的电路模块: (1)CMOS芯片:IMX537-AAMJ-C,输出4lanes SLVS-EC 4.752Gbps Lane速率; (2&…...
【day18】多线程高级应用
day17回顾 在深入探讨模块18之前,让我们回顾一下【day17】中的关键内容: 创建多线程: 继承Thread类: 定义一个类,继承Thread。重写run方法,设置线程任务。创建自定义线程对象。调用start方法,开…...
Python接口自动化测试的实现
1)环境准备: 接口测试的方式有很多,比如可以用工具(jmeter,postman)之类,也可以自己写代码进行接口测试,工具的使用相对来说都比较简单,重点是要搞清楚项目接口的协议是什么,然后有针对性的进行选择&#x…...
nvidia docker, nvidia docker2, nvidia container toolkits区别
背景 在docker容器中用GPU时,查阅了网上许多教程,教程之间概念模糊不清,相互矛盾,过时的教程和新的教程混杂在一起。主要原因是Nvidia为docker容器的支持发生了好几代变更,api发生了不少变化。下面来总结一下各代支持…...
字节跳动Java开发面试题及参考答案(综合篇)
HTTP 与 HTTPS 的区别? HTTP(超文本传输协议)和 HTTPS(超文本传输安全协议)主要有以下区别。 从安全性角度看,HTTP 是明文传输协议,数据在网络中传输时是以原始文本的形式发送的。这就好比在信件传递过程中没有进行密封,任何中间节点(如路由器、代理服务器等)都可以查…...
搭建Elastic search群集
一、实验环境 二、实验步骤 Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎Elasticsearch目录文件: /etc/elasticsearch/elasticsearch.yml#配置文件 /etc/elasticsearch/jvm.options#java虚拟机 /etc/init.d/elasticsearch#服务启动脚本 /e…...
深入解析 Spring WebFlux:原理与应用
优质博文:IT-BLOG-CN WebFlux 是 Spring Framework 5 引入的一种响应式编程框架,和Spring MVC同级,旨在处理高并发和低延迟的非阻塞应用。这是一个支持反应式编程模型的新Web框架体系。 顺便一提,Spring Cloud Gateway在实现上是…...
Docker 部署 SpringBoot VUE项目
是一套基于若依的wms仓库管理系统 一、后端部署 后端地址:https://gitee.com/zccbbg/wms-ruoyi/tree/v1/ 1、用IDEA拉代码,并修改API统一后缀 2、复制一个配置文件 application-dev.yaml,并修改里面的mysql与redis配置 3、将打包的jar上传…...
【Java基础面试题031】Java运行时异常和编译时异常之间的区别是什么?
回答重点 主要有三大区别,分别是发生时机、捕获和处理方式和设计意图 1)发生时机: 编译时异常(Checked Exception):发生在编译阶段,编译器会检查此类异常,程序必须堆这些异常进行…...
常见网络功能概述-主要拆解功能
大家觉得有意义和参考价值记得关注和点赞!!! 一、防火墙介绍 防火墙(Firewall)是一种网络安全系统,用于监控、过滤和控制进出网络的数据流量。它是一种屏障,通过策略规则来允许、限制或拒绝数…...
Chapter 3-1. Detecting Congestion in Fibre Channel Fabrics
Chapter 3. Detecting Congestion in Fibre Channel Fabrics This chapter covers the following topics: 本章包括以下主题: Congestion detection workflow. Congestion detection metrics. Congestion detection metrics and commands on Cisco MDS switches. Automatic A…...
Day13 用Excel表体验梯度下降法
Day13 用Excel表体验梯度下降法 用所学公式创建Excel表 用Excel表体验梯度下降法 详见本Day文章顶部附带资源里的Excel表《梯度下降法》,可以对照表里的单元格公式进行理解,还可以多尝试几次不同的学习率 η \eta η来感受,只需要更改学习率…...
重温设计模式--5、职责链模式
文章目录 职责链模式的详细介绍C 代码示例C示例代码2 职责链模式的详细介绍 定义与概念 职责链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它旨在将请求的发送者和多个接收者解耦,让多个对象都有机会处理请求&am…...
C语言-08复合类型-结构体
一、结构体 1.结构体struct struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起,这种类型称为结构体类型。 2.使用步骤 第一步:创建或声明结构体 第二步:定义结构体变量 第三步:调用并操作结…...
vue 基础学习
一、ref 和reactive 区别 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body><div id"app"><h1>{{Web.title}}</h1><h1&…...
Elasticsearch检索方案之一:使用from+size实现分页
前面两篇文章介绍了elasticsearch以及Kibana的安装,检索引擎以及可视化工具都已经安装完成,接下来介绍下如何使用golang的sdk实现简单的分页查询。 1、下载Elastic官方golang sdk 在讲解elasticsearch检索之前,需要先把golang的环境安装好&…...
Highcharts 饼图:数据可视化利器
Highcharts 饼图:数据可视化利器 引言 在数据可视化的领域中,饼图作为一种经典且直观的图表类型,被广泛应用于各种行业和场景中。Highcharts,作为一个功能强大且易于使用的JavaScript图表库,为我们提供了创建交互式和…...
Docker部署Sentinel
一、简介 是什么:面向分布式、多语言异构化服务架构的流量治理组件 能干嘛:从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性 官网地址:https://sentinelguard.io/zh-c…...
后端接口设计
一、基本规范 1.URL设计 应遵循RESTful风格,使用动词名词的方式描述接口的功能。应简洁明了,易于理解和记忆。 2.请求协议及方法 使用HTTPS协议进行数据传输,保证数据在传输过程中的安全性。如无特殊情况,统一使用post方法。 …...
GitLab部署到阿里云服务器上
GitLab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的web服务。可通过Web界面进行访问公开的或者私人项目。它拥有与Github类似的功能,能够浏览源代码,管理缺陷和注释。 一、安装 1.创建一…...
GitLab的卸载与重装
目录 一、GitLab的卸载 二、 GitLab的安装与配置 1. 创建安装目录 2. 安装 3. 使用 3.1 初始化 3.2 创建空白项目 编辑 3.3 配置SSH 3.3.1 配置公钥 编辑 3.3.2 配置私钥 3.4 配置本地git库 一、GitLab的卸载 1. 停止gitlab sudo gitlab-ctl stop 2. 卸载…...
动态住宅IP适合哪些数据采集项目?
在数据采集的广阔天地中,动态住宅IP代理能够灵活地变换身份,帮助我们在网络世界中自由地穿梭。这种代理IP因其住宅性质和动态变化的特点,成为了许多数据采集项目的理想选择。今天,我们就来聊聊动态住宅IP代理适合哪些数据采集项目…...
Git_撤销本地commit_查找仓库中大文件
Gitee普通账号的仓库总空间限制为5G; 右上角头像,下拉—》设置/账号设置—》数据管理下的仓库空间信息即可查看空间限额和各仓库空间大小;Gitee普通账号每次推送大小不能超过100MB,否则会推送失败;当提交大小超过100MB…...
golang windows打包为linux可执行文件
使用go的交叉编译功能 set GOOSlinux set GOARCHamd64然后再执行go build 可能会报异常, 所以贴出我的go env配置仅供参考 go env环境配置 D:\GoWork\src\go-tzv>go env set GO111MODULEauto set GOARCHamd64 set GOBIN …...
源码分析之Openlayers中GeometryCollection类
概述 本文主要介绍GeometryCollection类,GeometryCollection类继承于Geometry类,关于Geometry类,参考这篇文章源码分析之Openlayers中Geometry基类介绍 GeometryCollection类就是一组几何对象的集合. 源码分析 GeometryCollection类源码实现 GeometryCollection类源码实现…...
*【每日一题 基础题】 [蓝桥杯 2024 省 B] 好数
[蓝桥杯 2024 省 B] 好数 好数 一个整数如果按从低位到高位的顺序,奇数位(个位、百位、万位……)上的数字是奇数,偶数位(十位、千位、十万位……)上的数字是偶数,我们就称之为“好数”。 给定一…...
Redis大Key问题全解析
1. 引言 1.1 什么是Redis大Key? Redis大Key是指单个Key对应的数据量过大,占用过多的内存或导致操作耗时较长的现象。大Key可以是以下几种常见数据类型中的任意一种: String类型:单个字符串的长度过大。List类型:包含…...
一起学Git【第六节:查看版本差异】
git diff是 Git 版本控制系统中用于展示差异的强大工具。他可以用于查看文件在工作区、暂存区和版本库之间的差异、任意两个指定版本之间的差异和两个分支之间的差异等,接下来进行详细的介绍。 1.显示工作区与暂存区之间的差异 # 显示工作区和暂存区之间的差异,后面不加参数…...
USB Hub 检测设备
系列文章目录 xHCI 简单分析 USB Root Hub 分析 USB Hub 检测设备 文章目录 系列文章目录一、引言二、hub_eventshub_port_connect_changeusb_alloc_devusb_set_device_statehub_port_initusb_new_device 一、引言 USB Hub 检测设备 一文中讲到,当有 USB 插入时&…...
Python 正则表达式
正则在线实用工具:regex101 正则表达式(regular expression)是一种用于匹配字符串中字符组合模式的工具。它可以用来检查一个字符串是否匹配某个模式、提取字符串中的信息、替换字符串中的某些部分等。 Python 的 re 模块提供了对正则表达式…...
【Mac】终端改色-让用户名和主机名有颜色
效果图 配置zsh 1.打开终端,进入.zshrc配置 cd ~ vim .zshrc2.添加如下配置并保存 # 启用命令行颜色显示 export CLICOLOR1 ## 加载颜色支持 autoload -U colors && colors # 配置 zsh 提示符 PROMPT"%{$fg_bold[red]%}%n%{$reset_color%}%{$fg_bol…...
React 前端框架入门
这里写目录标题 React 前端框架入门什么是 React?核心特性基本概念1. JSX2. 组件3. State 和 Props4. 生命周期5. React Hooks React 应用示例项目结构如何启动 React 项目参考资料 React 前端框架入门 什么是 React? React 是由 Facebook 开发并开源的…...
复习打卡大数据篇——Hadoop YARN
目录 1.什么是yarn 2.yarn的三大角色 3.任务(MR)提交到YARN运行流程 4. 调度器Scheduler 5.YARN HA 高可用 1.什么是yarn YARN(Yet Another Resource Negotiator)是一个资源管…...
03.HTTPS的实现原理-HTTPS的工作流程
03.HTTPS的实现原理-HTTPS的工作流程 简介1. HTTPS的工作流程1.1. TCP的工作流程1.1.1. 三次握手的详细步骤1.1.2. 三次握手的作用 1.2. HTTPS的工作流程1.2.1. HTTPS与TCP的关系1.2.2. HTTPS的工作流程 2. 公钥和私钥的作用3. 对称密钥的生成和交换4. 对称加密和非对称加密的区…...
idea部署maven项目步骤(图+文)
博主介绍:专注于Java(springboot ssm 等开发框架) vue .net php phython node.js uniapp 微信小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设,从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不…...
Eclipse 添加书签
Eclipse 添加书签 Eclipse 是一款非常受欢迎的集成开发环境(IDE),广泛用于 Java、C、Python 等语言的开发。在处理大型项目时,开发者通常需要在不同文件和代码行之间频繁切换。为了提高工作效率,Eclipse 提供了书签功…...
ROSboard:为您的机器人提供强大的Web可视化工具
ROSboard:为您的机器人提供强大的Web可视化工具 rosboard ROS node that turns your robot into a web server to visualize ROS topics [这里是图片001] 项目地址: https://gitcode.com/gh_mirrors/ro/rosboard 项目介绍 ROSboard 是一个专为机器人设计的 Web 服…...
InnoDB引擎的内存结构
InnoDB擅长处理事务,具有自动崩溃恢复的特性 架构图: 由4部分组成: 1.Buffer Pool:缓冲池,缓存表数据和索引数据,减少磁盘I/O操作,提升效率 2.change Buffer:写缓冲区,…...
使用RTP 协议 对 H264 封包和解包,h264的avpacket和NAL的关系
学习内容: 本章探讨如何将h264的 avpacket的视频 数据,通过RTP协议发送到 流媒体 服务器 或者 对端接受者。 前提 我们在将 YUV数据变成avframe后,通过h264 编码变成AVPacket,例如,在安防项目中,或者直播…...
面试经典问题 —— 最大/小前K个数问题(top - K)问题
目录 常见思路更优的解法(面试官喜欢的) 常见思路 要选出最小的前K个数首先我们会想到排排升序建大堆,排降序建小堆 一个直观的想法是使用(小根堆),起始将所有元素放入堆中,然后再从堆中取出k 个…...
HNUST-数据分析技术课堂实验
1.要求 1,从下列第一、二、三组实验中各至少选取一个算法进行实验,选修组实验不作强制要求;2,实验过程不限,目标在于锻炼算法实现过程,即可采用C、C、Java、Python(建议)等任意语言编…...
HEIC 是什么图片格式?如何把 iPhone 中的 HEIC 转为 JPG?
在 iPhone 拍摄照片时,默认的图片格式为 HEIC。虽然 HEIC 格式具有高压缩比、高画质等优点,但在某些设备或软件上可能存在兼容性问题。因此,将 HEIC 格式转换为更为通用的 JPG 格式就显得很有必要。本教程将介绍如何使用简鹿格式工厂…...
Next.js 14 性能优化:从首屏加载到运行时优化的最佳实践
在现代 Web 应用中,性能优化直接影响用户体验和业务转化。Next.js 14 提供了多种内置的性能优化特性,今天我们就来深入探讨如何充分利用这些特性,以及一些实用的优化技巧。 图片和字体优化 1. 图片优化 Next.js 的 Image 组件供了强大的图…...
T-SQL语言的软件开发工具
T-SQL:微软SQL Server的强大工具 引言 在现代软件开发中,数据的管理与操作是不可或缺的一部分,而T-SQL(Transact-SQL)作为微软SQL Server的重要组成部分,承担着数据查询、数据操作、数据插入和数据删除等…...
Ubuntu环境 nginx.conf详解(二)
1、nginx.conf 结构详解: http 块:用于配置 HTTP 服务器的相关设置,包括处理 HTTP 和 HTTPS。 stream 块:用于配置 TCP/UDP 代理服务器,适用于需要进行四层负载均衡的情况。 ... # 全局块 events {...} …...