面向微服务的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校验过滤器

该过滤器的作用是:

  1. 路径白名单检查:
    1. 在处理请求之前,它会检查请求的 URL 路径是否在配置的白名单路径列表中。如果请求路径匹配白名单中的任意路径,则直接放行,不需要进行后续的令牌验证。
  2. JWT 验证:
    1. 对于不在白名单中的请求路径,它会从请求头中提取名为 token 的 JWT。
    2. 使用 JwtUtil.getUsername(token) 方法从 JWT 中解析出用户名(假设 JWT 中包含用户名信息)。
    3. 如果用户名和令牌都存在,它会尝试从 Redis 缓存中查找与用户名和令牌对应的用户信息。
  3. 用户信息添加到请求头:
    1. 如果用户信息存在于 Redis 中,它会将用户的 ID、类型和名称等信息添加到原始请求头中。这样做的目的是为了简化下游服务的逻辑,使得它们可以直接从请求头中获取用户信息,而无需再次查询数据库或缓存。
  4. 错误响应:
    1. 如果用户名为空,或者提供的令牌无效(即不存在于 Redis 中),则返回 401 未授权状态码,并附带一条消息提示用户需要先登录。
  5. 请求继续:
    1. 如果一切验证通过,它会使用修改后的请求(带有额外的用户信息头)继续向下游服务转发。
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

  1. 用户注册 (register 方法):
    1. 使用布隆过滤器快速检查用户名是否已存在,以防止缓存穿透。
    2. 如果用户名不存在,则尝试获取一个基于用户名的分布式锁来确保并发环境下的数据一致性。
    3. 将用户信息保存到数据库中,并在成功后更新布隆过滤器。
    4. 如果数据库插入过程中发生唯一索引冲突(即用户名重复),则抛出异常通知客户端用户名已存在。
    5. 如果未能获取锁,假设其他进程正在处理相同用户名的注册请求,并认为注册将会成功,因此直接告知客户端用户名已存在。
    6. 注意:这个注册只是最简单的注册,建议加上手机验证码或者邮箱验证码,然后给用户进行绑定,否则一个人可以注册大量的账号
  2. 检查用户名是否存在 (hasUsername 方法):
    1. 利用布隆过滤器快速判断用户名是否存在。由于布隆过滤器可能会产生误判(即假阳性),所以这里的逻辑是:如果布隆过滤器返回不存在,则可以确定用户名确实不存在;如果返回存在,虽然可能是误判,但是不管了,大不了这个用户名不让用户用。
  3. 用户登录 (login 方法):
    1. 根据提供的用户名和密码查询数据库,验证用户的身份。
    2. 检查用户状态,如果账户被停用,则拒绝登录。
    3. 检查用户是否已经登录,如果是,则刷新会话的有效时间并返回现有的 token。
    4. 如果用户未登录,则生成一个新的 JWT token 并将其存储在 Redis 中,同时设置过期时间。
    5. 返回包含 token 的响应给客户端。
  4. 用户登出 (logout 方法):
    1. 从 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_0user_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

其他服务

其他如果需要使用到用户上下文,也需要添加以下几个类

在这里插入图片描述

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

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

相关文章

浅析InnoDB引擎架构(已完结)

大家好&#xff0c;我是此林。 今天来介绍下InnoDB底层架构。 1. 磁盘架构 我们所有的数据库文件都保存在 /var/lib/mysql目录下。 由于我这边是docker部署的mysql&#xff0c;用如下命令查看mysql数据挂载。 docker inspect mysql-master 如下图&#xff0c;目前只有一个数…

Ajax中的axios

既然提到Ajax&#xff0c;那就先来说一说什么是Ajax吧 关于Ajax Ajax的定义 Asynchronous JavaScript And XML&#xff1a;异步的JavaScript和XML。 反正就是一句话总结&#xff1a; 使用XML HttpRequest 对象与服务器进行通讯。 AJAX 是一种在无需重新加载整个网页的情况下&…

苹果手机怎么清理空间:拯救你的拥挤手机

在数字生活的海洋中&#xff0c;我们的苹果手机就像一艘小船&#xff0c;载满了照片、应用、视频和各种下载的“宝贝”。随着时间的推移&#xff0c;这艘小船开始变得拥挤&#xff0c;航行速度放缓&#xff0c;甚至有时候直接卡壳。苹果手机怎么清理空间&#xff1f;是时候学会…

三、使用langchain搭建RAG:金融问答机器人--检索增强生成

经过前面2节数据准备后&#xff0c;现在来构建检索 加载向量数据库 from langchain.vectorstores import Chroma from langchain_huggingface import HuggingFaceEmbeddings import os# 定义 Embeddings embeddings HuggingFaceEmbeddings(model_name"m3e-base")#…

C语言 函数嵌套

#include <stdio.h> void new_line() {printf("hehe\n"); } void three_line() {int i 0;for (i 0; i < 3; i){new_line;} } int main() {three_line();return 0; } 函数可以嵌套调用&#xff0c;但不能嵌套定义 链式访问 main有三个参数 //main函数的…

问题解决:发现Excel中的部分内容有问题。是否让我们尽量尝试恢复? 如果您信任此工作簿的源,请单击“是”。

在开发同步导出功能是遇到了如标题所示的问题&#xff0c;解决后遂记录下来供大家参考。 RestController public class XxxController {PostMapping("/export")public BaseResponse export(RequestBody PolicyErrorAnalysisExportReq exportReq, HttpServletRespons…

基于ST STM32MP257FAK3的MP2控制器之工业PLC 方案

简介 1.可编程逻辑控制器&#xff08;PLC&#xff09;是种专门为在工业环境下应用而设计的数字运算操作电子系统。它采用一种可编程的存储器&#xff0c;在其内部存储执行逻辑运算、顺序控制、定时、计数和算术运算等操作的指令&#xff0c;通过数字式或模拟式的输入输出来控制…

golang自定义MarshalJSON、UnmarshalJSON 原理和技巧

问题出现的原因&#xff1a;在前后端分离的项目中&#xff0c;经常出现的问题是时间戳格式的问题。 后端的日期格式兼容性强&#xff0c;比较完善。前端由于各种原因&#xff0c;日期格式不完善。 就会产生矛盾。 ms int64比较通用&#xff0c;但是unix时间没有可读性&#xff…

初始Python篇(7)—— 正则表达式

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; Python 目录 正则表达式的概念 正则表达式的组成 元字符 限定符 其他字符 正则表达式的使用 正则表达式的常见操作方法 match方法的…

使用 AI 辅助开发一个开源 IP 信息查询工具:一

本文将分享如何借助当下流行的 AI 工具,一步步完成一个开源项目的开发。 写在前面 在写代码时&#xff0c;总是会遇到一些有趣的机缘巧合。前几天&#xff0c;我在翻看自己之前的开源项目时&#xff0c;又看到了 DDNS 相关的讨论。虽然在 2021 年我写过两篇相对详细的教程&am…

Powershell学习笔记

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&a…

《Java源力物语》-2.异常训练场

~犬&#x1f4f0;余~ “我欲贱而贵&#xff0c;愚而智&#xff0c;贫而富&#xff0c;可乎&#xff1f; 曰&#xff1a;其唯学乎” \quad 在java.lang古域的一处偏僻角落&#xff0c;矗立着一座古老的训练场。青灰色的围墙上布满了密密麻麻的源力符文&#xff0c;这些符文闪烁着…

一起学Git【第二节:创建版本库】

创建库 这个库相当于一个目录&#xff0c;目录中的文件都被Git管理&#xff0c;会记录每个文件的修改删除和添加工作&#xff0c;便于之后随时跟踪历史记录还原到之前的某一版本。如何创建库呢&#xff1f;有两种方式&#xff0c;本地创建库和云端克隆一个库。 1.本地创建库 …

HarmonyOS NEXT 技术实践-基于基础视觉服务的多目标识别

在智能手机、平板和其他智能设备日益智能化的今天&#xff0c;视觉识别技术成为提升用户体验和智能交互的重要手段。HarmonyOS NEXT通过基础视觉服务&#xff08;HMS Core Vision&#xff09;提供了一套强大的视觉识别功能&#xff0c;其中多目标识别作为其关键技术之一&#x…

nginx-静态资源部署

目录 静态资源概述 静态资源配置指令 listen指令 server_name指令 精确匹配 ​编辑 ​编辑 使用通配符匹配 使用正则表达式匹配 匹配执行顺序 default_server属性 location指令 root指令 alias指令 root与alisa指令的区别 index指令 error_page指令 直接使用 …

时空信息平台架构搭建:基于netty封装TCP通讯模块(IdleStateHandler网络连接监测,处理假死)

文章目录 引言I 异步TCP连接操作II 心跳机制:空闲检测(读空闲和写空闲)基于Netty的IdleStateHandler类实现心跳机制(网络连接监测)常规的处理假死健壮性的处理假死方案获取心跳指令引言 基于netty实现TCP客户端:封装断线重连、连接保持 https://blog.csdn.net/z92911896…

Linux之RPM和YUM命令

一、RPM命令 1、介绍 RPM(RedHat Package Manager).,RedHat软件包管理工具&#xff0c;类似windows里面的setup,exe是Liux这系列操作系统里而的打包安装工具。 RPMI包的名称格式&#xff1a; Apache-1.3.23-11.i386.rpm “apache’” 软件名称“1.3.23-11” 软件的版本号&am…

aosp15 - Activity生命周期切换

本文探查的是&#xff0c;从App冷启动后到MainActivity生命周期切换的系统实现。 调试步骤 在com.android.server.wm.RootWindowContainer#attachApplication 方法下断点&#xff0c;为了attach目标进程在com.android.server.wm.ActivityTaskSupervisor#realStartActivityLock…

【漫话机器学习系列】017.大O算法(Big-O Notation)

大 O 表示法&#xff08;Big-O Notation&#xff09; 大 O 表示法是一种用于描述算法复杂性的数学符号&#xff0c;主要用于衡量算法的效率&#xff0c;特别是随着输入规模增大时算法的运行时间或占用空间的增长趋势。 基本概念 时间复杂度 描述算法所需的运行时间如何随输入数…

ensp 基于端口安全的财务部网络组建

ARP IP数据包通过以太网发送&#xff0c;但以太网设备并不能识别IP地址&#xff0c;它们是以MAC地址传输的。因此&#xff0c;必须把IP目的地址转换成MAC目的地址。在以太网中&#xff0c;一个主机要和另一个主机进行直接通信&#xff0c;必须要知道目标主机的MAC地址。 ARP&…