Spring Boot+mysql+Vue.js最小子系统骨架Demo

本Demo实现了一个登录页面。具体功能如下:

  • 用户可以在网页的输入框中输入用户名和密码
  • 之后前端Vue.js使用axios发送get请求,将用户名和密码传送给后端
  • 后端Controller将获取到的用户名和密码与数据库(使用jdbc)中的查询结果比较,并将结果发送request回前端
  • 前端根据返回值,保存登录状态到会话Cookie
  • 用户在关闭浏览器(区分于关闭页面)前,会话Cookie均有效,登录状态可以保持。这样,页面间的跳转等操作均不会导致登录状态失效

具体的实现如下。

加入依赖(Maven)

首先创建一个Spring项目(可以参考之前的博客)。之后,配置Maven。Maven大部分应该由IDEA自动生成。只需要管dependency就好。之后,IDEA窗口上会有一个Maven图标+一个看上去像刷新的图标。点击一下,即可让Maven自动配置依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>XXXX</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>XXXX</name>
    <description>XXXX</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

程序入口

package com.example.xxxx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class XXXXApplication {
    public static void main(String[] args) {
        SpringApplication.run(XXXXApplication.class, args);
    }

    @GetMapping("/hello")
    public String sayHello(@RequestParam(value = "myName", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}

创建Model类(Java – Spring)

创建以下类,作为MVC中M的部分。

大概就是一个User类吧。具体的业务逻辑用的到。这里懒得写了。登录功能比较简单,所以不写这个也行。

public class User{
    private String username;
    private String password;
    set...;
    get...;
    func1...;
}

配置数据库(JDBC)

resources/application.properties中,加入这一行,避免启动时配置数据源失败问题【重要】

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

使用xml配置jdbc。resources/myjdbc.xml如下所示。url、username、password记得换成自己的。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="jdbc:mysql://localhost:3306/DATABASENAME?useSSL=false" />
        <property name="username" value="username" />
        <property name="password" value="YOURPASSWORD" />
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>

</beans>

读取数据库信息

CheckLoginFromDB.java用于在数据库中查找指定用户名的密码,并判断提供的密码是否正确。

package com.example.xxxx;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

public class CheckLoginFromDB {
    public static boolean login(String userID, String password) {
        ApplicationContext context = new ClassPathXmlApplicationContext("myjdbc.xml");
        JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTemplate");
        String sql = "Select password from RegisteredUser where userID = \"" + userID + "\"";
        List<String> results = jdbcTemplate.query(sql, new MyStringRowMapper());
        if(results.size() != 1) return false;
        if(password.equals(results.get(0))){
            return true;
        }
        return false;
    }

    private static class MyStringRowMapper implements RowMapper<String> {
        @Override
        public String mapRow(ResultSet rs, int rowNum) throws SQLException {
            return rs.getString("password");
        }
    }
}

控制类

LogInController.java是一个控制类(MVC中的C),将从前端的http的get请求中获取两个参数,并调用CheckLoginFromDB进行用户名、密码检查。之后,如果为真,则返回用户名(字符串)。否则返回"Failed"。

package com.example.xxxx;

import com.example.XXXXXXX.getfromdb.CheckLoginFromDB;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogInController {
    @GetMapping("/loginget")
    public String login(@RequestParam("username") String username, @RequestParam("password") String password){
        System.out.println("Calling login, username = " + username);
        if(CheckLoginFromDB.login(username, password) == true){
            System.out.println("Success Login");
            return username;
        }
        else return "Failed";
    }
}

写好前端页面(html – Vue.js)

在src/main/resources/static下建立login.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Cookie Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>

<div id="app">
  <input type="text" v-model="usernameInput">
  <input type="text" v-model="passwordInput">
  <button @click="login">LOGIN</button>
  <p>Value from Cookie: {{ cookieValue }}</p>
<!--  假装userID就是session的值,把这个值放到会话cookie中。会话cookie在页面关闭、页面跳转后仍然存在,但是在浏览器关闭后就消失了-->
</div>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        session: 'Failed',
        usernameInput: '',
        passwordInput: '',
        cookieValue: 'Not Logged In'
      };
    },
    created() {
      this.readFromCookie();
    },
    methods: {
      login() {
        axios.get('/loginget', {
          params: {
            username: this.usernameInput,
            password: this.passwordInput
          }
        })
              .then(response => {
                this.session = response.data;
                if(this.session != 'Failed'){
                  console.log("login success"); //异步
                  this.saveToCookie();
                  this.readFromCookie();
                }
              })
              .catch(error => {
                console.error(error);
              });
        // 异步。没等到request的时候,这里的代码(如果有)就可能已经执行了。

      },
      saveToCookie() {
        // 将session的值存储到会话 Cookie
        document.cookie = `session=${encodeURIComponent(this.session)};`;
        console.log('Value saved to Cookie:', this.session);
      },
      readFromCookie() {
        // 获取所有的 Cookie
        const cookies = document.cookie.split(';');

        // 遍历每个 Cookie,找到名为 "savedText" 的 Cookie
        for (let i = 0; i < cookies.length; i++) {
          const cookie = cookies[i].trim();
          if (cookie.startsWith('session=')) {
            // 提取保存的值并解码
            this.cookieValue = decodeURIComponent(cookie.substring('session='.length));
            console.log('Value from Cookie:', this.cookieValue);
            break;
          }
        }
      }
    }
  });
</script>
</body>
</html>

如何Demo

在浏览器中输入localhost:8080/login.html即可进入页面。
需要先保证数据库中有对应的数据。如果没有,可以先造一个“桩模块”,永远返回一个true。

public class CheckLoginFromDB {
    public static boolean login(String userID, String password) {
        return true;
    }
}

页面效果:有两个输入框,第一个用来输入用户名,第二个输入密码。之后有一个LOGIN按钮,以及下面一个字符串。未登录时是Not Logged In。
之后点击LOGIN按钮,如果密码正确,下面的字符串会变成用户名。刷新、关闭此页面但浏览器有其他页面的情况下再进入该页面,登录状态都会保持。

TODO

数据库不要明文存储密码

数据库中明文存储密码肯定是不好的。因此,可以将密码Hash过后存在数据库中;之后,每次获取密码之后,都先对字符串进行Hash之后,再去数据库中查询。以下是一个Java对字符串进行MD5运算的样例。需要注意的是,MD5其实在很多场景下,安全性并不够高。有需要的话,应该采用更好的Hash方法。

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class StrMD5 {
    public static void main(String[] args){
        String password = "123456789a";
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] messageDigest = md.digest(password.getBytes());
            BigInteger no = new BigInteger(1, messageDigest);
            String hashText = no.toString(16);
            System.out.println(hashText);
        } catch (NoSuchAlgorithmException e){
            System.out.println("NoSuchAlgorithmException");
        }
        return;
    }
}

Session ID而不是UserID

在这里为了偷懒,直接使用UserID作为“SessionID”使用了。事实上,后端应该为每个会话建立一个Session ID,并做出对应的维护、管理。