当前位置:   article > 正文

后端之路——登录校验前言(Cookie\ Session\ JWT令牌)_没有这个parseclaimsjws()

没有这个parseclaimsjws()

前言:Servlet

【登录校验】这个功能技术的基础是【会话技术】,那么在讲【会话技术】的时候必然要谈到【Cookie】和【Session】这两个东西,那么在这之前必须要先讲一下一个很重要但是很多人都会忽略的一个知识点:【Servlet

什么是Servlet?

        Servlet是用java编写的应用在服务器端的程序;对于它的定义,“广义上”是一个个很大的【类】,“狭义上”是【接口】

.

Servlet容器是什么?

        我们经常听说的Tomcat、Weblogic......这些都是各种【Servlet容器】,而【Servlet容器】就是Servlet的运行环境,也可以理解是Servlet的引擎,为请求和响应的这些操作提供网络服务。

        那么我们知道,我们运行网络服务的时候都要启动Tomcat服务器,Tomcat来解析处理【请求】、【响应】,并处理成报文信息,但是这些服务器的缺点是底层代码写死了,很不灵活,而且有时处理完的数据格式也不够规范;那么这时候就诞生了Servlet,它被用来“扩展服务器的性能”,它能够灵活的处理请求、响应数据并以规范形式返回。

        那么说回Servlet,其实最简单最简单的理解,就是一个很大很规范的【类】,它里面包含了很多其他【子类】(准确来说是接口),这些子类包括:CookieSessionHttpServletRequestHttpServletResponse、ServletConfig、ServletContext......

        那么这些【子类】里也写好了很详细、规范的各种处理网络服务的方法,当我们需要处理一些网络服务逻辑的时候,就需要调用Servlet里的【子类】的【实例化对象】的【方法】

        打个比方:处理前端客户端发送请求的数据并生成对应的请求头报文时,就需要调用【HttpServletRequest】的实例化对象的方法;  处理服务器端返回回去的响应头报文的时候,就需要调用【HttpServletResponse】的实例化对象的方法......等等

那么现在我们重新来看一下(B/S架构)浏览器客户端与服务器端传输的流程:

1、浏览器客户端发送请求到服务器端

2、服务器端接收到信息,交给Servlet

3、Servlet(通过调用里面的一些子类的方法)处理逻辑,然后生成响应信息,给回服务器

4、服务器将响应结果返回浏览器客户端

一、会话技术

1、何为会话?

浏览器与服务器之间的一次连接就是一次会话

2、会话跟踪

【会话跟踪】就是:识别多个请求是否来自于同一个浏览器,然后在同一个浏览器的多个请求之间共享数据

3、会话跟踪方案:

浏览器与服务器之间的交互使用的是【http】协议,但是【http】协议是无状态的,也就是所有请求都是相互独立的,并不能在多个请求之间共享数据

那么就有了这么几种【会话跟踪技术】:

1、客户端会话跟踪技术:Cookie

.

2、服务端会话跟踪技术:Session

3、token令牌技术:JWT

这里不讲解HTTP是啥,这里有一篇讲的比较详细:HTTP 协议详解(史上最全)-CSDN博客

HTTP协议里的【请求头】和【响应头】的文章:Request Headers 和Response Headers——请求头和响应头-CSDN博客

只需要知道HTTP协议是一个浏览器与服务器联系传输数据的超文本传输协议,它两规定了一种传输数据的格式,然后里面有【响应报文Response Headers】和【请求报文Request Headers】这两部分,下面是简单讲解:

请求报文Request Headers】:

前端通过【浏览器/客户端】发送请求传给【服务器】的数据信息

一个【请求报文Request Headers】里包括了【请求行】【请求头】【请求体】

【请求体】就是前端传过来的具体的数据信息

【请求头】是服务器获取客户端信息的一些依据(用什么格式获取?从哪个地址获取?哪个浏览器发的?......)

响应头报文Reponse Headers】:

【服务器】返回给【浏览器/客户端】的响应数据信息

一样,一个【响应报文Response Headers】里也包括了【响应行】【响应头】【响应体】

【响应体】就是针对前端发来的请求数据而相应回去的具体数据值

【响应头】包含了服务器的响应讯息,如http版本,压缩方式,响应文件类型,文件编码等

(1)Cookie技术(Cookie是一个Servlet里的“子类”)

虽然每个请求和响应之间的Http协议是独立的,但是Http协议的【请求头】和【响应头】支持携带【Cookie】这个信息,那多个请求间就可以通过Cookie这个标识来获取用户信息数据

Cookie简单说:就是存放在客户端(浏览器)】的会话信息

—— Cookie是通过在【请求报文Request Headers】里的【请求头】处传递的

—— 传递关系是:【客户端(浏览器)——传Cookie——>服务器

—— Set-Cookie是通过【响应报文Response Headers】里的【响应头】处传递的

—— 传递关系是:【服务器——返回Set-Cookie——>客户端(浏览器)

简单用代码直观一点展示Cookie和Set-Cookie:(切记别记忆这些代码,简单了解即可)

Set-Cookie:

【HttpServletResponse】这个接口的实现类是专门设置【响应报文】信息的(切记:要导入的包一定要选这个【javax.servlet.http】)

.

然后【HttpServletResponse】的实现类对象的【.addCookie( )】方法能设置一个Cookie的值,需要往里面传一个【Cookie对象】(切记:这个【Cookie对象】对应的类型必须也是【javax.servlet.http】)

.

最后【Cookie对象】里数据形式是“键值对Key=value”:【name=value】,所以要传一个name参数、一个value值,分别代表 “键” 和 “值”

  1. /**
  2. * 模拟【服务器】设置【用户的Cookie】的操作
  3. * @param response
  4. * @return
  5. */
  6. @GetMapping("/setCookie")
  7. public Result setCookie(HttpServletResponse response){
  8. //调用这个响应方法就能设置一个Cookie值,Cookie对象里是【键值对】信息:name=value
  9. response.addCookie(new javax.servlet.http.Cookie("userName","岑梓铭"));
  10. return Result.success();
  11. }

怎么看效果?

1、首先输入网址,千万先别回车

2、摁F12打开网页检查,然后再在网址那回车,就会看到一个网络响应

3、单击它,然后就能在【响应报文Response Header】处看到【Set-Cookie】

Cookie:

【HttpServletRequest】这个接口的实现类是专门设置【请求报文】信息的(切记:要导入的包一定要选这个【javax.servlet.http】)

.

然后【HttpServletRequest】的实现类对象的【.getCookies( )】方法能返回所有Cookie的值,返回值是一个数组;需要用一个【Cookie对象类型的数组】接收(切记:这个【Cookie对象】类型对应的类型必须也是【javax.servlet.http】)

.

最后【Cookie对象】里数据形式是“键值对Key=value”:【name=value】,所以要获取Cookie数组里每一个Cookie的 “键”,就要调用【Cookie对象】的【.getName( )】方法

  1. @GetMapping("/getCookie")
  2. public Result getCookie(HttpServletRequest request){
  3. //发送请求后,获取返回的【所有的Cookies】
  4. javax.servlet.http.Cookie[] cookies = request.getCookies();
  5. //遍历所有Cookie,如果有对应这个【键(name)】的,就返回对应的【值(value)】
  6. for(javax.servlet.http.Cookie cookie : cookies){
  7. if(cookie.getName().equals( "userName" )){
  8. System.out.println("userName: " + "【" + cookie.getValue() + "】");
  9. }
  10. }
  11. return Result.success();
  12. }

 怎么看效果?还是一样

1、首先输入网址,千万先别回车

2、摁F12打开网页检查,然后再在网址那回车,就会看到一个网络响应

3、单击它,然后就能在【响应报文Response Header】处看到【Set-Cookie】

缺点

1、移动端环境不是浏览器,浏览器才有Cookie这玩意

.

2、用户可以随意自己设置禁用Cookie,会用电脑的应该不用我解释,浏览器设置那里有

.

3、Cookie不能跨域(跨域就是【协议、IP/域名、端口】至少其中一样不一样,就是两个域,就存在跨域访问,那么我们都知道前端、后端是分别部署到两个不同的服务器的,不同服务器的地址肯定是不一样的,浏览器在发请求、返回响应时必然会要访问前端和后端的两个服务器,就会跨域)

总结

(2)Session技术(Session也是Servlet里的一个“子类”)

Session的本质其实就是对Cookie的优化,是存放在【服务器端】的会话信息

为什么说是Cookie的优化?因为它的逻辑其实是这样:

        首先浏览器发请求,产生一个会话,然后服务器这边就立刻产生一个【Session会话信息】和一个对应这个Session会话信息的【sessionId】,然后把【Session会话信息】存在服务器,只会把【sessionId】存进Cookie,通过Set-Cookie响应回给浏览器

。。

        然后下次浏览器再次发送请求想获取这个【Session会话信息】的时候,传递过去的是【装着sessionId的Cookie】,然后服务器检查Cookie里的【sessionId】,再到session里找有没有对应这个【sessionId】的【Session会话信息】,找到了的话,把【sessionId】和【Session会话信息】一起塞进【Cookie】返回给浏览器

模拟服务器【保存session】并【生成sessionId】的逻辑
  1. /**
  2. * 模拟【服务器端】生成并响应回【session】的操作
  3. * @param session
  4. * @return
  5. */
  6. @GetMapping("/setSession")
  7. public Result setSession(HttpSession session){
  8. //打印一下当前session的哈希码值,这个哈希码值代表指向了哪一个session会话,是【整数】
  9. //但是注意区分,这个不是sessionId,sessionId是HttpSession对象的唯一标识符,是【字符串】
  10. log.info("session_hashCode: {}",session.hashCode());
  11. //调用这个方法可以往session会话里存入一个数据,然后对应生成一个sessionID并塞进Cookie里
  12. session.setAttribute("LoginName","岑梓铭");
  13. return Result.success();
  14. }

模拟浏览器通过sessionId【接收session信息】的逻辑
  1. /**
  2. * 模拟【浏览器】发请求后获得服务器生成的【session】的操作
  3. * @param request
  4. * @return
  5. */
  6. @GetMapping("/getSession")
  7. public Result getSession(HttpServletRequest request){
  8. //调用HttpServletRequest对象的getSession()方法可以获取到session会话对象
  9. HttpSession session = request.getSession();
  10. //打印一下当前session的哈希码值,这个哈希码值代表指向了哪一个session会话,是【整数】
  11. log.info("session_hashCode: {}",session.hashCode());
  12. //Session的getAttribute方法,里面传入“键”参数,就能返回对应的seesion会话对象里的具体值
  13. //逻辑是浏览器这边Cookie里只有sessionId,然后通过Session的getAttribute方法把Cookie里的sessionId给到服务器端
  14. //最终经过服务器检测sessionId,然后将session会话里对应“userName”的会话具体数据再塞进Cookie,返回给浏览器
  15. Object userInfo = session.getAttribute("LoginName");
  16. log.info("userInfo: {}",userInfo);
  17. return Result.success(userInfo);
  18. }

 缺点

1、服务器集群情况下(也就是连接多个服务器的情况下),不同的服务器之间存着不同的会话信息,那就算浏览器的Cookie里有sessionId,那第二台服务器那里能靠第一台服务器seesion的 “钥匙” 来 “开“ 第二台服务器seesion的 “大门” 呢?

.

2、session会话信息都存在服务器,随着浏览器请求增多,服务器内存越来越不够用

.

3、既然它还是基于Cookie优化而来的,那必然也继承了Cookie的缺点

总结

(3)JWT令牌技术

——概念:

JWT,全称:【Json Web Token】,简单来说就是一长串带有【数字签名、签名算法、具体自定义信息......等等】json字符串,每一次请求响应都会带着它,通过计算来校验、得出其中的身份信息

一个JWT字符串主要包括三大部分:

        第一部分:Header(头),记录令牌类型、名算法等。例如:{"alg":"HS256","type":"JWT"}

        第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}

        第三部分:Siqnature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

而组成这个字符串的原理是:

1、前面两部分是【Base64编码】,是一种由【A-Z  a-z  1-9  还有/】组成的来表示二进制数据的编码

2、最后一部分那一段是根据前面指定的一种【签名算法】计算后得到的编码

应用场景:

其实很简单,流程就是:

1、浏览器发送请求到服务器

2、服务器拦截请求,查看有没有token?没有就拒绝访问数据,并生成一个JWT令牌

3、下一次再来检查到有JWT令牌了,那就校验JWT令牌对不对,不对就拒绝访问;对就开放访问权限。

生成JWT令牌的方法:
1、引入依赖

在pom.xml文件引入下面依赖

  1. <!-- JWT令牌 -->
  2. <dependency>
  3. <groupId>io.jsonwebtoken</groupId>
  4. <artifactId>jjwt</artifactId>
  5. <version>0.9.1</version>
  6. </dependency>

注意,如果点了右上角的【maven刷新】按钮只后还是爆红,又可能只是连接中央库下载安装这个依赖包的时候网络不好,毕竟这些依赖都是在国外的公司的中央库,那么控制台那会有一个 “蓝色” 的提示——“尝试使用 -U 标记(强制更新快照)运行 Maven导入”,直接点它让Maven帮我们换个方案下载安装就行了

2、在Test类测试一下生成JWT令牌

只需要记住6步:

1、先设置一个哈希表集合,因为jwt令牌的【有效信息部分】要用【哈希表集合类型】接收

2、创建一个jwt的方法是【Jwts.builder( )】

3、一个Jwt令牌的第一部分是指定 “签名算法类型”“签名密钥”;那么【.signWith( )方法】就是设置jwt令牌第一部分。以我个人理解,“签名算法类型” 就是指根据不同类型的不同算法,“签名密钥” 就是以你自定义输入的一串字符串作为一个 “密钥”,用你的这个 “签名密钥” 才能在解析jwt令牌时知道你要解析的是哪一个(至于“签名算法类型”具体哪些类型有啥区别我也不知道,尽量先都用HS256这个类型就行)

4、一个Jwt令牌的第二部分是【有效荷载】,也就是用户的一些【有效信息部分】,【Jwts.addClaims( )】方法则是在创建一个jwt对象之后接收这个【有效信息部分】的方法,接收【哈希表集合】类型数据

5、jwt令牌要有个【有效时间】,就跟你们平时登录时的验证码一样,不然的话没时间限制那不是留够了时间给黑客破解吗。【.setExiration( )】方法就是设置【有效时间】,需要接受的是Date时间类型(System.currentTimeMillis()是目前的系统时间,加一个有效时间期限就行)

6、jwt是一个对象,要用【.compact( )】方法才能转化成【字符串】

  1. @Test
  2. void testGenJWT(){
  3. //先设置一个哈希表集合,因为jwt令牌的【有效信息部分】要用【哈希表集合类型】接收
  4. Map<String , Object> claims = new HashMap<>(); //值用Object因为可能是数字、可能是字符串
  5. claims.put("id",1);
  6. claims.put("name","岑梓铭");
  7. String jwt = Jwts.builder() //builder就是创建一个JWT令牌
  8. .signWith(SignatureAlgorithm.HS256,"yjtlwkbz") //设置【签名算法的类型(比如HS256)】、【签名内容(比如yjtlwkbz)】
  9. .addClaims(claims) //有效荷载部分,接收哈希表集合类型,存入用户有效信息
  10. .setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) //设置有效时间1h(3600秒 * 1000毫秒)
  11. .compact(); //把结果生成字符串
  12. System.out.println(jwt);
  13. }

然后提示几点:

1、因为顶上的【@SpringBootTest】注解会影响整个项目,直接注释了,然后点对应这个当前这个【@Test】测试方法运行最快

.

2、如果刚刚编写生成jwt令牌代码时爆红爆错,检查这几个问题:

—— 报错classNotFoundException的下载jaxb-api依赖,2.1版本(版本别填错了)

—— 使用Base64编码字符串长度至少为43位,位数报错的可以把 “签名内容” 改成任意的大于等于43位的字符串(比如我代码里的"yjtlwkbz"改成"ahjahsdgaysdgkuywdgjwdbasbcjhcjasyasgkjjsh")

3、还有有的人可能会出现【test】包下的test类(class文件)全变成java文件了,没法运行,右键也不能新建class类文件,那可能是IDE出了点问题,清楚IDE缓存再重新启动一次就行,见下图

然后运行完成后,我们把控制台生成的【jwt令牌】复制,到这个网站可以查看解析我们的【jwt令牌】的信息:JSON Web Tokens - jwt.io

解析JWT令牌的方法

更简单,三步:

1、【Jwts.parser( )】方法解析jwt令牌

2、【.setSigningKey( )】方法就是根据你前面生成jwt令牌时的那个【签名密钥】来 “打开解密大门”

3、【.parseClaimsJws( )】把刚刚生成的【jwt令牌】整个塞进去就能被解析了

注意,别选成【.parseClaimsJwt( )】了,这两是两个东西,选下图这个

4、【.getBody( )】能够获取出【有效荷载】部分那些具体信息,并封装在一个Claims类型对象

  1. @Test
  2. void testGetJWT(){
  3. Claims claims = Jwts.parser()
  4. .setSigningKey("yjtlwkbz") //对应生成jwt的.signWith(SignatureAlgorithm.HS256,"yjtlwkbz")那个密钥
  5. //对应刚刚运行生成的jwt令牌
  6. .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5bKR5qKT6ZOtIiwiaWQiOjEsImV4cCI6MTcyMDQwOTg1Nn0.OmpQBJoP50BjjPnItLBGCmhgAVTcDzYGsTBMT7qohoE")
  7. .getBody(); //获取有效荷载部分
  8. System.out.println(claims);
  9. }

注意几点:

1、有效信息最后要用一个Claims对象来接收

2、一个jwt令牌有时效和使用次效,你如果超过了你设置的失效期限、或者已经运行执行了一次解析jwt令牌,那么这串jwt令牌就作废了,需要你再次运行【生成jwt令牌】,然后再运行【解析jwt令牌】获取信息,否则会报错

3、【.parseClaimsJws( )】别写成了【.parseClaimsJwt( )】

二、利用jwt令牌技术校验身份

现在我们学完了最先进的jwt令牌技术,那么就来实践一下如何运用它。

1、先为了前后端请求响应方便,封装好一个jwt令牌工具类

很简单,我们前面已经知道怎么【生成】和【解析】jwt令牌了,那么在封装工具类里只要改几点:

/

1、【生成jwt令牌】的时候首先要接收一个前端传过来的装着用户有效信息的【哈希表集合】参数;并最后要把生成的令牌字符串return出去

.

2、【解析jwt令牌】的时候需要接收生成的jwt字符串;并把解析获得【有效荷载信息】return回前端

  1. package com.czm.tliaswebmanagement.utils;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.Jwts;
  4. import io.jsonwebtoken.SignatureAlgorithm;
  5. import java.util.Date;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. //别忘了加这个注解,让类放入IOC容器
  9. @Component
  10. public class JwtUtils {
  11. private static String signKey = "yjtlwkbz"; //定义【签名密钥】是“yjtlwkbz”
  12. private static Long time = (long)60*5 * 1000; //定义有效时间是5分钟
  13. /**
  14. * 接收前端有效信息,生成jwt令牌并返回给前端
  15. * @param claims
  16. * @return
  17. */
  18. public String generateJWT(Map<String,Object> claims){
  19. String jwt = Jwts.builder() //builder就是创建一个JWT令牌
  20. .signWith(SignatureAlgorithm.HS256,signKey) //设置【签名算法的类型】、【签名内容】
  21. .addClaims(claims) //有效荷载部分,接收哈希表集合类型,存入用户有效信息
  22. .setExpiration(new Date(System.currentTimeMillis() + time)) //设置有效时间
  23. .compact(); //把结果生成字符串
  24. return jwt;
  25. }
  26. /**
  27. * 接收前端传来的jwt令牌,解析并返回有效荷载
  28. * @param jwt
  29. * @return
  30. */
  31. public Claims parseJWT(String jwt){
  32. Claims claims = Jwts.parser()
  33. .setSigningKey(signKey)
  34. .parseClaimsJws(jwt)
  35. .getBody();
  36. return claims;
  37. }
  38. }

2、然后完成登录接口(三层架构)代码编写

1、首先根据接口文档规定,来确定前端传入的是什么格式数据

比如这个文档,以它为例子,那么确定前端传入【JSON格式】的【用户登录信息】,那么就要用一个Emp对象(我前几篇一直用的案例,员工对象)来接收这些参数值,然后用【@RequestBody】解析JSON成对象。然后post请求跟接口是“/login”,那就【@PostMapping("/login")】

(contrller)

2、第二步,在controller层调用service、并把刚刚解析的参数Emp传给service,service调用mapping进行sql查询,根据这个【用户登录信息】参数查询完数据库之后返回结果,再一级一级返回controller,老生常谈的流程我就不细说了。

(controllerr)

(service )

(mapping)        

3、然后在controller层再用一个【新的Emp对象】接收【查询完返回的结果】,如果查询到结果就说明数据库有这个账户,那么调用【JWT工具类】为这个账户【生成一个jwt令牌】(生成jwt的逻辑,在JWT工具类已经帮我们做好了,我们只需要传一个【装有用户信息】的【Map哈希表集合类的参数】给JWT工具类就行了)

4、最后,在查询到账户的情况下,将生成含有用户信息的JWT令牌返回给前端即可;如果查不到信息,就说明账户密码有误,查无此人,那就直接返回失败。

controller的完整代码:(其他的层的就不展示了)

  1. package com.czm.tliaswebmanagement.controller;
  2. import com.czm.tliaswebmanagement.pojo.Emp;
  3. import com.czm.tliaswebmanagement.pojo.Result;
  4. import com.czm.tliaswebmanagement.service.EmpService;
  5. import com.czm.tliaswebmanagement.utils.JwtUtils;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.web.bind.annotation.*;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. @Slf4j
  12. //@RequestMapping("/emps")
  13. @RestController
  14. public class EmpController {
  15. @Autowired
  16. private EmpService empService;
  17. //获取JWT令牌工具类
  18. @Autowired
  19. private JwtUtils jwtUtils;
  20. /**
  21. * 登录接口
  22. */
  23. @PostMapping("/login")
  24. public Result login(@RequestBody Emp emp){
  25. log.info("传过来的员工账号密码信息:{}",emp);
  26. //先调用service查找数据库有无此账户
  27. Emp e = empService.login(emp);
  28. //判断能否根据用户名、密码在数据库查到此人
  29. //有的话,生成属于它的令牌,并返回给他
  30. if(e != null){
  31. //因为生成jwt令牌需要的是【哈希表集合】类型,所以用一个【哈希表集合】装查到的员工信息
  32. Map<String , Object> claims = new HashMap<>();
  33. //这里经过service、mapper查询回来的员工信息e,获取出他的id、name、username作为有效荷载信息
  34. claims.put("id",e.getId());
  35. claims.put("name",e.getName());
  36. claims.put("username",e.getUsername());
  37. //调用jwt工具类生成jwt方法获取jwt令牌
  38. String jwt = jwtUtils.generateJWT(claims);
  39. //然后把jwt令牌返回给前端
  40. return Result.success(jwt);
  41. }
  42. //那么如果数据库都没查到这个账户,就说明账号密码输入错误,返回错误就行了
  43. return Result.error("登陆失败,查无此人");
  44. }
  45. }

然后我要解释一下这个jwt令牌到底在哪里传输

就在一个叫【token的玩意里存着,你可以理解为【token是一张磁卡,然后 jwt 就是类似这个磁卡的信号信息,你用【token这个磁卡刷门禁、刷刷卡机的时候,把 jwt 信息传过去验证。

然后这个【token可以放请求体、也可以是请求头,不过一般都是放请求头

那么现在前端只要登陆成功,就已经能获取到后端为之生成的jwt令牌了,现在只需要下次再携带这个jwt令牌发送请求,后端就可针对这个令牌进行判断:是否给这个用户放行使用软件、网页了,那么这就涉及到【过滤器filter】和【拦截器Interceptor】,下一篇再讲

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/856935
推荐阅读
相关标签
  

闽ICP备14008679号