SpringBoot+JWT实现注册、登录、状态续签流程分析

目录
  • 一、实现流程
    • 1.注册
    • 2.登录
    • 3.登录保持【状态续签】
  • 二、实现方法
    • 1.引入依赖
    • 2.application配置文件
    • 3.mysql建表
    • 4.Bean
  • 三、测试
    • 1.注册
    • 2.登录
    • 3.状态续签【登录保持】

一、实现流程

1.注册

2.登录

3.登录保持【状态续签】

二、实现方法

项目结构

1.引入依赖

<!-- spring-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql连接器 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Mybatis Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3.4</version>
</dependency>
<!-- druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
<!-- jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

2.application配置文件

spring:
  application:
    name: jwtLogin   # 应用名称
  datasource:
    druid:
      url: jdbc:mysql://192.168.0.111:3306/login_test?useSSL=false&serverTimezone=UTC
      username: samon
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
server:
  port: 8848   # 应用服务 WEB 访问端口

3.mysql建表

用户表

4.Bean

1.bean/user.java
用户bean

package com.cxstar.bean;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
@Data
public class User {
    // 自增长id
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String userName;
    private String passWord;
    private Date createTime;
    private Date lastLogin;
    public  User() {}
    public User(Integer id, String userName) {
        this.id = id;
        this.userName = userName;
    }
}

2.bean/ServiceRes.java
统一Service返回类

package com.cxstar.bean;
import lombok.Data;
@Data
public class ServiceRes {
    private Integer code;
    private String msg;
    private String jwt;
    private ServiceRes() {}
    public ServiceRes(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public ServiceRes(Integer code, String msg, String jwt) {
        this.code = code;
        this.msg = msg;
        this.jwt = jwt;
    }
}

3.bean/ControllerRes.java
统一Controller返回类

package com.cxstar.bean;
import lombok.Data;
@Data
public class ControllerRes {
    private Integer code;
    private String msg;
    public ControllerRes(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

5.Mapper mapper/UserMapper.java
继承MP

package com.cxstar.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxstar.bean.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

6.service service.userService.interface
登录、注册、改密业务【状态续签测试】

package com.cxstar.service;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
public interface userService {
    // 注册
    ServiceRes register(User user);
    // 登录
    ServiceRes login(User user);
    // 改密【带权限业务,用于状态续签测试】
    ServiceRes changePassWord(User user);
}

service.impl.UserServiceImpl.java
登录、注册、改密业务【状态续签测试】

package com.cxstar.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
import com.cxstar.mapper.UserMapper;
import com.cxstar.service.userService;
import com.cxstar.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class UserServiceImpl implements userService {
    @Autowired
    UserMapper userMapper;
    /**
     * 注册
     * @param user 用户类
     * @return ServiceRes
     */
    @Override
    public ServiceRes register(User user) {
        // 判断用户名是否唯一
        if(this.checkUserNameIsUnique(user)) {
            // 判断用户名密码是否合法
            if(this.checkUserNameAndPassword(user)) {
                // 密码MD5加密
                user.setPassWord(this.MD5Code(user.getPassWord()));
                // 加入创建时间
                user.setCreateTime(new Date());
                // 入库
                userMapper.insert(user);
                return new ServiceRes(1, "注册成功");
            } else return new ServiceRes(-1, "用户名或密码不合法");
        } else return new ServiceRes(-1, "用户名已存在");
    }
    /**
     * 登录
     * @param user 用户类
     * @return ServiceRes
     */
    @Override
    public ServiceRes login(User user) {
        // 判断用户名密码是否合法
        if(this.checkUserNameAndPassword(user)) {
            // 密码MD5加密
            user.setPassWord(this.MD5Code(user.getPassWord()));
            // 检查用户是否存在
            User curUser = this.checkUserIsExit(user);
            if(curUser!=null) {
                // 更新用户最后登录时间
                curUser.setLastLogin(new Date());
                userMapper.updateById(curUser);
                // 生成jwt
                Map<String, String> payload = new HashMap<>();
                payload.put("userId", curUser.getId().toString()); // 加入一些非敏感的用户信息
                payload.put("userName", curUser.getUserName());    // 加入一些非敏感的用户信息
                String jwt = JwtUtil.generateToken(payload);
                return new ServiceRes(1, "登录成功", jwt);
            } else return new ServiceRes(-1, "用户名或密码错误");
        } else return new ServiceRes(-1, "用户名或密码不合法");
    }
    /**
     * 改密业务
     * @return ServiceRes
     */
    @Override
    public ServiceRes changePassWord(User user) {
        if(this.updatePassWord(user)) return new ServiceRes(1, "改密成功");
        else return new ServiceRes(-1, "改密失败");
    }
    /**
     * 非对称加密
     * @param text 明文
     * @return 密文
     */
    private String MD5Code(String text) {
        return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8));
    }
    /**
     * 修改密码方法
     * @param user 传入用户名和新密码
     * @return 改密成功返回 true 失败返回 false
     */
    private Boolean updatePassWord(User user) {
        // 密码非对称加密
        user.setPassWord(this.MD5Code(user.getPassWord()));
        // 更新密码
        return userMapper.updateById(user)>0;
    }
    /**
     * 检查用户是否存在【用户名密码相同】
     * @param user 用户类
     * @return 用户存在返回 用户对象 不存在返回 null
     */
    private User checkUserIsExit(User user) {
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getUserName, user.getUserName());
        lqw.eq(User::getPassWord, user.getPassWord());
        return userMapper.selectOne(lqw);
    }
    /**
     * 判断用户名是否唯一
     * @param user 用户类
     * @return 唯一返回 true 不唯一返回 false
     */
    private Boolean checkUserNameIsUnique(User user) {
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getUserName, user.getUserName());
        List<User> userList = userMapper.selectList(lqw);
        return userList.size() == 0;
    }
    /**
     * 判断用户名密码是否合法
     * @param user 用户类
     * @return 满足 【英文字母、数字、下划线】 返回 true,否则返回 false
     */
    private Boolean checkUserNameAndPassword(User user) {
        String regex = "^[_a-z0-9A-Z]+$";
        return user.getUserName().matches(regex) && user.getPassWord().matches(regex);
    }

}

6.Controller controller/UserController.java
登录、注册、改密业务【状态续签测试】

package com.cxstar.controller;
import com.cxstar.bean.ControllerRes;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
import com.cxstar.service.userService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
    @Autowired
    userService userService;
    @PostMapping("/register")
    public ControllerRes register(User user) {
        // 注册
        ServiceRes serviceRes = userService.register(user);
        return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
    }
    @PostMapping("/login")
    public ControllerRes login(User user, HttpServletResponse response) {
        // 登录
        ServiceRes serviceRes = userService.login(user);
        // 登录成功后往响应头插入jwt
        if(serviceRes.getJwt() != null) response.addHeader("access-token", serviceRes.getJwt());
        return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
    }
    @PutMapping("/pwd")
    public ControllerRes changePassWord(User user, HttpServletRequest request) {
        // 取出jwt中的用户
        User jwtUser = (User)request.getAttribute("jwt-user");
        // 合并jwt中用户的用户名与传入用户的新密码
        // 此处不能直接使用传入的用户名,防止恶意修改其他用户的密码
        user.setId(jwtUser.getId());
        // 改密
        ServiceRes serviceRes = userService.changePassWord(user);
        return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
    }
}

7.JWT工具类 utils/JwtUtil.java
生成和解析 token 的方法

package com.cxstar.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
    // 签名密钥
    private static final String SECRET = "hello JWT *%$#$&";
    /**
     * 生成token
     * @param payload token携带的信息
     * @return token字符串
     */
    public static String generateToken(Map<String,String> payload){
        // 指定token过期时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR, 24);  // 24小时
        JWTCreator.Builder builder = JWT.create();
        // 构建payload
        payload.forEach(builder::withClaim);
        // 指定签发时间、过期时间 和 签名算法,并返回token
        String token = builder.withIssuedAt(new Date()).withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET));
        return token;
    }

    /**
     * 解析token
     * @param token token字符串
     * @return 解析后的token类
     */
    public static DecodedJWT decodeToken(String token){
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        return decodedJWT;
    }
}

8.HandlerInterceptor拦截器 interceptor.java
拦截器业务实现

package com.cxstar.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.cxstar.bean.ControllerRes;
import com.cxstar.bean.User;
import com.cxstar.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 拦截需要授权的接口
 */
@Slf4j
public class PermisssionInterceptor implements HandlerInterceptor {

    // 目标方法执行前调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        // 检查用户JWT
        String jwt = request.getHeader("access-token");

        // 校验并取出私有信息
        try {

            // token 解码
            DecodedJWT dj = JwtUtil.decodeToken(jwt);

            // 取出基本用户信息加入请求头 --------------------------------------------------------------------------------
            String userId = dj.getClaim("userId").asString();
            String userName = dj.getClaim("userName").asString();
            // jwt校验合格的,将 jwt 中存的用户信息加入请求头,不合格的,请求头存个空用户
            request.setAttribute("jwt-user", userId!=null?new User(Integer.valueOf(userId), userName):new User());
            // -------------------------------------------------------------------------------------------------------

            // 计算当前时间是否超过过期时间的一半,如果是就帮用户续签 --------------------------
            // 此处并不是永久续签,只是为 大于过期时间的一半 且 小于过期时间 的 token 续签
            Long expTime = dj.getExpiresAt().getTime();
            Long iatTime = dj.getIssuedAt().getTime();
            Long nowTime = new Date().getTime();
            if((nowTime-iatTime) > (expTime-iatTime)/2) {
                // 生成新的jwt
                Map<String, String> payload = new HashMap<>();
                payload.put("userId", userId); // 加入一些非敏感的用户信息
                payload.put("userName", userName);    // 加入一些非敏感的用户信息
                String newJwt = JwtUtil.generateToken(payload);
                // 加入返回头
                response.addHeader("access-token", newJwt);
            }
            // -----------------------------------------------------------------------

            return true;

        } catch (JWTDecodeException e) {
            log.error("令牌错误");
            addResBody(response, new ControllerRes(-1, "令牌错误"));  // 新增返回体
            return false;

        } catch (TokenExpiredException e) {
            log.error("令牌过期");
            addResBody(response, new ControllerRes(-1, "令牌过期"));  // 新增返回体
            return false;
        }

    }

    // 目标方法执行后调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    // 页面渲染前调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    private void addResBody(HttpServletResponse response, ControllerRes res) throws IOException {

        response.setStatus(HttpServletResponse.SC_FORBIDDEN);        // 设置状态码

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(JSONObject.toJSONString(res));
        out.flush();
        out.close();

    }

}

config/PermissionWebConfig.java
拦截器拦截规则

package com.cxstar.config;
import com.cxstar.interceptor.PermisssionInterceptor;
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 PermissionWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermisssionInterceptor())
                .addPathPatterns("/**")    // 拦截哪些页面
                .excludePathPatterns("/user/login", "/user/register");   // 放行哪些页面
    }
}

三、测试

1.注册

注册成功

数据入库

2.登录

登录成功

查看登录后返回的token

3.状态续签【登录保持】

使用上一步登录返回的 token 请求改密业务

当 JWT 存在时间小于 JWT 过期时间的一半时
业务会执行成功
执行业务不会返回续签的 token

当 JWT 存在时间大于 JWT 过期时间的一半 且 小于过期时间 时
业务会执行成功
执行业务会返回续签的 token,前端的下次请求需要使用新续签的 token

当 JWT 存在时间大于 JWT 过期时间 时
业务会执行失败
执行业务不会返回续签的 token

到此这篇关于SpringBoot+JWT实现注册、登录、状态续签的实战教程的文章就介绍到这了,更多相关SpringBoot JWT登录状态续签内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot+SpringSecurity+JWT实现用户登录和权限认证示例

    如今,互联网项目对于安全的要求越来越严格,这就是对后端开发提出了更多的要求,目前比较成熟的几种大家比较熟悉的模式,像RBAC 基于角色权限的验证,shiro框架专门用于处理权限方面的,另一个比较流行的后端框架是Spring-Security,该框架提供了一整套比较成熟,也很完整的机制用于处理各类场景下的可以基于权限,资源路径,以及授权方面的解决方案,部分模块支持定制化,而且在和oauth2.0进行了很好的无缝连接,在移动互联网的授权认证方面有很强的优势,具体的使用大家可以结合自己的业务场景进行选

  • springboot+jwt+springSecurity微信小程序授权登录问题

    场景重现:1.微信小程序向后台发送请求 --而后台web采用的springSecuriry没有token生成,就会拦截请求,,所以小编记录下这个问题 微信小程序授权登录问题 思路 参考网上一大堆资料 核心关键字: 自定义授权+鉴权 (说的通俗就是解决办法就是改造springSecurity的过滤器) 参考文章 https://www.jb51.net/article/204704.htm 总的来说的 通过自定义的WxAppletAuthenticationFilter替换默认的UsernameP

  • SpringBoot使用JWT实现登录验证的方法示例

    什么是JWT JSON Web Token(JWT)是一个开放的标准(RFC 7519),它定义了一个紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息.这些信息可以通过数字签名进行验证和信任.可以使用秘密(使用HMAC算法)或使用RSA的公钥/私钥对来对JWT进行签名. 具体的jwt介绍可以查看官网的介绍:https://jwt.io/introduction/ jwt请求流程 引用官网的图片 中文介绍: 用户使用账号和面发出post请求: 服务器使用私钥创建一个jwt: 服务器返

  • SpringBoot+Vue+JWT的前后端分离登录认证详细步骤

    前后端分离的概念在现在很火,最近也学习了一下前后端分离的登录认证. 创建后端springboot工程 这个很简单了,按照idea的一步一步创建就行 文件目录结构: pom文件依赖导入. <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </

  • vue+springboot+shiro+jwt实现登录功能

    目录 1.导入依赖 2.JWTToken 替换 Shiro 原生 Token 3.JWT token 工具类,提供JWT生成.校验.获取token存储的信息 4.JWTFilter请求拦截 5.登录授权realm 6.shiro配置 7.登录web端 8.异常处理 9.缓存调用登录接口传过来的token 10.请求头设置,带上token 11.生产环境nginx配置 公司开发的系统原先的用户信息是基于shiro session 进行管理,但是session不适用于app端,并且服务器重启后需要重

  • springboot+jwt+微信小程序授权登录获取token的方法实例

    目录 前言 配置 XcxAuthenticationProvider XcxAuthenticationToken 小程序授权登录 前言 我们有时候在开发中,遇到这样的问题,就是我们需要小程序授权登录我们自己的后台,通过小程序的信息换取我们自己后台的token,实现账号密码.小程序授权登录的多种登录方式. 配置 在 SecurityConfig文件中配置 XcxAuthenticationProvider public class XcxAuthenticationProvider implem

  • SpringBoot集成Spring Security用JWT令牌实现登录和鉴权的方法

    最近在做项目的过程中 需要用JWT做登录和鉴权 查了很多资料 都不甚详细 有的是需要在application.yml里进行jwt的配置 但我在导包后并没有相应的配置项 因而并不适用 在踩过很多坑之后 稍微整理了一下 做个笔记 一.概念 1.什么是JWT Json Web Token (JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519) 该token被设计为紧凑且安全的 特别适用于分布式站点的单点登录(SSO)场景 随着JWT的出现 使得校验方式更加简单便

  • SpringBoot JWT实现token登录刷新功能

    目录 1. 什么是JWT 2. JWT组成部分 3. JWT加密方式 4.实战 5.总结 1. 什么是JWT Json web token (JWT) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.简答理解就是一个身份凭证,用于服务识别. JWT本身是无状态的,这点有别于传统的session,不在服务端存储凭证.这种特性使其在分布式场景,更便于扩展使用. 2. JWT组成部分 JWT有三部分组成,头部(header),载荷(payload),是签名(signature). 头

  • SpringBoot+JWT实现注册、登录、状态续签流程分析

    目录 一.实现流程 1.注册 2.登录 3.登录保持[状态续签] 二.实现方法 1.引入依赖 2.application配置文件 3.mysql建表 4.Bean 三.测试 1.注册 2.登录 3.状态续签[登录保持] 一.实现流程 1.注册 2.登录 3.登录保持[状态续签] 二.实现方法 项目结构 1.引入依赖 <!-- spring-web --> <dependency> <groupId>org.springframework.boot</groupId

  • SpringBoot如何实现持久化登录状态获取

    目录 SpringBoot 持久化登录状态获取 1.编写登录的controller文件 2.编写首页Controller逻辑 3.运行测试,成功 SpringBoot 实现登录登出,登录态管理 1.设计表结构 2.controller层 3.创建请求拦截器 4.登出 SpringBoot 持久化登录状态获取 1.编写登录的controller文件 写入cookie //登陆成功后 //...将用户账号信息存入数据库中 //写cookie,(因存入数据库,无需写入session了) respons

  • 基于redis的小程序登录实现方法流程分析

    这张图是小程序的登录流程解析: 小程序登陆授权流程: 在小程序端调用wx.login()获取code,由于我是做后端开发的这边不做赘述,直接贴上代码了.有兴趣的直接去官方文档看下,链接放这里: wx.login() wx.login({ success (res) { if (res.code) { //发起网络请求 wx.request({ url: 'https://test.com/onLogin', data: { code: res.code } }) } else { console

  • SpringBoot通过ThreadLocal实现登录拦截详解流程

    目录 1 前言 2 具体类 2.1HandlerInterceptor 2.2WebMvcConfigurer 3 代码实践 1 前言 注册登录可以说是平时开发中最常见的东西了,但是一般进入到公司之后,像这样的功能早就开发完了,除非是新的项目.这两天就碰巧遇到了这样一个需求,完成pc端的注册登录功能. 实现这样的需求有很多种方式:像 1)HandlerInterceptor+WebMvcConfigurer+ThreadLocal 2)Filter过滤器 3)安全框架Shiro(轻量级框架) 4

  • Vue登录注册并保持登录状态的方法

    关于vue登录注册,并保持登录状态,是vue玩家必经之路,网上也有很多的解决方法,但是有一些太过于复杂,新手可能会看的一脸懵逼,现在给大家介绍一种我自己写项目在用而且并不难理解的一种方法. 项目中有一些路由是需要登录才可以进入的,比如首页,个人中心等等 有一些路由是不需要登录就可以进入,比如登录页,注册页,忘记密码等等 那如何判断路由是否需要登录呢?就要在路由JS里面做文章 在router.js中添加meta区分 比如登录注册页面,不需要登录即可进入,那么我们把meta中的isLogin标志设置

  • SpringBoot实现简单的登录注册的项目实战

    目录 第一步:建立简单的项目 第二步:建一个简单的数据表 第三步:配置文件如下: 第一步:建立简单的项目 第二步:建一个简单的数据表 第三步:配置文件如下: pom.xml文件配置: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/

随机推荐