做一个鉴权系统
1、准备数据库
层层递进,用户-组-角色-权限-资源,五个基础表+四个连接表,一共九个表
2、配置文件
server:port: 8888spring:application:name: shoplook2025-gatewaycloud:#nacos用于服务注册与发现nacos:discovery:server-addr: 127.0.0.1:8848username: nacospassword: nacosnamespace: a26e6d61-aac8-4e06-858a-4895ef3ef5cdgroup: shoplook2025-services#注册自身ip和端口ip: 192.168.110.11port: 8888#网关配置gateway:#全局跨域配置globalcors:#解决options请求被拦截问题add-to-simple-url-handler-mapping: truecors-configurations:# 匹配所有请求路径"[/api/**]":#允许的域名,可以配置多个allowedOrigins:- "http://localhost:5000"#允许任意请求方法allowedMethods: "*"#允许任意请求头allowedHeaders: "*"#允许携带Cookie及认证权限allowCredentials: true#预检请求缓存时间(单位:秒maxAge: 36000#路由配置routes:#商品品牌微服务,id属性用于指定路由规则名称,可自定义- id: brand-api#转发的目标地址和端口,其中lb表示loadBalancer协议(负载均衡),后面是nacos上的微服务名称uri: lb://brand-api#可定义多组路由断言,满足指定条件则进行转发。断言类型包括Path、Before、After、Between、Cookie、Header、Host、Method、Query、RemoteAttr等多种#Path较常用,表示满足请求路径模式则转发,基于Ant模式匹配。注意:"/**"是一个整体,属于Ant风格通配符,表示匹配何意层级的路径,包括零次#此处示例支持两种类型的地址匹配模式predicates:- Path=/api/brands/**, /api/v1/brands/**#网关支持多种过滤器。RewritePath表示重写请求路径,支持正则表达式。filters:#1.正则表达式中的第1个问号,即"?:",表示非捕获分组,即其外部的小括号并不进行分组捕获。#2.正则表达式中的第2个问号,即"?<segment>"表示自定义捕获分组的名字(可自定义),若不自定义名称,则默认为$1、$2、$3...,若自定义,则名称为${xxx}#3.正则表达式中的第3个问号,即"(xxx)?",表示前面的分组匹配0次或1次#4.$后的反斜杠表示转义,如果不加,则会被理解为获取当前配置文件中的变量值,加上转义字符则打断识别为变量的这个过程。其实左大括号,在正常情况下不加转义字符就能识别,加左花括号只是为了打断将其识别为变量而已- RewritePath=/api/brands(?:(?<group1>/.*))?, /api/v1/brands$\{group1}#商品分类微服务- id: category-apiuri: lb://category-apipredicates:- Path=/api/v1/categories/**#商品微服务- id: good-apiuri: lb://good-apipredicates:- Path=/api/v1/goods/**#后台用户微服务- id: user-apiuri: lb://user-apipredicates:- Path=/api/v1/users/**#地址微服务- id: address-apiuri: lb://address-apipredicates:- Path=/api/v1/addresses/**#会员微服务- id: member-apiuri: lb://member-apipredicates:- Path=/api/v1/members/**#会员收货地址微服务- id: member-address-apiuri: lb://member-address-apipredicates:- Path=/api/v1/member_addresses/**#购物车微服务- id: cart-apiuri: lb://cart-apipredicates:- Path=/api/v1/cart_items/**#订单微服务和订单项微服务- id: order-apiuri: lb://order-apipredicates:- Path=/api/v1/orders/**,/api/v1/order_items/**#rbac微服务,用户组、角色、权限、资源等- id: rbac-apiuri: lb://rbac-apipredicates:- Path=/api/v1/groups/**,/api/v1/roles/**,/api/v1/perms/**,/api/v1/resources/**,/api/v1/user_groups/**,/api/v1/group_roles/**,/api/v1/role_perms/**,/api/v1/perm_resources/**,/api/v1/menus/**,/api/v1/rbac/**#sku微服务、规格组微服务、规格项微服务- id: sku-apiuri: lb://sku-apipredicates:- Path=/api/v1/skus/**,/api/v1/spec_groups/**,/api/v1/spec_items/**#文件上传微服务,及富文本编辑器- id: upload-apiuri: lb://upload-apipredicates:- Path=/api/v1/uploads/**,/api/v1/editors/**discovery:locator:#动态路由配置:相当于断言Path=[微服务名称]/**,过滤器则为StripPrefix=1,去掉第1级前缀,即去掉微服务名称#如:访问网关地址http://ip:8888/brand-api/api/v1/brands,去掉第1级前缀,转发给http://ip:port/api/v1/brandsenabled: truelowerCaseServiceId: true#jwt的密钥,使用Jasypt对15963294666的一个加密串。本质上可以为任意字符串
jwt:secret:key: EJGtYhUyp9TPiKrnYbSWH2lQ24RkeAhM/if5MGG6ZIvGEPyLtMc50SGtY5zlfzdprbac:white-list:- get:/api/v1/users/captcha/**- post:/api/v1/users/login**- get:/api/v1/rbac/resource_perm_mappings/**- get:/api/v1/rbac/perms/**gateway: shoplook2025-gateway
配置如上之后,可以实现负载均衡,将带有检测之后的路径访问打到配置文件中对应的微服务上。
3、白名单和WebClient
@Getter
@Setter
@ConfigurationProperties(prefix = "rbac")
@Configuration
public class GatewayConfig {private List<String> whiteList;@LoadBalanced@Beanpublic WebClient.Builder webClientBuilder() {return WebClient.builder();}
}
开启负载均衡,并映射了配置文件中前缀名为rabc的配置到属性上去。返回一个开启负载均衡的WebClient
4、拦截器类
// 声明这是一个Spring组件,作为全局过滤器
@Component
public class AuthFilter implements GlobalFilter {// 日志记录器private static final Logger log = Logger.getLogger(AuthFilter.class.getName());// 从配置文件中注入JWT密钥@Value("${jwt.secret.key}")private String jwtSecret;// JSON处理对象private ObjectMapper objectMapper;// 网关配置类private GatewayConfig gatewayConfig;// WebClient构建器,用于发起HTTP请求private WebClient.Builder webClientBuilder;// RBAC网关地址@Value("${rbac.gateway}")private String gateway;// 通过Setter方法注入ObjectMapper@Autowiredpublic void setObjectMapper(ObjectMapper objectMapper) {this.objectMapper = objectMapper;}// 通过Setter方法注入GatewayConfig@Autowiredpublic void setGatewayConfig(GatewayConfig gatewayConfig) {this.gatewayConfig = gatewayConfig;}// 通过Setter方法注入WebClient.Builder@Autowiredpublic void setWebClientBuilder(WebClient.Builder webClientBuilder) {this.webClientBuilder = webClientBuilder;}// 核心过滤方法@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取当前请求对象ServerHttpRequest req = exchange.getRequest();// 获取请求路径String uri = req.getPath().toString();// 获取上下文路径String ctx = req.getPath().contextPath().value();// 移除上下文路径,得到纯路径uri = uri.replace(ctx, "");// 获取HTTP方法String method = req.getMethod().toString();// 保存最终路径的最终变量(用于内部类访问)final String finalUri = uri;// 获取白名单URL列表List<String> ignoreUrls = gatewayConfig.getWhiteList();// 路径匹配器PathMatcher matcher = new AntPathMatcher();// 遍历白名单模式for (String pattern : ignoreUrls) {// 解析模式字符串UriPattern up = UriPattern.of(pattern);// 检查当前请求是否匹配白名单模式和HTTP方法if (matcher.match(up.pattern(), uri) && up.matchMethod(method)) {// 直接放行白名单请求return chain.filter(exchange);}}// 从请求头获取JWT令牌String jwt = req.getHeaders().getFirst("Authorization");// 如果头中没有,尝试从查询参数获取if (!StringUtils.hasText(jwt)) {jwt = req.getQueryParams().getFirst("jwt");}// 如果存在JWT令牌if (StringUtils.hasText(jwt)) {// 创建JWT验证器JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();try {// 验证JWT令牌DecodedJWT dj = jwtVerifier.verify(jwt);// 从JWT中提取用户信息Integer userId = dj.getClaim("userId").asInt(); // 用户IDString username = dj.getAudience().getFirst(); // 用户名// 构建WebClient用于发起权限验证请求WebClient webClient = webClientBuilder.build();// 第一步:获取资源-权限映射关系return webClient.get().uri("http://" + gateway + "/api/v1/rbac/resource_perm_mappings").retrieve().bodyToMono(JsonResult.class).flatMap(it -> {// 提取资源-权限映射数据Map<String, List<String>> resourcePermMappings = (Map<String, List<String>>) it.getData();// 第二步:获取当前用户的所有权限return webClient.get().uri("http://" + gateway + "/api/v1/rbac/perms/{userId}/true", userId).retrieve().bodyToMono(JsonResult.class).flatMap(it1 -> {// 提取用户权限列表List<String> perms = (List<String>) it1.getData();// 检查用户是否有访问权限if (hasPerm(finalUri, method, perms, resourcePermMappings)) {// 构造认证信息头String authInfo = userId + ":" + username;// 修改请求,添加认证信息头ServerHttpRequest mutatedRequest = exchange.getRequest().mutate().header("x-auth-info", authInfo).build();// 放行请求return chain.filter(exchange.mutate().request(mutatedRequest).build());} else {// 返回403禁止访问响应return writeJson(exchange.getResponse(), HttpStatus.FORBIDDEN, JsonResult.fail("无权访问"));}});});} catch (JWTVerificationException e) {// JWT验证失败,记录日志并返回401未授权响应log.log(Level.SEVERE, "jwt校验异常", e);return writeJson(exchange.getResponse(), HttpStatus.UNAUTHORIZED, JsonResult.fail("jwt无效或已过期"));}} else {// 没有提供JWT,返回401未授权响应return writeJson(exchange.getResponse(), HttpStatus.UNAUTHORIZED, JsonResult.fail("无jwt,请重新认证"));}}// 辅助方法:向响应写入JSON数据private Mono<Void> writeJson(ServerHttpResponse resp, HttpStatus status, JsonResult<?> jsonResult) {// 设置HTTP状态码resp.setStatusCode(status);// 设置内容类型为JSONresp.getHeaders().add("Content-Type", "application/json;charset=utf-8");try {// 将JSON结果对象序列化为字符串String json = objectMapper.writeValueAsString(jsonResult);// 创建数据缓冲区DataBuffer db = resp.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));// 写入响应return resp.writeWith(Mono.just(db));} catch (JsonProcessingException ex) {// 序列化异常时抛出运行时异常throw new RuntimeException("json序列化异常", ex);}}// 内部记录类:用于表示URI模式及其允许的HTTP方法private record UriPattern(String[] allowedMethods, String pattern) {// 检查是否匹配HTTP方法private boolean matchMethod(String method) {for (String am : allowedMethods) {// 允许通配符*匹配所有方法if (am.equalsIgnoreCase(method) || am.equals("*")) {return true;}}return false;}// 从字符串创建UriPattern的工厂方法private static UriPattern of(String uri) {String[] parts = uri.split(":");if (parts.length == 1) {// 默认允许所有HTTP方法return new UriPattern(new String[]{"*"}, parts[0]);} else {// 解析指定的HTTP方法return new UriPattern(parts[0].split(","), parts[1]);}}}// 检查用户是否具有访问指定URI的权限private boolean hasPerm(String uri, String method, List<String> userPerms, Map<String, List<String>> resourcePermMappings) {PathMatcher matcher = new AntPathMatcher();// 遍历所有资源-权限映射for (Map.Entry<String, List<String>> entry : resourcePermMappings.entrySet()) {String resource = entry.getKey(); // 资源模式List<String> perms = entry.getValue(); // 访问该资源所需的权限// 解析资源模式UriPattern up = UriPattern.of(resource);// 检查当前请求是否匹配资源模式和HTTP方法if (matcher.match(up.pattern(), uri) && up.matchMethod(method)) {// 检查用户是否拥有任一所需权限for (String perm : perms) {if (userPerms.contains(perm)) {return true;}}}}return false;}
}