Cookie,Session与Token

参考资料

水平有限,欢迎交流!仅做学习交流使用
一定要看,非常透彻!【Cookie、Session、Token究竟区别在哪?如何进行身份认证,保持用户登录状态?】
黑马jwt详解
Javaweb程序设计与项目案例 郭煦

直接上结论

共同点

Cookie,Session与Token 三者都实现了 Http 无状态这一特点的补充,通过存储在不同硬件或不同算法加密来存储用户状态

不同点

Cookie 将用户状态保存在客户端(如浏览器)
Session 将用户状态保存在服务器端

Cookie 详解

Cookie 是服务器发送到用户浏览器并保存在本地的小块数据。Cookie 由浏览器(客户端)管理,每次请求都会自动发送到服务器。需要注意的是,由于隐私和安全原因,许多现代浏览器对第三方 Cookie(即由不同于当前浏览网站的域发送的 Cookie)进行了限制。此外,一些用户也可能选择禁用 Cookie,这可能会影响依赖于 Cookie 的功能正常运作。

特点

  1. 存储限制:每个域名下可以存储的 Cookie 数量有限,通常不超过 20 个。
  2. 大小限制:每个 Cookie 的大小通常限制在 4 KB 左右。
  3. 安全问题:Cookie 可以被客户端篡改,因此不适用于存储敏感信息。
  4. 作用域:Cookie 可以设置作用域,例如仅在当前域名下有效。

分类

此外,Cookie 还可以分为会话 Cookie 和持久性 Cookie:

  • 会话 Cookie 只在用户与网站交互期间存在(存放在客户端内存),并且在用户关闭浏览器时就会被删除。
  • 持久性 Cookie 有一个明确的过期日期,即使用户关闭浏览器后,Cookie 也会被保留 (存放在客户端硬盘),直到过期日期为止。

用途

  • 会话管理:通过 Cookie 识别用户身份,实现用户登录状态的持久化。
  • 个性化设置:存储用户偏好设置,如语言、主题等。

流程

Cookies 是网站用来在用户浏览器上存储小量数据的一种机制。它们用于跟踪用户的活动,保存用户的偏好设置,以及实现其他功能性的需求。下面是 Cookie 的基本工作流程:

  1. 用户首次访问一个网站(此时不存在 Cookie),浏览器向服务器发送请求。
  2. 服务器处理请求,并根据需要将数据发送给浏览器,同时可以附加一个或多个 Cookie。Set-Cookie: name = value
  3. 浏览器接收到响应后,会检查是否有任何 Cookie 信息,并存储这些 Cookie 在本地硬盘上。
  4. 当浏览器后续请求同一站点时,浏览器都会自动包含这个 Cookie 在 HTTP 请求头中 发送给服务器。
  5. 服务器根据接收到的 Cookie 来识别用户,从而提供个性化的网页内容或者恢复用户的偏好设置等。
    image-20241001152305819

常用方法

image-20241001152320560

案例

import java.io.IOException;
import java.io.PrintWriter;

//tomcat版本10.1 用的jakarta,版本较低可将jakarta换为javax!
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/CookieDemo")
public class CookieDemo extends HttpServlet{

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	// TODO Auto-generated method stub
	PrintWriter out = resp.getWriter();
	Cookie[] cookies = req.getCookies();
	resp.setContentType("text/html;charset = UTF-8");
	boolean flag = false;
	if(cookies!=null) {
		for (Cookie cookie : cookies) {
			if(cookie.getName().equals("ip")) {
				out.write("welcome!上次ip为:"+ cookie.getValue());
				flag = true;
				break;
			}
		}
			if(!flag) {
				out.write("欢迎新用户!");
				Cookie c = new Cookie("ip",req.getRemoteAddr());
				c.setMaxAge(1*60);//单位:s  这里为1min
				resp.addCookie(c);
			}
		}
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// TODO Auto-generated method stub
		super.doPost(req, resp);
	}
}

image-20241001152344437

Session 详解

Session 是服务器端用来跟踪用户会话的一种机制。当用户首次访问服务器时,服务器会创建一个 Session,并生成一个唯一的 Session ID,这个 ID 通常会存储在 Cookie 中,随请求发送回服务器。

特点:

  1. 存储位置:Session 数据存储在服务器端,安全性较高。
  2. 存储限制:由于存储在服务器,理论上没有大小限制,但会占用服务器资源。
  3. 生命周期:可以设置 Session 的过期时间,过期后自动销毁。
  4. 作用域:通常与浏览器窗口或标签页的生命周期相关。

用途:

  • 用户身份验证:存储用户登录后的会话信息。
  • 购物车:存储用户的购物车数据。

常用功能

image-20241001152359210

流程

  1. 用户发起请求:当用户打开浏览器并访问一个网站时,浏览器向服务器发送一个 HTTP 请求。
  2. 服务器创建 Session:服务器接收到请求后,如果发现这是一个新用户(即没有可用的 Session ID),则会创建一个新的 Session 并分配一个唯一的 Session ID 给这个用户。如果用户之前已经有过会话,则服务器会从请求中提取已有的 Session ID。
  3. 存储 Session 数据:服务器会在服务器端存储与 Session ID 相关的信息,比如用户的偏好设置、购物车中的商品或者其他状态信息。
  4. 返回响应和 Session ID:服务器处理完请求后,会将处理结果和 Session ID 一起返回给客户端。通常,Session ID 是通过 Cookie 或者 URL 参数传递给客户端的。
  5. 客户端存储 Session ID:客户端(通常是浏览器)接收到响应后,会存储 Session ID(如果通过 Cookie 传递的话)。对于后续请求,浏览器会自动将 Session ID 发送给服务器。
  6. 使用 Session 数据:对于用户后续的请求,服务器会读取 Session ID,并查找与此 ID 相关联的数据来个性化用户的体验或继续处理用户的请求。
  7. Session 的结束:Session 通常会在一定时间后因无活动而超时,或者当用户显式地登出时结束。服务器会清除与该 Session ID 相关的数据。
    image-20241001152414209

补充说明

Session 的创建

Session 的超时时间设置

设置 Session 超时可以通过多种方式来实现,这取决于使用的编程语言和框架。以下是三种常见的方法来设置 Session 的超时时间:

1. Tomcat 全局配置

image-20241001152543133

2. 通过配置文件设置

许多 Web 应用框架允许你在配置文件中设置 Session 的超时时间。这种方式的好处是可以全局控制所有 Session 的超时时间,而无需修改代码。

示例:jsp 项目中web. Xml 中的配置

image-20241001152627837

<session-config>
	<session-timeout>2</session-timeout>
</session-config>

示例:Java Spring Boot 中的配置

在 Spring Boot 中,可以通过 application.propertiesapplication.yml 文件来设置 Session 的超时时间:

server.tomcat.session-timeout=30 # 单位为分钟

3. 在代码中动态设置

在某些情况下,你可能希望针对不同的用户或操作设置不同的 Session 超时时间。这时可以在代码中动态地设置超时时间。

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class SessionServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 获取 HttpSession 对象
        HttpSession session = request.getSession();
        
        // 设置 Session 的超时时间为 30 分钟(以毫秒为单位)
        long timeoutInMillis = 30 * 60 * 1000; // 30 minutes
        session.setMaxInactiveInterval(timeoutInMillis / 1000); // 注意这里需要转换成秒
        
        // 输出确认信息
        response.getWriter().println("Session timeout set to " + session.getMaxInactiveInterval() + " seconds.");
    }
}

4. 使用中间件或过滤器,拦截器(本质还是 3)

对于一些框架,如 Node. Js 或者 Ruby on Rails,你可以编写中间件或者过滤器来控制 Session 的超时时间。
在 Java Web 开发中,Filter 和 Interceptor 是两种不同的机制,它们都可以用来拦截请求并在请求处理前后执行某些操作。尽管 Filter 和 Interceptor 都可以用来实现类似的功能,但是它们的应用场景和配置方式略有不同。

使用 Filter 设置 Session 的超时时间

Filter 是 Servlet 规范的一部分,可以直接在 Servlet 容器中使用。如果你想在 Filter 中设置 Session 的超时时间,可以通过以下步骤实现:

  1. 创建 Filter 类:首先创建一个实现了 javax.servlet.Filter 接口的类。
  2. 重写 doFilter 方法:在这个方法中,你可以获取 HttpSession 并设置其超时时间。
  3. 注册 Filter:在 web. Xml 中或者通过注解的方式注册你的 Filter。
示例代码
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebFilter(urlPatterns = {"/*"})
public class SessionTimeoutFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化方法,可以在这里进行一些初始化工作
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 获取 HttpSession 对象
        HttpSession session = httpRequest.getSession();
        
        // 设置 Session 的超时时间为 30 分钟(以秒为单位)
        int timeoutInSeconds = 30 * 60; // 30 minutes
        session.setMaxInactiveInterval(timeoutInSeconds);
        
        // 执行下一个 Filter 或者目标资源
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // 销毁方法,可以在这里进行一些清理工作
    }
}
使用 Interceptor 设置 Session 的超时时间

Interceptor 是特定框架(如 Spring MVC 或 Struts 2)提供的机制,用于拦截请求并在请求处理前后执行操作。如果你使用的是 Spring MVC,可以通过定义一个 HandlerInterceptor 来设置 Session 的超时时间。

示例代码
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class SessionTimeoutInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        
        // 获取 HttpSession 对象
        HttpSession session = request.getSession();
        
        // 设置 Session 的超时时间为 30 分钟(以秒为单位)
        int timeoutInSeconds = 30 * 60; // 30 minutes
        session.setMaxInactiveInterval(timeoutInSeconds);
        
        return true; // 返回 true 表示继续处理请求
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        // 在请求处理之后执行的操作
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        // 在请求完成之后执行的操作
    }
}
注册 Interceptor

要让 Interceptor 生效,还需要在 Spring MVC 的配置类中注册它:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SessionTimeoutInterceptor());
    }
}

Session 的销毁

情形一

Session 超时

情形二

调用 HttpSession 的 invalidate ()

情形三

会话式 session 保存 session-id 时,用户关闭浏览器

案例

![[Pasted image 20240929213640.png]]
login. jsp 文件如下

<%@ page contentType="text/html; charset=UTF-8"%>  
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
<html>  
<head>  
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">  
<title>Login</title>  
</head>  
<body>  
  <fieldset>    
  <legend>登录</legend>   
<!-- 表单数据的提交方式为POST -->  
    <form action="SessionDemoServlet" method="post">  
        <table cellpadding="2" align="center">  
            <tr>                
            <td align="right">用户名:</td>  
                <td>                    
                <!-- 1.文本输入框控件 -->  
                    <input type="text" name="username" />      
                </td>  
            </tr>            
            <tr>                
            <td align="right">密码:</td>  
                <!-- 2.密码输入框控件 -->  
                <td><input type="password" name="password" /></td>  
            </tr>            
            <tr>                
                <td colspan="2"  align="center">  
                    <!-- 3.提交按钮控件 -->  
                    <input type="submit" value="登录" />    
<!-- 4.重置按钮控件,单击后会清空当前form -->  
                    <input type="reset" value="重填" />      
                </td>  
            </tr>        
        </table>    
    </form>   
    </fieldset>
</body>  
</html>

welcome. jsp 如下

<%@ page contentType="text/html;charset=UTF-8"%>  
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
<html>  
<head>  
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">  
<title>Insert title here</title>  
</head>  
<body>  
欢迎<%= session.getAttribute("name") %>,登陆成功!  
<a href="LogoutServlet">退出</a>  
</body>  
</html>

SessionDemoServlet. java

import java.io.IOException;  
import java.io.PrintWriter;  
  
import jakarta.servlet.ServletException;  
import jakarta.servlet.annotation.WebServlet;  
import jakarta.servlet.http.HttpServlet;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import jakarta.servlet.http.HttpSession;  
  
@WebServlet("/SessionDemoServlet")  
public class SessionDemoServlet extends HttpServlet{  
    protected void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException,ServletException {  
       response.setContentType("text/html;charset = utf-8");  
         
       String userName = request.getParameter("username");  
       String password = request.getParameter("password");  
       if(userName.equals("芩离")&&password.equals("123456")) {//改成自己的!  
          HttpSession session = request.getSession();  
          session.setAttribute("name", userName);  
          response.sendRedirect("welcome.jsp");  
       }  
       else{  
          PrintWriter out = response.getWriter();  
          out.print("用户名或密码错误,返回<a href=\'login.jsp\'>登录</a>");
          response.setHeader("refresh","3;url = login.jsp");//失败后三秒跳转登录页
       }  
    }  
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {  
       super.doGet(request, response);  
    }  
}

LogoutServlet. java

import java.io.IOException;  
  
import jakarta.servlet.ServletException;  
import jakarta.servlet.annotation.WebServlet;  
import jakarta.servlet.http.HttpServlet;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import jakarta.servlet.http.HttpSession;  
  
@WebServlet("/LogoutServlet")  
public class LogoutServlet extends HttpServlet{  
  
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
       HttpSession session = req.getSession();  
       session.removeAttribute("name");  
       session.invalidate();  
       resp.sendRedirect("login.jsp");  
    }  
      
}

Token 详解

Token 是一种无状态的认证机制,通常用于 RESTful API。Token 通常是一个加密的字符串,包含了用户的身份信息和一些验证信息。

特点:

  1. 无状态:服务器不需要存储 Session 信息,每次请求都独立验证 Token。
  2. 安全性:Token 可以包含过期时间,减少被盗用的风险。
  3. 跨域:Token 可以跨域使用,适合分布式系统。
  4. 自定义:Token 的内容可以根据需要自定义。

常见类型:

  • JWT(JSON Web Token):一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明。
  • OAuth:一个行业标准的协议,允许用户提供一个令牌来访问他们存储在另外的服务提供者上的信息,而无需将他们的凭据提供给第三方应用。

用途:

  • API 认证:用于保护 RESTful API,确保请求者的身份。
  • 单点登录(SSO):允许用户使用一套凭证访问多个系统。
    image-20241001152705939

jwt 工具类及其使用

依赖

要想使用JWT令牌,需要先引入JWT的依赖:

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

工具类

import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.JwtBuilder;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
import java.nio.charset.StandardCharsets;  
import java.util.Date;  
import java.util.Map;  
  
public class JwtUtil {  
    /**  
     * 生成jwt  
     * 使用Hs256算法, 私匙使用固定秘钥  
     *  
     * @param secretKey jwt秘钥  
     * @param ttlMillis jwt过期时间(毫秒)  
     * @param claims    设置的信息  
     * @return  
     */  
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {  
        // 指定签名的时候使用的签名算法,也就是header那部分  
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;  
  
        // 生成JWT的时间  
        long expMillis = System.currentTimeMillis() + ttlMillis;  
        Date exp = new Date(expMillis);  
  
        // 设置jwt的body  
        JwtBuilder builder = Jwts.builder()  
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的  
                .setClaims(claims)  
                // 设置签名使用的签名算法和签名使用的秘钥  
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))  
                // 设置过期时间  
                .setExpiration(exp);  
  
        return builder.compact();  
    }  
  
    /**  
     * Token解密  
     *  
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个  
     * @param token     加密后的token  
     * @return  
     */  
    public static Claims parseJWT(String secretKey, String token) {  
        // 得到DefaultJwtParser  
        Claims claims = Jwts.parser()  
                // 设置签名的秘钥  
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))  
                // 设置需要解析的jwt  
                .parseClaimsJws(token).getBody();  
        return claims;  
    }  
}

校验过滤器的注册

@Configuration  
@Slf4j  
public class WebMvcConfiguration extends WebMvcConfigurationSupport {  
  
    @Autowired  
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;   
    /**  
     * 注册自定义拦截器  
     *  
     * @param registry  
     */  
    protected void addInterceptors(InterceptorRegistry registry) {  
        log.info("开始注册自定义拦截器...");  
        registry.addInterceptor(jwtTokenAdminInterceptor)  
                .addPathPatterns("/admin/**")  
                .excludePathPatterns("/admin/employee/login");  
    }
}

令牌校验

import com.sky.constant.JwtClaimsConstant;  
import com.sky.context.BaseContext;  
import com.sky.properties.JwtProperties;  
import com.sky.utils.JwtUtil;  
import io.jsonwebtoken.Claims;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Component;  
import org.springframework.web.method.HandlerMethod;  
import org.springframework.web.servlet.HandlerInterceptor;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
  
/**  
 * jwt令牌校验的拦截器  
 */  
@Component  
@Slf4j  
public class JwtTokenAdminInterceptor implements HandlerInterceptor {  
  
    @Autowired  
    private JwtProperties jwtProperties;  
  
    /**  
     * 校验jwt  
     *     
     * @param request  
     * @param response  
     * @param handler  
     * @return  
     * @throws Exception  
     */    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        //判断当前拦截到的是Controller的方法还是其他资源  
        if (!(handler instanceof HandlerMethod)) {  
            //当前拦截到的不是动态方法,直接放行  
            return true;  
        }  
  
        //1、从请求头中获取令牌  
        String token = request.getHeader(jwtProperties.getAdminTokenName());  
  
        //2、校验令牌  
        try {  
            log.info("jwt校验:{}", token);  
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);  
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());  
            log.info("当前员工id:{}", empId);  
            BaseContext.setCurrentId(empId);  
            //3、通过,放行  
            return true;  
        } catch (Exception ex) {  
            //4、不通过,响应401状态码  
            response.setStatus(401);  
            return false;  
        }  
    }  
}

Controller层

@PostMapping("/login")
    @ApiOperation(value = "员工登录")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
        log.info("员工登录:{}", employeeLoginDTO);

        Employee employee = employeeService.login(employeeLoginDTO);

        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(employee.getId())
                .userName(employee.getUsername())
                .name(employee.getName())
                .token(token)
                .build();

        return Result.success(employeeLoginVO);
    }

Service层

public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);

        //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
        if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }

        //密码比对
        // 对前端明文密码进行md5加密,然后再进行比对
        password = DigestUtils.md5DigestAsHex(password.getBytes());//md5加密
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

        if (Objects.equals(employee.getStatus(), StatusConstant.DISABLE)) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }

        //3、返回实体对象
        return employee;
    }

Mapper层

@Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);
posted @ 2024-10-01 15:34  yuanyulinyi  阅读(4)  评论(0编辑  收藏  举报