700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证

SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证

时间:2018-12-30 18:13:13

相关推荐

SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证

文章目录

一、 Gateway filter应用一、filter简介二、全局过滤器的使用 二、 Gateway filter + JWT实现token拦截一、jwt简介二、jwt工具类三、登录签发token四、filter拦截token验证,并对特殊接口放行

一、 Gateway filter应用

一、filter简介

1、gateway filter的生命周期

Spring Cloud Gateway同zuul类似,有“pre”和“post”两种方式的filter。客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端

pre类型的filter:在业务逻辑之前post类型的filter:在业务逻辑之后

2、gateway filter的应用场景

pre类型的过滤器:参数校验、权限校验、流量监控、日志输出、协议转换等post类型的过滤器:响应内容、响应头的修改,日志的输出,流量监控等

二、全局过滤器的使用

1、引入依赖和application.yml配置文件

引入依赖

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId><version>3.0.7</version></dependency><dependency><groupId>com.mdx</groupId><artifactId>mdx-shop-common</artifactId><version>1.0.0</version></dependency>

application.yml配置

server:port: 9010spring:application:name: mdx-shop-gatewaycloud:nacos:discovery:server-addr: localhost:8848namespace: mdxgroup: mdxgateway:discovery:locator:enabled: true #开启通过服务中心的自动根据 serviceId 创建路由的功能gateway:routes:config:data-id: gateway-routes #动态路由group: shopnamespace: mdx

注:

配置文件中的相关路由信息已经配置在了nacos的配置中心springcloud gateway路由相关和引入gateway依赖和spring-boot-starter-web依赖冲突问题可以先看下面的文章springcloud gateway的使用 +

nacos动态路由

2、创建自定义全局过滤器

新建自定义filter类,需要实现GlobalFilter, Ordered类

其中GlobalFilter是gateway的全局过滤类

他的实现类如下:

Ordered类是过滤器的执行级别,数值越小执行顺序越靠前

MdxAuthFilter完整代码

注:先简单的模拟了一个token验证的流程

package com.mdx.gateway.filter;import com.alibaba.fastjson.JSONObject;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpResponse;import org.ponent;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;/*** @author : jiagang* @date : Created in /8/8 15:30*/@Component@Slf4jpublic class MdxAuthFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("=========================请求进入filter=========================");// 模拟token验证String token = exchange.getRequest().getHeaders().getFirst("token");if (!"token123".equals(token)){log.error("token验证失败...");return writeResponse(exchange.getResponse(),401,"token验证失败");}log.info("token验证成功...");return chain.filter(exchange);}/*** 值越小执行顺序越靠前* @return*/@Overridepublic int getOrder() {return 0;}/*** 构建返回内容** @param response ServerHttpResponse* @param code返回码* @param msg返回数据* @return Mono*/protected Mono<Void> writeResponse(ServerHttpResponse response, Integer code, String msg) {JSONObject message = new JSONObject();message.put("code", code);message.put("msg", msg);byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);response.setStatusCode(HttpStatus.OK);// 指定编码,否则在浏览器中会中文乱码response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");return response.writeWith(Mono.just(buffer));}}

writeResponse是对返回错误信息结果的封装

这里有个比较容易犯错的点,我们通常会在业务系统中建立全局异常处理器,来建立友好的错误返回信息,但是对于filter来说是不生效的,因为filter是在controller之前执行的,所全局异常处理器是不生效的

测试

通过访问网关服务路由到其他服务,查看filter反应

我这里是启动了一个网关服务和一个订单服务

访问接口并在请求头添加token(9010端口为网关服务)

可以看到请求是成功经过了过滤器

测试传递一个错误的token

成功返回错误信息

二、 Gateway filter + JWT实现token拦截

一、jwt简介

1、官方解释什么是jwt

JWT(JSON WEB

TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串

2、jwt的结构

Header Header 主要包括两部分,分别是Token的类型(typ)和签名算法(alg)Payload Payload 表示有效负载,主要`是关于实体和其他数据的声明。主要有三种类型,分别是 registered(已注册信息),

public(公开信息),private(私有信息)

3、jwt的作用

由于http协议是无状态的,所以客户端每次访问都是新的请求。这样每次请求都需要验证身份,传统方式是用session+cookie来记录/传输用户信息,而JWT就是更安全方便的方式。它的特点就是简洁,紧凑和自包含,而且不占空间,传输速度快,而且有利于多端分离,接口的交互等等

JWT是一种Token规范,主要面向的还是登录、验证和授权方向,当然也可以用只来传递信息。一般都是存在header里,也可以存在cookie里

3、jwt和token的关系

Token是服务器签发的一串加密字符串,是为了给客户端重复访问的一个令牌,作用是为了证明请求者(客户端)的身份,保持用户长期保持登录状态JWT就是token的一种载体,或者说JWT是一种标准,而Token是一个概念,而JWT就是这个概念执行的一种规范,通俗点说token就是一串字符串,jwt就是加上类型,信息等数据再加密包装一下成为一个新的token

二、jwt工具类

1、引入jwt依赖

<!-- jwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>

2、jwt工具类

package com.mdx.gateway.utils;import mon.utils.LocalDateUtil;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.ponent;import java.util.Date;import java.util.HashMap;import java.util.Map;/*** JWTProvider需要至少提供两个方法,一个用来创建我们的token,另一个根据token获取Authentication。* provider需要保证Key密钥是唯一的,使用init()构建,否则会抛出异常。* @author : jiagang* @date : Created in /2/9 14:12*/@Component@Slf4jpublic class JWTProvider {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;/*** 根据用户信息生成token** @param userName* @return*/public String generateToken(String userName) {Map<String, Object> claims = new HashMap<>();claims.put("CLAIM_KEY_USERNAME", userName);claims.put("CLAIM_KEY_CREATED", new Date());return generateToken(claims);}/*** 从token中获取登录用户名* @param token* @return*/public String getUserNameFromToken(String token){String username;try {Claims claims = getClaimsFormToken(token);// username = claims.getSubject();username = claims.get("CLAIM_KEY_USERNAME").toString();} catch (Exception e) {username = null;}return username;}/*** 验证token是否有效* @param token* @param userName* @return*/public boolean validateToken(String token,String userName){String username = getUserNameFromToken(token);return username.equals(userName) && !isTokenExpired(token);}/*** 判断token是否可以被刷新* @param token* @return*/public boolean canRefresh(String token){return !isTokenExpired(token);}/*** 刷新token* @param token* @return*/public String refreshToken(String token){Claims claims = getClaimsFormToken(token);claims.put("CLAIM_KEY_CREATED",new Date());return generateToken(claims);}/*** 判断token是否失效* @param token* @return*/private boolean isTokenExpired(String token) {Date expireDate = getExpiredDateFromToken(token);return expireDate.before(new Date());}/*** 从token中获取过期时间* @param token* @return*/public Date getExpiredDateFromToken(String token) {Claims claims = getClaimsFormToken(token);return claims.getExpiration();}/*** 从token中获取荷载* @param token* @return*/private Claims getClaimsFormToken(String token) {Claims claims = null;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {e.printStackTrace();}return claims;}/*** 根据荷载生成JWT TOKEN** @param claims* @return*/private String generateToken(Map<String, Object> claims) {return Jwts.builder().setSubject(claims.get("CLAIM_KEY_USERNAME").toString()).setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 生成token失效时间** @return*/private Date generateExpirationDate() {// 向后推7天return new Date(System.currentTimeMillis() + expiration * 1000);}public static void main(String[] args) {Date date = new Date(System.currentTimeMillis() + 604800 * 1000);String s = LocalDateUtil.dateToString(date, "yyyy-MM-dd HH:mm:ss");System.out.println(s);}}

2、配置文件

# 这个是 jwt 的配置jwt:tokenHeader: Authorizationsecret: mdx-secrt000001expiration: 604800 #秒 7天prefix: Bearer

三、登录签发token

1、引入mysql和jpa依赖(redis也会用到)

注:此文章是连载文章,因为之前没有用到mysql,在这里登录要查数据库,所以在这里引入mysql

SpringCloud Alibaba 入门可以从这里看:

springcloud alibaba微服务工程搭建(保姆级)

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.9</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

注意:因为gateway模块引用了webflux,webflux是无法使用mysql的,所以如果你的mysql是放在了最父级的pom中,启动gateway是会报错的,所以建议将mysql相关依赖放到其他子模块,或者可以在gateway启动类增加注解:@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

2、配置文件

server:port: 9090spring:application:name: mdx-shop-userdatasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://127.0.0.1:3306/mdx_shop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8driverClassName: com.mysql.cj.jdbc.Driverusername: rootpassword: Bendi+Ceshi+cloud:nacos:discovery:server-addr: localhost:8848namespace: mdxgroup: mdxsentinel:transport:dashboard: localhost:8080 #配置Sentinel dashboard地址port: 8719redis:database: 0host: localhostport: 6379jedis:pool:max-active: 100max-idle: 3max-wait: -1min-idle: 0timeout: 2000feign:sentinel:enabled: true# 这个是 jwt 的配置jwt:tokenHeader: Authorizationsecret: mdx-secrt000001expiration: 604800 #秒prefix: Bearer

3、表结构

/*Navicat Premium Data TransferSource Server : 本地2Source Server Type : MySQLSource Server Version : 50724Source Host : localhost:3306Source Schema : mdx_shopTarget Server Type : MySQLTarget Server Version : 50724File Encoding : 65001Date: 09/08/ 10:07:37*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for mdx_user-- ----------------------------DROP TABLE IF EXISTS `mdx_user`;CREATE TABLE `mdx_user` (`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名',`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码',`nick` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称',`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',`email` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电子邮件',`status` int(1) NULL DEFAULT NULL COMMENT '状态 0 启用 1禁用',`sex` int(1) NULL DEFAULT NULL COMMENT '性别 0 男 1 女',`remarks` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '个人描述',`last_login_time` datetime(0) NOT NULL COMMENT '上次登录时间',`create_time` datetime(0) NOT NULL COMMENT '创建时间',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',PRIMARY KEY (`user_id`) USING BTREE,INDEX `idx_phone`(`phone`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;-- ------------------------------ Records of mdx_user-- ----------------------------INSERT INTO `mdx_user` VALUES (1, 'admin', '$2a$10$c./nfmokuQSEn1KKbXbGw.AgTyT5a.Hs3O/qaXQ5BTjb8xRivgytK', '管理员', '13612345678', '123456789@', 0, 18, NULL, '-02-08 17:15:11', '-02-08 17:15:03', NULL);SET FOREIGN_KEY_CHECKS = 1;

4、用户登录接口实现

jpa接口

/*** @author : jiagang* @date : Created in /2/8 17:01*/@Repositorypublic interface MdxUserRepository extends JpaRepository<MdxUser,Long> {/*** 获取用户信息* @param userName* @return*/MdxUser findByUserName(String userName);}

service实现类

@Servicepublic class UserServiceImpl implements UserService {@Autowiredprivate OrderFeign orderFeign;@Autowiredprivate MdxUserRepository userRepository;@Autowiredprivate JWTProvider jwtProvider;@Autowiredprivate RedisManager redisManager;@Value("${jwt.prefix}")private String prefix;/*** 登录* @param mdxUserDTO* @return*/@Overridepublic LoginVo login(MdxUserDTO mdxUserDTO) {MdxUser mdxUser = userRepository.findByUserName(mdxUserDTO.getUserName());if (mdxUser == null){throw new BizException("用户不存在");}BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();// 判断用户名密码是否正确if (StringUtils.isEmpty(mdxUser.getUserName()) ||! encoder.matches(mdxUserDTO.getPassword(), mdxUser.getPassword())){throw new BizException("用户名或者密码错误");}// 生成tokenString token = jwtProvider.generateToken(mdxUser.getUserName());// 将token存入redisredisManager.set(UserConstant.USER_TOKEN_KEY_REDIS + mdxUser.getUserName(),token,604800);return LoginVo.builder().userId(mdxUser.getUserId().toString()).userName(mdxUser.getUserName()).token(prefix + " " + token).build();}public static void main(String[] args) {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();String password = encoder.encode("admin");System.out.println(password);}@Overridepublic String getOrderNo(String userId, String tenantId, HttpServletRequest request) {return orderFeign.getOrderNo(userId,tenantId, request.getHeader("token"));}}

密码加密方法

public static void main(String[] args) {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();String password = encoder.encode("admin");System.out.println(password);}

controller

@Autowiredprivate UserService userService;/*** 登录* @param mdxUserDTO* @return*/@PostMapping("login")public CommonResponse<LoginVo> login(@RequestBody MdxUserDTO mdxUserDTO){return CommonResponse.success(userService.login(mdxUserDTO));}

测试

成功返回jwt token

四、filter拦截token验证,并对特殊接口放行

1、重新修改我们之前gateway服务中的filter

完成代码如下:

@Component@Slf4jpublic class MdxAuthFilter implements GlobalFilter, Ordered {@Autowiredprivate RedisManager redisManager;@Autowiredprivate JWTProvider jwtProvider;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.prefix}")private String prefix;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("=========================请求进入filter=========================");// 验证tokenString authHeader = exchange.getRequest().getHeaders().getFirst(tokenHeader);if (authHeader != null && authHeader.startsWith(prefix)){String authToken = authHeader.substring(prefix.length());String userName = jwtProvider.getUserNameFromToken(authToken);// 查询redisObject token = redisManager.get(UserConstant.USER_TOKEN_KEY_REDIS + userName);if (token == null){log.error("token验证失败或已过期...");return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");}// 这里也可以使用 jwtProvider.validateToken() 来验证token,使用redis是因为管理员可以在任意时间将用户token踢出// 去除首尾空格String trimAuthToken = authToken.trim();if (! trimAuthToken.equals(token.toString())){log.error("token验证失败或已过期...");return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");}}else {return writeResponse(exchange.getResponse(),500,"token不存在");}log.info("token验证成功...");return chain.filter(exchange);}/*** 值越小执行顺序越靠前* @return*/@Overridepublic int getOrder() {return 0;}/*** 构建返回内容** @param response ServerHttpResponse* @param code返回码* @param msg返回数据* @return Mono*/protected Mono<Void> writeResponse(ServerHttpResponse response, Integer code, String msg) {JSONObject message = new JSONObject();message.put("code", code);message.put("msg", msg);byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);response.setStatusCode(HttpStatus.OK);// 指定编码,否则在浏览器中会中文乱码response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");return response.writeWith(Mono.just(buffer));}}

2、测试

通过访问gateway服务路由到order服务的接口来看下效果

我们先不传token,访问接口(其中9010端口为gateway服务)

提示token不存在

添加一个正确token,将我们上面登录时获取的token填入即可

成功返回订单服务结果

再测试一个错误的token

可以看到返回401

3、特殊接口放行

在一些实际业务中有一些接口是不用登录的,比如登录接口,注册接口等,所以我们需要对这些接口放行。

我们先试下通过网关访问登录接口,不做特殊接口处理的情况

提示我们token不存在

然后我们对登录接口进行放行

配置文件添加如下配置,一般是将以下配置放到nacos配置中心

# 不用登录就可以访问的接口allowed:paths: /mdx-shop-user/user/login

filter方法新增逻辑

完整代码如下

/*** @author : jiagang* @date : Created in /8/8 15:30*/@Component@Slf4jpublic class MdxAuthFilter implements GlobalFilter, Ordered {@Autowiredprivate RedisManager redisManager;@Autowiredprivate JWTProvider jwtProvider;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.prefix}")private String prefix;@Value("${allowed.paths}")private String paths; // 不需要登录就能访问的路径@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("=========================请求进入filter=========================");ServerHttpRequest request = exchange.getRequest();String requestPath = request.getPath().toString();boolean allowedPath = false;if (paths != null && !paths.equals("")){allowedPath = StringUtil.checkSkipAuthUrls(requestPath, paths.split(","));}if (allowedPath || StringUtils.isEmpty(requestPath)){return chain.filter(exchange);}// 验证tokenString authHeader = exchange.getRequest().getHeaders().getFirst(tokenHeader);if (authHeader != null && authHeader.startsWith(prefix)){String authToken = authHeader.substring(prefix.length());String userName = jwtProvider.getUserNameFromToken(authToken);// 查询redisObject token = redisManager.get(UserConstant.USER_TOKEN_KEY_REDIS + userName);if (token == null){log.error("token验证失败或已过期...");return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");}// 这里也可以使用 jwtProvider.validateToken() 来验证token,使用redis是因为管理员可以在任意时间将用户token踢出// 去除首尾空格String trimAuthToken = authToken.trim();if (! trimAuthToken.equals(token.toString())){log.error("token验证失败或已过期...");return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");}}else {return writeResponse(exchange.getResponse(),500,"token不存在");}log.info("token验证成功...");return chain.filter(exchange);}/*** 值越小执行顺序越靠前* @return*/@Overridepublic int getOrder() {return 0;}/*** 构建返回内容** @param response ServerHttpResponse* @param code返回码* @param msg返回数据* @return Mono*/protected Mono<Void> writeResponse(ServerHttpResponse response, Integer code, String msg) {JSONObject message = new JSONObject();message.put("code", code);message.put("msg", msg);byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);response.setStatusCode(HttpStatus.OK);// 指定编码,否则在浏览器中会中文乱码response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");return response.writeWith(Mono.just(buffer));}}

checkSkipAuthUrls工具类方法

/*** 将通配符表达式转化为正则表达式** @param path* @return*/public static String getRegPath(String path) {char[] chars = path.toCharArray();int len = chars.length;StringBuilder sb = new StringBuilder();boolean preX = false;for (int i = 0; i < len; i++) {if (chars[i] == '*') {// 遇到*字符if (preX) {// 如果是第二次遇到*,则将**替换成.*sb.append(".*");preX = false;} else if (i + 1 == len) {// 如果是遇到单星,且单星是最后一个字符,则直接将*转成[^/]*sb.append("[^/]*");} else {// 否则单星后面还有字符,则不做任何动作,下一把再做动作preX = true;continue;}} else {// 遇到非*字符if (preX) {// 如果上一把是*,则先把上一把的*对应的[^/]*添进来sb.append("[^/]*");preX = false;}if (chars[i] == '?') {// 接着判断当前字符是不是?,是的话替换成.sb.append('.');} else {// 不是?的话,则就是普通字符,直接添进来sb.append(chars[i]);}}}return sb.toString();}public static boolean checkSkipAuthUrls(String reqPath,String[] skipAuthUrls) {for (String skipAuthUrl:skipAuthUrls) {if(wildcardEquals(skipAuthUrl, reqPath)) {return true;}}return false;}/*** 通配符模式** @param skipAuthUrl - 需要跳过的地址* @param reqPath - 请求地址* @return*/public static boolean wildcardEquals(String skipAuthUrl, String reqPath) {String regPath = getRegPath(skipAuthUrl);return pile(regPath).matcher(reqPath).matches();}

再次访问登录接口

可以看到gateway已经放行,成功获取到结果

创作不易,点个赞吧👍

最后的最后送大家一句话

白驹过隙,沧海桑田

与君共勉

上一篇文章

springcloud gateway的使用 + nacos动态路由

项目地址

mdx-shop 完整项目

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。