Springboot3+Vue3+MybatisPlus+Vite+TS+ElementPlus搭建网站


趁着寒假充实一下技能…

顺便更新一下自己的技术栈Springboot3+Vue3+MybatisPlus+Vite+TS+ElementPlus

路由守卫

import { jwtDecode } from 'jwt-decode';
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) { // 检查路由是否需要登录认证
    const token = localStorage.getItem('token');
    const isAuthenticated = token !== null;
    
    if (isAuthenticated) {
      // 如果本地存储中存在 token,则解析 token 并提取过期时间
      try {
        const decodedToken = jwtDecode(token);
        const expirationDate = decodedToken.exp * 1000; // 将过期时间转换为毫秒

        if (Date.now() < expirationDate) {
          next(); // 如果 token 有效,则继续导航
        } else {
          next('/login'); // 如果 token 已过期,则重定向到登录页面
        }
      } catch (error) {
        console.error('Token decoding failed:', error);
        next('/login'); // 如果 token 无效或解码失败,则重定向到登录页面
      }
    } else {
      next('/login'); // 如果用户未登录,则重定向到登录页面
    }
  } else {
    next(); // 如果路由不需要登录认证,则继续导航
  }
});

Mybatis plus

首先禁用所有的名称转换方法,以保持数据库字段名与实体类字段名完全一致:

mybatis-plus.configuration.map-underscore-to-camel-case=false

在典型的 Spring Boot 应用程序中,通常会遵循 MVC(Model-View-Controller)架构模式。在这个模式中,Controller 层负责处理用户请求,Service 层处理业务逻辑,而 MapperRepository 层负责数据访问。下面是这些层级的一般职责:

  • Controller:接收客户端发送的请求,调用 Service 层的方法处理这些请求,然后返回响应。Controller 层是请求的入口点。
  • Service:包含业务逻辑,通常会调用 Mapper/Repository 来访问数据库。Service 层位于 Controller 和 Mapper/Repository 层之间,起到了协调它们的作用。
  • Mapper/Repository:直接与数据库交互,执行 CRUD 操作(创建、读取、更新、删除)。在 MyBatis 中,Mapper 接口定义了与数据库交互的方法。

这里放一个实例:

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

@Data
@TableName("comweb_users") //指定需要操作的表名
public class Users {
    @TableId // 默认是不会自动生成主码且主码不需要注名
    private String userName;
    // @TableField("passwd") 指定需要操作的字段,如果不写这一行默认是按照下方定义的字段去匹配,写了可以指定一个不同于字段名的列名,不需要指定长度由数据库自己决定
    private String passwd;
    private String ywProf;
    private String asset;

    // 省略 getter 和 setter 方法
    //    userName = models.CharField(max_length=10, primary_key=True)
    //    passwd = models.CharField(max_length=10, null=True, blank=True)
    //    ywProf = models.CharField(max_length=16, null=True, blank=True) # 专业组名字
    //    asset = models.CharField(max_length=10, null=True, blank=True) # 权限
}

在 MyBatis Plus 中,BaseMapper 提供了一些基本的 CRUD 方法,但它不会自动生成特定于字段的查询方法,如 selectByUserNameAndPasswd。这种方法需要您自行实现,要么通过定义 XML 映射文件,要么通过使用 MyBatis Plus 提供的注解或者使用 QueryWrapper 来构建查询。

如果您希望 MyBatis Plus 自动生成这样的方法,您需要使用 @Select 注解或者使用 MyBatis Plus 的 IServiceServiceImpl 来扩展基本的 CRUD 功能。但请注意,即使使用了 IServiceServiceImpl,也不会自动生成特定的方法,如 selectByUserNameAndPasswd。您仍然需要自定义这样的方法。

1. 使用 QueryWrapper 自定义查询:

如果您不想写额外的注解或 XML 映射,可以在服务层使用 QueryWrapper 来构建自定义查询条件。这样,您就可以利用 BaseMapper 提供的 selectOne 方法来执行查询,如前面的示例所示。

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.yourpackage.entity.Users;
import com.example.yourpackage.mapper.UsersMapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UsersService {

    @Autowired
    private UsersMapper usersMapper;

    public Users getUserByUserNameAndPasswd(String userName, String passwd) {
        // 创建 QueryWrapper 实例
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        // 设置查询条件
        queryWrapper.eq("userName", userName).eq("passwd", passwd);
        // 执行查询
        return usersMapper.selectOne(queryWrapper);
    }
}

对应mapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.yourpackage.entity.Users;

public interface UsersMapper extends BaseMapper<Users> {
    // 这里不需要添加任何额外的方法,因为BaseMapper已经提供了selectOne等CRUD方法
}

对应controller

import com.example.yourpackage.entity.Users;
import com.example.yourpackage.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UsersController {

    @Autowired
    private UsersService usersService;

    @GetMapping("/login")
    public Users getUserByUserNameAndPasswd(@RequestParam String userName, @RequestParam String passwd) {
        return usersService.getUserByUserNameAndPasswd(userName, passwd);
    }
}

2. 使用注解自定义 SQL:

如果您希望在 Mapper 接口中定义自定义方法并使用注解提供 SQL,您可以这样做:

public interface UsersMapper extends BaseMapper<Users> {
    @Select("SELECT * FROM comweb_users WHERE userName = #{userName} AND passwd = #{passwd}")
    Users selectByUserNameAndPasswd(@Param("userName") String userName, @Param("passwd") String passwd);
}

    @Select("<script>" +
            "SELECT * FROM comweb_users WHERE " +
            "<foreach collection='filterOptions' item='option' separator=' OR '>" +
            "   <foreach collection='option.filterValues' item='filter' separator=' OR '>" +
            "       ${option.columnName} = #{filter}" +
            "   </foreach>" +
            "</foreach>" +
            "</script>")
    List<Users> selectUsersByFilterOptions(@Param("filterOptions") List<Map<String, List<String>>> filterOptions);

对应controller

package com.example.comweb.controller;

import com.example.comweb.mapper.UsersMapper;
import com.example.comweb.model.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UsersController {
    @Autowired
    private UsersMapper usersMapper;

    @GetMapping("/api/users/login")
    public String login(@RequestParam String userName, @RequestParam String passwd) {
        Users user = usersMapper.selectByUserNameAndPasswd(userName, passwd);
        if (user != null) {
            return "Login successful!"; // 登录成功
        } else {
            return "Login failed!"; // 登录失败
        }
    }
}

3. 使用 XML 映射文件(plus中可以不使用xml):

如果您更喜欢使用 XML 映射文件,您需要创建一个与 Mapper 接口同名的 XML 文件,在其中定义您的自定义 SQL 语句。然后,确保 MyBatis Plus 能够找到并加载这个 XML 映射文件。

在任何情况下,MyBatis Plus 都不会自动为您生成特定字段的查询方法,您需要手动提供查询逻辑。如果您想避免书写 SQL 语句,应该使用 QueryWrapper 或者 LambdaQueryWrapper 来构建查询条件。

如果你选择使用 MyBatis Plus 的 XML 映射文件而不是注解来定义 SQL 语句,你需要在 mapper 目录下为你的 UsersMapper 接口创建一个对应的 XML 文件。这个文件通常与接口同名,并且放在相同的包路径下。

以下是一个基本的 XML 映射文件例子,它定义了一个通过用户名和密码查询用户的操作:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.yourpackage.mapper.UsersMapper">
    <!-- 定义resultMap,映射数据库列到Java实体属性 -->
    <resultMap id="UserResultMap" type="com.example.yourpackage.entity.Users">
        <id column="id" property="id"/>
        <result column="user_name" property="userName"/>
        <result column="passwd" property="password"/>
        <!-- 其他字段映射 -->
    </resultMap>

    <!-- 通过用户名和密码查询用户的SQL -->
    <select id="selectByUserNameAndPasswd" resultMap="UserResultMap">
        SELECT * FROM users
        WHERE user_name = #{userName} AND passwd = #{password}
    </select>
</mapper>

在这个文件中:

  • <!DOCTYPE> 声明了 MyBatis 的 DTD,这是为了验证 XML 文件的结构。
  • <mapper> 元素的 namespace 属性应该与你的 Mapper 接口的完全限定名匹配。
  • <resultMap> 定义了如何将数据库表的列映射到你的 Java 实体类的属性。id 元素通常用于主键字段,而 result 用于其他字段。
  • <select> 元素定义了一个 SQL 查询语句。id 属性应该与你的 Mapper 接口中的方法名匹配。resultMap 属性指定了返回结果应该如何映射到 Java 对象。

现在在你的 UsersMapper 接口中,你可以定义一个与 XML 文件中的 select 语句相匹配的方法:

import com.example.yourpackage.entity.Users;
import org.apache.ibatis.annotations.Param;

public interface UsersMapper extends BaseMapper<Users> {
    Users selectByUserNameAndPasswd(@Param("userName") String userName, @Param("password") String password);
}

在你的服务层中,你现在可以直接调用这个方法:

@Service
public class UsersService {

    @Autowired
    private UsersMapper usersMapper;

    public Users getUserByUserNameAndPasswd(String userName, String passwd) {
        return usersMapper.selectByUserNameAndPasswd(userName, passwd);
    }
}

确保你的 MyBatis 配置文件已经配置了 mapper XML 文件的路径,否则 MyBatis Plus 找不到这些文件。例如,在 mybatis-config.xml 中,你需要有如下配置:

<configuration>
    <!-- 其他配置... -->
    <mappers>
        <mapper resource="com/example/yourpackage/mapper/UsersMapper.xml"/>
        <!-- 其他mapper文件 -->
    </mappers>
</configuration>

这样,MyBatis Plus 在启动时会加载并解析这些 XML 文件,你就可以在你的应用程序中使用这些自定义的查询了。

4.Websocket通信

使用 JWT 拦截请求时,ws 请求不能和 http 同时处理,一般有两种方法,独立为 ws 写一个拦截器。或者通过 HeaderMapRequestWrapper 对 WebSocket 请求的 HttpServletRequest 进行包装,在包装过程中将 WebSocket 请求头中的 Sec-WebSocket-Protocol 值提取出来并放入一个名为 Authorization 的请求头中。将 WebSocket 请求转变为标准的 HTTP 请求,从而可以通过常规的 HTTP 过滤器来处理认证逻辑。

/**
 * 修改header信息,key-value键值对儿加入到header中
 */
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
    private static final Logger LOGGER = LoggerFactory.getLogger(HeaderMapRequestWrapper.class);
    /**
     * construct a wrapper for this request
     *
     * @param request
     */
    public HeaderMapRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    private Map<String, String> headerMap = new HashMap<>();
    /**
     * add a header with given name and value
     *
     * @param name
     * @param value
     */
    public void addHeader(String name, String value) {
        headerMap.put(name, value);
    }
    @Override
    public String getHeader(String name) {
 
        String headerValue = super.getHeader(name);
        if (headerMap.containsKey(name)) {
            headerValue = headerMap.get(name);
        }
        return headerValue;
    }
    /**
     * get the Header names
     */
    @Override
    public Enumeration<String> getHeaderNames() {
        List<String> names = Collections.list(super.getHeaderNames());
        for (String name : headerMap.keySet()) {
            names.add(name);
        }
        return Collections.enumeration(names);
    }
    @Override
    public Enumeration<String> getHeaders(String name) {
 
        List<String> values = Collections.list(super.getHeaders(name));
 
        if (headerMap.containsKey(name)) {
 
            values = Arrays.asList(headerMap.get(name));
        }
        return Collections.enumeration(values);
    }
}
@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        /**
         * 通过request的url判断是否为websocket请求
         * 如果为websocket请求,先处理Authorization
         */
        if(request.getRequestURI().contains("/websocket")){
            HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper((HttpServletRequest) request);
            //修改header信息 将websocket请求中的Sec-WebSocket-Protocol值取出处理后放入认证参数key
            //此处需要根据自己的认证方式进行修改  Authorization 和 Bearer
            requestWrapper.addHeader("Authorization","Bearer "+request.getHeader("Sec-WebSocket-Protocol"));
            request = (HttpServletRequest)  requestWrapper;
        }
        //处理完后就可以走系统正常的登录认证权限认证逻辑了
		//下边就是自己的验证token 登陆逻辑
        LoginUser loginUser = getLoginUser(request);
        ·······
        chain.doFilter(request, response);
    }

5.异步处理

springboot支持异步处理,不影响正常的请求,比如路由守卫的时候可以加一些计算。

@Async

Element Plus

npm install -D unplugin-vue-components unplugin-auto-import

vite.config.ts 按需引入

import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

全局配置,既然全面拥抱 ts,我们写ts

<template>
  <el-config-provider :size="size" :z-index="zIndex">
    <div id="app">
      <router-view></router-view>
    </div>
  </el-config-provider>
</template>

<script setup lang="ts">
// 删除 export default,因为使用 script setup 会自动导出组件
//在 Vue 3 中,当你使用 `<script setup>` 语法时,组件的名称通常是由其文件名决定的。
import { ref } from 'vue';

// 使用 ref 创建响应式引用
const zIndex = ref(3000);
const size = ref('small');
</script>

<style scoped>
/* 全局样式... */
</style>

Icon

https://www.jb51.net/article/283664.htm

BackTop

回到顶部实现不难,但是容易被打断,第二滚动后立即点击会被中断,如

const scrollToTop = () => {
  // 先停止当前所有滚动
  window.scrollTo(0, window.scrollY);

  // 稍等片刻后开始平滑滚动到顶部
  setTimeout(() => {
    window.addEventListener('wheel', disableScroll, { passive: false });
    window.scrollTo({
      top: 0,
      behavior: 'smooth'
    });

    const checkIfAtTop = () => {
      if (window.scrollY === 0) {
        window.removeEventListener('wheel', disableScroll);
        window.removeEventListener('scroll', checkIfAtTop);
      }
    };

    window.addEventListener('scroll', checkIfAtTop);
  }, 40); // 延迟40毫秒开始滚动到顶部
};

尝试n多种方法,都不太理想,包括监听滚轮

const disableScroll = (event) => {
  event.preventDefault();
};

window.addEventListener('scroll', disableScroll, { passive: false });

// window.removeEventListener('scroll', disableScroll);

实际上这个bug不是鼠标滚轮导致的,因为通过监听可以发现,鼠标滚动停止后再点击回到顶部会被打断,而此时已经没有了滚动,所以阻止滚动是无效的,查看各大平台网站,我自己的hexo主题,还有淘宝他们的网站都是没有这个bug的,这里面包含了比较复杂的动画处理,而且Vue3和TS对Jquery支持有限,无法很好使用Jquery的animate。最后只能采用最简单朴素的方法,就是延迟一小段时间再回到顶部。255已经是不会触发这个bug的最小时间。

const scrollToTop = () => {
    setTimeout(() => {
    window.scrollTo({
      top: 0,
      behavior: "smooth" // 使用平滑滚动
    });
  
  }, 255); // 延迟
};

样式参考

<transition name="slide">
  <div class="top-scroll" v-if="showScrollTop" @click="scrollToTop">
    <a class="btn-floating" href="#!">
      <font-awesome-icon :icon="['fas', 'arrow-up']" style="color: #fff; font-size: 28px;" />
    </a>
  </div>
</transition>

选用图标如下

import { faArrowUp } from '@fortawesome/free-solid-svg-icons';
library.add(faArrowUp)

sass参考

.top-scroll
  position: fixed
  bottom: -20px
  right: -20px
  z-index: 100
  padding: 10px
  width: 100px 
  height: 100px 


  .btn-floating
    display: flex
    justify-content: center
    align-items: center
    width: 60% 
    height: 60% 
    padding: 0 
    border-radius: 50%
    background: #ff2a4e
    box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.163)
    transition: background 0.5s ease

    &:hover
      background: #ff513a


.slide-enter-active, .slide-leave-active
  transition: all 0.3s ease

.slide-enter-from, .slide-leave-to
  transform: translateY(100%)
  opacity: 0

这里还有一个坑,缓慢转变颜色不能用渐变色用函数也不行。

完整代码

<script setup lang="ts">
import { RouterLink, useRoute } from "vue-router";
import { ref, onMounted, onBeforeUnmount, watchEffect, onUnmounted  } from "vue";
import { library } from '@fortawesome/fontawesome-svg-core'
import $ from 'jquery'
import { faSquareFacebook, faTwitter, faSquareInstagram } from '@fortawesome/free-brands-svg-icons'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons';

// 将图标添加到库中
library.add(faTwitter, faSquareFacebook, faSquareInstagram, faArrowUp)

const route = useRoute();
const isTransparent = ref(false);
const showScrollTop = ref(false);

const scrollToTop = () => {
    setTimeout(() => {
    window.scrollTo({
      top: 0,
      behavior: "smooth" // 使用平滑滚动
    });
  
  }, 255); // 延迟 1000 毫秒(1 秒)执行回到顶部的操作
};

// 监听滚动事件
const handleScroll = () => {
  showScrollTop.value = window.scrollY > 100;
  isTransparent.value = route.path === '/' && window.scrollY < 50;
};

// 注册和注销滚动事件监听器
onMounted(() => {
  window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll);
});

watchEffect(() => {
  // 当路由为'/'时,根据滚动位置设置透明度
  isTransparent.value = route.path === '/' && window.scrollY < 50;
  console.log("Current route path:", route.path);
});

const isMenuOpen = ref(false);

const toggleMenu = () => {
  isMenuOpen.value = !isMenuOpen.value;
};


</script>

<template>
  <section id="header1" class="d-flex align-items-center justify-content-between top-0 left-0">
    <div class="contactEmail">
      Contact Email: <a href="mailto:info@jmymelody.com">info@jmymelody.com</a>
    </div>
    <!-- <img src="../../img/logo.png" class="logo" alt=""> -->
    <nav id="navbar" :class="{ 'active': isMenuOpen }" class="d-flex align-items-center justify-content-center m-0">
      <a href="https://www.facebook.com" target="_blank"><font-awesome-icon :icon="['fab', 'square-facebook']" /> Facebook</a>
      <a href="https://www.twitter.com" target="_blank"><font-awesome-icon :icon="['fab', 'twitter']" /> Twitter</a>
      <a href="https://www.instagram.com" target="_blank"><font-awesome-icon :icon="['fab', 'square-instagram']" /> Instagram</a>
    </nav>
    <div id="mobile" class="d-none align-items-center">
      <a><i class="bi bi-bag bag"></i></a>
      <span class="d-flex align-items-end" @click="toggleMenu">
        <i  class="fas fa-outdent bar"></i>
      </span>
    </div>
  </section>

  <section id="header" :class="{'bg-transparent': isTransparent}" class="d-flex align-items-center justify-content-between position-sticky top-0 left-0">
    <div class="webName">JMYmelody</div>
    <!-- <img src="../../img/logo.png" class="logo" alt=""> -->

    <nav id="navbar" :class="{ 'active': isMenuOpen }" class="d-flex align-items-center justify-content-center m-0">
      <RouterLink active-class="active" to="/">Home</RouterLink>
      <RouterLink active-class="active" to="/shop">Shop</RouterLink>
      <RouterLink active-class="active" to="/blog">Blog</RouterLink>
      <RouterLink active-class="active" to="/about">About</RouterLink>
      <RouterLink active-class="active" to="/contact">Contact</RouterLink>
      <RouterLink active-class="active" to="/login">login</RouterLink>
      <RouterLink active-class="active" to="/cart"><i  id="lg-bag" class="bi bi-bag"></i></RouterLink>
      <RouterLink active-class="active" to="/" href="#" id="close" @click="toggleMenu"><i class="bi bi-x-lg"></i></RouterLink>
    </nav>
    <div id="mobile" class="d-none align-items-center">
      <a><i class="bi bi-bag bag"></i></a>
      <span class="d-flex align-items-end" @click="toggleMenu">
        <i  class="fas fa-outdent bar"></i>
      </span>
    </div>
  </section>

  <transition name="slide">
    <div class="top-scroll" v-if="showScrollTop" @click="scrollToTop">
      <a class="btn-floating" href="#!">
        <font-awesome-icon :icon="['fas', 'arrow-up']" style="color: #fff; font-size: 28px;" />
      </a>
    </div>
  </transition>

</template>

<style src="../../assets/header.sass" scoped lang="sass"></style>

部署

因为有些情况是需要部署两个 springboot 应用在同一个服务器的,建议采用 nginx 挂代理的方式,例如:

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/
# 全局设置
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# 动态模块加载
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    # 日志格式和访问日志位置
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    # 基础优化配置
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;
    
    # 默认 HTTP 配置,强制跳转到 HTTPS
    server {
        listen       80;
        listen       [::]:80;
        server_name  不带www和http(s)的域名;
        root         /usr/share/nginx/html;

        # 重定向所有 HTTP 请求到 HTTPS
        return 301 https://$host$request_uri;
    }
    server {  
        listen              443 ssl;  
        server_name         不带www和http(s)的域名;
        ssl_certificate     ssl证书.crt;
        ssl_certificate_key ssl证书.key;
        ssl_session_cache   shared:SSL:1m;  
        ssl_session_timeout 5m;  
        ssl_ciphers         HIGH:!aNULL:!MD5;         
        ssl_prefer_server_ciphers  on;  

        # 主应用代理(8080 端口)
        location / {
            proxy_pass http://127.0.0.1:8080;  # 第一个应用
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # 新应用代理(9000 端口)
        location /workspace {
            proxy_pass http://127.0.0.1:9000;  # 第二个应用
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # 错误页面配置(可选)
        error_page 404 /404.html;
        location = /404.html {
            root /usr/share/nginx/html;
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }  
    }

}

这里是将 nginx 挂载在 443,然后通过两个代理部署两个应用。注意挂载新的 http 端口需要查看该端口是否开放,一般在你的服务器提供商那里修改安全组,开放新的端口即可。

在外面的 windows 测试端口开放:

Test-NetConnection -ComputerName <IP> -Port 9000

在服务器上测试端口开放:

telnet <IP> 9000

也可以在 java 里放好 ssl 证书的 jks,这样就不需要用 nginx,可以一个证书挂多个应用:

server.ssl.enabled=true
server.ssl.key-store=classpath:source/ssl证书.jks
server.ssl.key-store-password=
server.ssl.keyStoreType=JKS

需要在 resource 文件夹下放好 source/ssl证书.jks。

检查端口占用情况,有占用可以 kill:

sudo lsof -i :9000 

后台常驻启动 jar 包命令,输出日志到 workspace.log

nohup java -jar workspace-0.0.1-SNAPSHOT.jar > workspace.log 2>&1 &
ps aux | grep workspace-0.0.1-SNAPSHOT.jar

检查java后台

ps aux | grep java

检查服务器监听的网络,显示 tcp 是 ipv4,tcp6 是 ipv6,一般只显示一个 ipv6 是可以兼容 ipv4 的:

sudo netstat -tuln | grep 9000

服务器本地测试请求和 ssl 握手:

curl -k https://127.0.0.1:9000/workspace/api/users/login \
-H "Content-Type: application/json" \
-d '{"ssno":"SX2316098","pwd":"123"}' \
-X POST

服务器本地跨域测试:

curl -i -k https://localhost:9000/workspace/api/users/login \
-X POST \
-H "Content-Type: application/json" \
-H "Origin: https://www.baidu.com" \
-d '{"ssno":"SX2316098","pwd":"123"}'

ssl 握手测试:

openssl s_client -connect 127.0.0.1:9000

成功会输出:

SSL handshake has read 1234 bytes and written 567 bytes


文章作者: Alex Lee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex Lee !
评论
  目录