开发者

SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

目录
  • 本文简介
  • 项目搭建及配置
  • 1.登录认证全栈实现 ->基础版
    • 1.1 后端实现
      • 1.1.1 架构设计
      • 1.1.2 实体类
      • 1.1.3 Controller
      • 1.1.4 Service
      • 1.1.5 Mapper
    • 1.2 前端实现
    • 2.Cookie/Session
      • 3.统一返回结果封装
        • 4.图形验证码
          • 5.MD5加密
            • 6.拦截器

              本文简介

              目的:

              Spring生态为Java后端开发提供了强大支持,但将分散的技术点整合成完整解决方案往往令人困惑。本文将以登录接口为切入点,系统演示如何将IOC/DI、MyBATis数据持久化、MD5加密、Session/Cookie管理、JWT令牌和拦截器机制融合运用,打造企业级认证方案

              技术栈:

              • 前端:html + css + javascript + jquery
              • 后端:SpringBoot + Mybatis + JWT

              搭建环境:

              • 数据库:mysql8.4.0
              • 项目结构:maven
              • 前端框架:Jquery
              • 后端框架:SpringBoot
              • JDK:17
              • 编译器:IDEA

              目录结构

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              项目搭建及配置

              1.创建SpringBoot3.0.0+项目并添加依赖:Spring Web、MyBatis Framework、MySQL Driver、Lombok

              2.初始化数据库:

              create database spring_blog_login charset utf8mb4;      
              use spring_blog_login;
              create table user_info (id int primary key auto_increment,user_name varchar(128) unique ,
                                      password varchar(128) not null,delete_flag int default 0,
                                      create_time datetime default now(),update_time datetime default now()
              );
              insert into user_info (user_name,password) values 
                                                             ('张三','123456'),
                                                             ('李四','123456'),
                                                             ('王五','123456');
              

              3.将application.properties修改为application.yml并添加如下配置:

              spring:
                datasource:
                  url: jdbc:mysql://127.0.0.1:3306/spring_blog_login?characterEncoding=utf8&useSSL=false
                  username: root
                  password: 123456
                  driver-class-name: com.mysql.cj.jdbc.Driver
              mybatis:
                configuration:
                  map-underscore-to-camel-case: true #自动驼峰转换
              server:
                port: 8080 #不显式设置默认为8080
              

              按住Ctrl + F5,如果程序能运行成功则说明搭建及配置都没问题(MySQL服务器必须要处于运行状态)

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              1.登录认证全栈实现 ->基础版

              1.1 后端实现

              1.1.1 架构设计

              本次登录功能采用Controller、Service、Mapper三层架构:Controller层依赖于Service层来执行业务逻辑并获取处理结果,而Service层又依赖于Mapper层来进行数据持久化操作

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              1.1.2 实体类

              实体类用于封装业务数据,需要与数据库表结构一一对应

              import lombok.Data;
              import java.util.Date;
              @Data
              public class UserInfo {
                  private Integer id;
                  private String userName;
                  private String password;
                  private Integer deleteFlag;
                  private Date createTime;
                  private Date updateTime;
              }
              

              1.1.3 Controller

              处理HTTP请求、参数校验、返回响应

              import org.example.springlogin.service.UserService;
              import org.springframework.beans.factory.annotation.Autowired;
              import org.springframework.util.StringUtils;
              import org.springframework.web.bind.annotation.RequestMapping;
              import org.springframework.web.bind.annotation.RestController;
              
              @RestController
              @RequestMapping("/user")
              public class UserController {
              
                  private final UserService userService;
              
                  @Autowired
                  public UserController(UserService userService) {
                      this.userService = userService;
                  }
              
                  @RequestMapping("/login")
                  public String login(String userName,String password) {
                      if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
                          return "用户或密码为空";
                      }
                      return userService.getUserInfoByUserName(userName,password);
                  }
              }
              

              1.1.4 Service

              业务逻辑处理

              import org.example.springlogin.mapper.UserMapper;
              import org.example.springlogin.model.UserInfo;
              import org.springframework.beans.factory.annotation.Autowired;
              import org.springframework.stereotype.Service;
              
              @Service
              public class UserService {
              
                  private final UserMapper userMapper;
              
                  @Autowired
                  public UserService(UserMapper userMapper) {
                      this.userMapper = userMapper;
                  }
              
                  public String getUserInfoByUserName(String userName,String password) {
                      UserInfo userInfo = userMapper.getUserInfoByUserName(userName);
                      if (userInfo == null) {
                          return "用户不存在";
                      }
                      if (!password.equals(userInfo.getPassword())) {
                          return "密码错误";
                      }
                      return "登录成功";
                  }
              }
              

              1.1.5 Mapper

              数据持久化操作

              import org.apache.ibatis.annotations.Mapper;
              import org.apache.ibatis.a编程nnotations.Select;
              import org.example.springlogin.model.UserInfo;
              
              @Mapper
              public interface UserMapper {
              
                  @Select("select * from user_info where user_name = #{userName}")
                  UserInfo getUserInfoByUserName(String userName);
              }
              

              1.2 前端实现

              Gitee:项目前端代码:https://gitee.com/lys3210728077/test.-java-ee-advanced/tree/master/spring-login/src/main/resources/static,Gitee上的前端代码是最新提交的,如下效果图仅作参考

              效果演示:

              • 1.用户或密码为空
              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              • 2.用户不存在
              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              • 3.密码错误
              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              • 4.登录成功
              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              2.Cookie/Session

              HTTP(超文本传输协议)设计为无状态协议,指服务器默认不保留客户端请求之间的任何状态信息。每个请求独立处理,服务器不会记忆之前的交互内容(如下图)

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              优点:

              • 请求独立性:每次请求被视为新请求,服务器不依赖历史请求数据
              • 简单高效:无状态设计降低服务器资源消耗,简化实现逻辑

              缺点:

              • 身份识别困难:需通过额外机制(如Cookies、Session)跟踪用户状态
              • 重复传输数据:每次请求需携带完整信息,可能增加冗余(如认证信息)

              cookie:是存储在客户端(浏览器)的小型文本数据,由服务器通过HTTP响应头Set-Cookie发送给客户端,并在后续请求中自动携带

              session:是存储在服务器端的用户状态信息,通常通过一个唯一的Session ID标识,该ID可能通过Cookie或URL传递

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              如上图片引用自我的博客:Java EE(13)——网络原理——应用层HTTP协议,服务器内部实际上专门开辟了一个session空间用于存储用户信息,每当新用户发送第一次请求时服务器会将用户信息存储在session中并生成一个session id通过Set-Cookie方法返回给客户端,即cookie

              session结构如下:

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              修改Controller类代码:

              import jakarta.servlet.http.HttpSession;
              import lombok.extern.slf4j.Slf4j;
              import org.example.springlogin.service.UserService;
              import org.springframework.beans.factory.annotation.Autowired;
              import org.springframework.util.StringUtils;
              import org.springframework.web.bind.annotation.RequestMapping;
              import org.springframework.web.bind.annotation.RestController;
              import java.util.HashMap;
              
              @RestController
              @RequestMapping("/user")
              @Slf4j
              public class UserController {
              
                  private final UserService userService;
              
                  @Autowired
                  public UserController(UserService userService) {
                      this.userService = userService;
                  }
              
                  @RequestMapping("/login")
                  public String login(String userName, String password, HttpSession session) {
                      log.info("接收到参数,userName:{},password:{}",userName,password);
                      if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
                          return "用户或密码为空";
                      }
                      String result = userService.getUserInfoByUserName(userName, password);
                      if (result.equals("登录成功")){
                          HashMap<String,String> map = new HashMap<>();
                          map.put("userName",userName);
                          map.put("password",password);
                          //将map作为用户信息存储到session/会话中
                          session.setAttribute("cookie", map);
                          log.info("登录成功");
                      }
                      return result;
                  }
              

              修改前端代码:

                  function login() {
                    $.AJAX({
                      url: '/user/login',
                      type: "post",
                      data:{
                        userName:$('#username').val(),
                        password:$('#password').val(),
                      },
                      success: function(result) {
                        alert(result);
                      },
                    })
                  }
              

              Fiddler抓包结果

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

               

              前端/浏览器按住Ctrl + Shift + i打开控制台点击应用程序/application,打开Cookie

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              3.统一返回结果封装

              统一返回结果封装是后端开发中的重要设计模式,能够保持API响应格式的一致性,便于前端处理

              1.创建枚举类:统一管理接口或方法的返回状态码和描述信息,标准化业务逻辑中的成功或失败状态

              import lombok.Getter;
              @Getter
              public enum ResultStatus {
                  SUCCESS(200,"成功"),
                  FAIL(-1,"失败"),
                  ;
              
                  private final Integer code;
                  private final String message;
              
                  ResultStatus(Integer code, String mes编程客栈sage) {
                      this.code = code;
                      this.message = message;
                  }
              }
              

              2.创建Result< T >类:主要用于规范服务端返回给客户端的响应数据格式。通过固定结构(状态码、错误信息、数据)确保前后端交互的一致性

              import lombok.Data;
              @Data
              //通过泛型<T>设计,可以灵活封装任意类型的数据对象到data字段
              public class Result<T> {
                  //业务码
                  private ResultStatus code;
                  //错误信息
                  private String errorMessage;
                  //数据
                  private T data;
              
                  public static <T> Result<T> success(T data) {
                      Result<T> result = new Result<>();
                      result.setCode(ResultStatus.SUCCESS);
                      result.setErrorMessage(null);
                      result.setData(data);
                      return result;
                  }
              
                  public static <T> Result<T> fail(String errorMessage) {
                      Result<T> result = new Result<>();
                      result.setCode(ResultStatus.FAIL);
                      result.setErrorMessage(errorMessage);
                      result.setData(null);
                      return result;
                  }
              }
              

              3.修改Controller代码

              @RestController
              @RequestMapping("/user")
              @Slf4j
              public class UserController {
              
                  private final UserService userService;
              
                  @Autowired
                  public UserController(UserService userService) {
                      this.userService = userService;
                  }
              
                  @RequestMapping("/login")
                  public Result<String> login(String userName, String password, HttpSession session) {
                      log.info("接收到参数,userName:{},password:{}",userName,password);
                      if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
                          return Result.fail("用户或密码为空");
                      }
                      String result = userService.getUserInfoByUserName(userName, password);
                      if (!result.equals("登录成功")){
                          return Result.fail(result);
                      }
                      HashMap<String,String> map = new HashMap<>();
                      map.put("userName",userName);
                      map.put("password",password);
                      //将map作为用户信息存储到session/会话中
                      session.setAttribute("cookie", map);
                      log.info("登录成功");
                      return Result.success(result);
                  }
              }
              

              4.修改前端代码

                  function login() {
                    $.ajax({
                      url: '/user/login',
                      type: "post",
                      data:{
                        userName:$('#username').val(),
                        password:$('#password').val(),
                      },
                      success: function(result) {
                        if (result.code === "SUCCESS") {
                          alert(result.data)
                        }else {
                          alert(result.error)
                        }
                      },
                    })
                  }
              

              4.图形验证码

              图形验证码(captcha)是一种区分用户是人类还是自动化程序的技术,主要通过视觉或交互任务实现。其核心意义体现在以下方面:

              • 防止自动化攻击:通过复杂图形或扭曲文字,阻止爬虫、暴力破解工具等自动化程序批量注册或登录,降低服务器压力
              • 提升安全性:在敏感操作(如支付、修改密码)前增加验证步骤,减少数据泄露或恶意操作风险

              Hutool提供了CaptchaUtil类用于快速生成验证码,支持图形验证码和GIF动态验证码。在pom.XML文件中添加图下配置:

              <dependency>
                  <groupId>cn.hutool</groupId>
                  <artifactId>hutool-all</artifactId>
                  <!-- 版本号应与springboot版本兼容 -->
                  <version>5.8.40</version>
              </dependency>
              

              1.创建CaptchaController类,用于生成验证码并返回给前端

              import cn.hutool.captcha.CaptchaUtil;
              import cn.hutool.captcha.LineCaptcha;
              import jakarta.servlet.http.HttpServletResponse;
              import jakarta.servlet.http.HttpSession;
              import lombok.extern.slf4j.Slf4j;
              import org.springframework.web.bind.annotation.RequestMapping;
              import org.springframework.web.bind.annotation.RestController;
              import java.io.IOException;
              
              @RestController
              @RequestMapping("/captcha")
              @Slf4j
              public class CaptchaController {
                  //设置过期时间
                  public final static long delay = 60_000L;
              
                  @RequestMapping("/get")
                  public void getCaptcha(HttpSession session, HttpServletResponse response) {
                      log.info("getCaptcha");
                      LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
                      //设置返回类型
                      response.setContentType("image/jpeg");
                      //禁止缓存
                      response.setHeader("Pragma", "No-cache");
                      try {
                          //通过响应输出生成的图形验证码
                          lineCaptcha.write(response.getOutputStream());
                          //保存code
                          session.setAttribute("CAPTCHA_SESSION_CODE", lineCaptcha.getCode());
                          //保存当前时间
                          session.setAttribute("CAPTCHA_SESSION_DATE", System.currentTimeMillis());
                          //关闭输出流
                          response.getOutputStream().close();
                      } catch (IOException e) {
                          throw new RuntimeException(e);
                      }
                  }
              }
              

              2.修改前端代码:最终版

              <!DOCTYPE html>
              <html lang="zh-CN">
              <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>微信登录</title>
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="external nofollow" >
                <link rel="stylesheet" href="css/login.css" rel="external nofollow" >
              </head>
              <body>
                <div class="login-container">
                  <div class="logo">
                    <i class="fab fa-weixin"></i>
                  </div>
                  <h2>微信登录</h2>
                  <form id="loginForm">
                    <div class="input-group">
                      <i class="fas fa-user"></i>
                      <label for="username"></label><input type="text" id="username" placeholder="请输入用户名" required>
                    </div>
                    <div class="input-group">
                      <i class="fas fa-lock"></i>
                      <label for="password"></label><input type="password" id="password" placeholder="请输入密码" required>
                    </div>
                    <div class="input-group">
                      <div class="captcha-container">
                        <label for="inputCaptcha"></label><input type="text" id="inputCaptcha" class="captcha-input" placeholder="输入验证码">
                        <img id="verificationCodeImg" src="/captcha/get" class="captcha-img" title="SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)" alt="SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)">
                      </div>
                    </div>
                    <div class="agreement">
                      <input type="checkbox" id="agreeCheck" checked>
                      <label for="agreeCheck">我已阅读并同意<a href="#" rel="external nofollow"  rel="external nofollow" >《服务条款》</a>和<a href="#" rel="external nofollow"  rel="external nofollow" >《隐私政策》</a></label>
                    </div>
                    <button type="submit" class="login-btn" onclick="login()">登录</button>
                  </form>
                  <div class="footer">
                    <p>版权所有 九转苍翎</p>
                  </div>
                </div>
                <!-- 引入jQuery依赖 -->
                <script src="js/jquery.min.js"></script>
                <script>
                  //刷新验证码
                  $("#verificationCodeImg").click(function(){
                    //new Date().getTime()).fadeIn()防止前端缓存
                    $(this).hide().attr('src', '/captcha/get?dt=' + newwww.devze.com Date().getTime()).fadeIn();
                  });
                  //登录
                  function login() {
                    $.ajax({
                      url: '/user/login',
                      type: "post",
                      data:{
                        userName:$('#username').val(),
                        password:$('#password').val(),
                        captcha:$('#inputCaptcha').val(),
                      },
                      success: function(result) {
                        console.log(result);
                        if (result.code === "SUCCESS") {
                          alert(result.data)
                        }else {
                          alert(result.error)
                        }
                      },
                    })
                  }
                </script>
              </body>
              </html>
              

              3.在UserController类新增captcha形参接收来自CaptchaController类的请求,并传递给UserService

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              import jakarta.servlet.http.HttpSession;
              import org.example.springlogin.controller.CaptchaController;
              import org.example.springlogin.mapper.UserMapper;
              import org.example.springlogin.model.UserInfo;
              import org.springframework.beans.factory.annotation.Autowired;
              import org.springframework.stereotype.Service;
              
              @Service
              public class UserService {
              
                  private final UserMapper userMapper;
              
                  @Autowired
                  public UserService(UserMapper userMapper) {
                      this.userMapper = userMapper;
                  }
              
                  public String getUserInfoByUserName(String userName, String password, String captcha, HttpSession session) {
                      UserInfo userInfo = userMapper.getUserInfoByUserName(userName);
                      if (userInfo == null) {
                          return "用户不存在";
                      }
                      if (!password.equals(userInfo.getPassword())) {
                          return "密码错误";
                      }
                      long saveTime = (long)session.getAttribute("CAPTCHA_SESSION_DATE");
                      if (System.currentTimeMillis() - saveTime > CaptchaController.delay) {
                          return "验证码超时";
                      }
                      if (!captcha.equalsIgnoreCase((String) session.getAttribute("CAPTCHA_SESSION_CODE"))) {
                          return "验证码错误";
                      }
                      return "登录成功";
                  }
              }
              

              实现效果:

              SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              5.MD5加密

              MD5(Message-Digest Algorithm 5)是一种广泛使用的哈希函数,可将任意长度数据生成固定长度(128位,16字节)的哈希值,通常表示为32位十六进制字符串,常用于校验数据完整性或存储密码。但因其安全性不足,通常结合盐值(Salt)配合使用

              • 不可逆性:无法通过哈希值反推原始数据
              • 唯一性:理论上不同输入产生相同哈希值的概率极低(哈希碰撞)
              • 固定长度:无论输入数据大小,输出均为32位十六进制字符串

              1.创建SecurityUtil类用于生成和验证密文

              import org.springframework.util.DigestUtils;
              import org.springframework.util.StringUtils;
              import java.util.UUID;
              
              public class SecurityUtil {
                  //加密
                  public static String encrypt(String inputPassword){
                      //生成随机盐值
                      String salt = UUID.randomUUID().toString().replaceAll("-", "");
                      //(密码+盐值)进行加密
                      String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());
                      return salt + finalPassword;
                  }
                  //验证
                  public static boolean verify(String inputPassword, String sqlPassword){
                      if (!StringUtils.hasLength(inputPassword)){
                          return false;
                      }
                      if (sqlPassword == null || sqlPassword.length() != 64){
                          return false;
                      }
                      //取出盐值
                      String salt = sqlPassword.substring(0,32);
                      //(输入密码 + 盐值)重新生成 加密密码
                      String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());
                      //判断数据库中储存的密码与输入密码是否一致
                      return (salt + finalPassword).equals(sqlPassword);
                  }
              
                  public static void main(String[] args) {
                      System.out.println(SecurityUtil.encrypt("123456"));
                  }
              }
              

              2.将数据库中的密码替换为加密后的值

              3.修改验证密码的逻辑(UserService类)

                      if (!SecurityUtil.verify(password,userInfo.getPassword())) {
                   android       return "密码错误";
                      }
              

              6.拦截器

              Spring拦截器(Interceptor)是一种基于AOP的机制,用于在请求处理的不同阶段插入自定义逻辑。常用于权限校验、日志记录、参数预处理等场景

              1.创建拦截器类并实现HandlerInterceptor接口,该接口提供了三种方法:

              • preHandle:在Controller方法执行前调用
              • postHandle:Controller方法执行后、视图渲染前调用
              • afterCompletion:请求完成、视图渲染完毕后调用
              import jakarta.servlet.http.HttpServletRequest;
              import jakarta.servlet.http.HttpServletResponse;
              import lombok.extern.slf4j.Slf4j;
              import org.springframework.stereotype.Component;
              import org.springframework.web.servlet.HandlerInterceptor;
              import org.springframework.web.servlet.ModelAndView;
              
              @Slf4j
              @Component
              public class Interceptor implements HandlerInterceptor {
              
                  @Override
                  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
                      //1.获取token
                      String cookie = request.getHeader("cookie");
                      if (cookie == null) {
                          response.setStatus(401);
                          return false;
                      }
                      l编程客栈og.info("接收到cookie:{}",cookie);
                      //2.校验token
                      return true;
                  }
              
                  @Override
                  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
                      log.info("postHandle");
                  }
              
                  @Override
                  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                      log.info("afterCompletion");
                  }
              }
              

              2.注册拦截器

              import org.example.springlogin.intercepter.Interceptor;
              import org.springframework.beans.factory.annotation.Autowired;
              import org.springframework.context.annotation.Configuration;
              import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
              import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
              import java.util.Arrays;
              import java.util.List;
              
              @Configuration
              public class Config implements WebMvcConfigurer {
                  private final Interceptor Interceptor;
              
                  @Autowired
                  public Config(Interceptor interceptor) {
                      Interceptor = interceptor;
                  }
                  //排除不需要拦截的路径
                  private static final List<String> excludes = Arrays.asList(
                          "/**/login.html",
                          "/user/login",
                          "/captcha/get"
                  );
              
                  @Override
                  public void addInterceptors(InterceptorRegistry registry) {
                      //注册拦截器
                      registry.addInterceptor(Interceptor)
                              //拦截所有路径
                              .addPathPatterns("/**")
                              .excludePathPatterns(excludes);
                  }
              }
              

              3.创建home.html文件,并且在登录成功后跳转到该页面(在login.html中添加location.href="/home.html")

              <!DOCTYPE html>
              <html lang="en">
              <head>
                  <meta charset="UTF-8">
                  <title>home</title>
              </head>
              <body>
                  <h1>Hello World</h1>
              </body>
              </html>
              

              实现效果:

              • 成功登陆时
              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              • 未登录直接访问home.html页面时
              • SpringBoot登录认证前后端实现方案:SpringBoot + Mybatis + JWT(图文实例)

              到此这篇关于SpringBoot登录企业级认证系统实现方案:Session、统一封装、MD5加密与拦截器的文章就介绍到这了,更多相关SpringBoot登录认证方案内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

              0

              上一篇:

              下一篇:

              精彩评论

              暂无评论...
              验证码 换一张
              取 消

              最新开发

              开发排行榜