后端实现

环境要求:

新建SpringBoot项目

打开Idea,FIle -> New -> Project -> Spring Initializr。设置项目名,SDK等,点击下一步,选择Web -> Spring Web,点击Finish。

)

SpringBoot项目大概分为四层:

(1)DAO层:包括XxxMapper.java(数据库访问接口类),XxxMapper.xml(数据库链接实现);(有人喜欢命名为Dao,有人喜欢用Mapper)

(2)Bean层:model层,映射实体类。存放从数据库中取出的数据的对象称为Model Object(MO);

(3)Service层:也叫服务层,业务层,包括XxxService.java(业务接口类),XxxServiceImpl.java(业务实现类);(可以在service文件夹下新建impl文件放业务实现类,也可以把业务实现类单独放一个文件夹下,更清晰)

(4)Controller层:与前端交互。

依照上面四层,创建目录结构如下:

建立并连接数据库

(略)安装Mysql,建立数据库springboot,创建表t_user(含三个字段:id、username、password),向表中添加内容。

打开IDE右侧的Database标签,连接数据库

修改配置文件application.properties

springboot项目的配置文件支持properties、yaml、yml三种格式。SpringBoot的很多配置都有默认值,如果想修改默认配置,在application.properties(yml、yaml) 文件中配置.

将application.properties改为application.yml,并添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
datasource:
#name: springboot #数据库名
url: jdbc:mysql:///springboot #url,因为是本地运行,用三个/,省略ip和端口
username: root #用户名
password: 123456 #密码
driver-class-name: com.mysql.cj.jdbc.Driver #数据库链接驱动,报红原因:在依赖中scope是runtime,在编译时不生效,在运行时是没问题的

mybatis:
mapper-locations: classpath:mapper/*.xml #配置映射文件,当前项目是resources/mapper/UserMapper.xml
type-aliases-package: com.example.test.bean #配置实体类别名,在映射文件中的resultType中可以直接写类名,省略路径
# config-location: # 指定mybatis的核心配置文件

根据数据库表格,生成MO。

下载安装Mybatisx插件:File -> setting -> Plugins,搜索MyBatisx。

右键表格,点击MybatisX-Generator。

img

img

img

生成的代码使用了一些库,打开项目根目录下的的pom.xml文件,(在 <dependencies></dependencies>标签内)添加相关依赖:

  • mybatis:持久层框架,支持自定义SQL、存储过程以及高级映射。就是你写了sql可以直接转对象,不用你手工转,而且支持一对一,一对多的关系表的ORM
  • Lombok:可以为类中的字段自动生成get、set 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

<!--MySQL-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope> <!--只在运行时生效-->
</dependency>

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

<!--lombok用来简化实体类:需要安装lombok插件??-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>

参考:

MybatisX快速开发插件 | 官方文档

满满的MybatisX干货哦~:点击小鸟实现跳转

MyBatisX插件介绍


Mybatisx会生成几个文件,修改代码,通过账户(username)密码(password),从数据库中获取id:

1
2
3
4
5
6
7
8
9
10
11
//mapper/TUserMapper.java
package com.example.demo.mapper;

import com.example.demo.bean.TUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
TUser getInfo(String name,String password);
}
1
2
3
4
5
6
7
8
9
10
//service/TUserService.java:
package com.example.demo.service;

import com.example.demo.bean.TUser;
import com.baomidou.mybatisplus.extension.service.IService;

public interface TUserService extends IService<TUser> {
TUser loginIn(String name,String password);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//service/impl/TUserServiceImpl.java
ackage com.example.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.bean.TUser;
import com.example.demo.service.TUserService;
import com.example.demo.mapper.TUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser>
implements TUserService{
//将DAO层的注入Service层
@Autowired
private TUserMapper userMapper;

@Override
public TUser loginIn(String name, String password) {
return userMapper.getInfo(name,password);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--  resources/mapper/TUserMapper.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.demo.mapper.TUserMapper">

<resultMap id="BaseResultMap" type="com.example.demo.bean.TUser">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
</resultMap>

<sql id="Base_Column_List">
id,username,password
</sql>

<select id="getInfo" parameterType="String" resultType="com.example.demo.bean.TUser">
SELECT * FROM t_user WHERE username = #{name} AND password = #{password}
</select>

</mapper>

测试是否能成功读取数据库信息

在/src/test/java/com.example.test/DemoApplicationTests中进行测试,注意代码中loginIn参数,修改为自己的表中含有的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
///src/test/java/com.example.test/DemoApplicationTests
package com.example.test;

import com.example.test.bean.UserBean;
import com.example.test.mapper.UserMapper;
import com.example.test.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class DemoApplicationTests {
@Autowired
UserService userService;

@Test
public void contextLoads() {
UserBean userBean = userService.loginIn("zhangsan","123"); //改为自己表中的数据
System.out.println("该用户ID为:");
System.out.println(userBean.getId());
}
}

前端代码解析 – 登录篇

  • vue-element-admin
  • Vscode
  • Vscode插件
    • Live Server: 更改代码会自动刷新浏览器
    • Vetur
    • Vue Language Features
    • vue-helper

”首先我们不管什么权限,来实现最基础的登录功能:随便找一个空白页面撸上两个input的框,再放置一个登录按钮。我们将登录按钮上绑上click事件,点击登录之后向服务端提交账号和密码进行验证。如果你觉得还要写的更加完美点,你可以在向服务端提交之前对账号和密码做一次简单的校验。“

找到登录按钮相应``m `代码位置:在views/login/index.vue中,找到其绑定的click事件:

该事件会调用store/modules/user.js中的login方法,该方法又会调用api/user.js中的login方法:

打开api/user.js后,可以看到url,这就是登陆时前端的请求url,修改url内容:

其中的 request的实现来自 /src/utils/request.js,此文件已封装了axios。

请求拦截器

/src/utils/request.js文件中含有请求拦截器(限制未登录状态下对核心功能页面的访问)和响应拦截器。

一个简单请求拦截器的逻辑如下:

  1. 用户访问 URL,检测是否为登录页面,如果是登录页面则不拦截
  2. 如果用户访问的不是登录页面,检测用户是否已登录,如果未登录则跳转到登录页面import axios from ‘axios’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { MessageBox, Message } from 'element-ui'
import store from '@/store' //为了拿到token
import { getToken } from '@/utils/auth'

// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
//baseURL: 'http://localhost:8080/', // url = base url + request url // api的base_url

// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout // 请求超时时间
})

// request interceptor 请求前的拦截,携带的token字段
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['token'] = getToken() //注意,这里‘token'名要和后端一样,具体是。。? // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)

// response interceptor 响应的拦截
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/

/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
// * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中

// if the custom code is not 20000, it is judged as an error.
//这里有对状态码的验证,如果项目中没有相应状态码,注释以下代码,直接返回res
// if (res.code !== 20000) {
// Message({
// message: res.message || 'Error',
// type: 'error',
// duration: 5 * 1000
// })

// // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
// if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// // to re-login
// MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
// confirmButtonText: 'Re-Login',
// cancelButtonText: 'Cancel',
// type: 'warning'
// }).then(() => {
// store.dispatch('user/resetToken').then(() => {
// location.reload() ;// 为了重新实例化vue-router对象 避免bug
// })
// })
// }
// return Promise.reject(new Error(res.message || 'Error'))
// } else {
// return res
// }
return res
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)

export default service

注意文件中响应拦截部分,判定的响应码是20000,因此需要后端返回的成功响应码为20000。如果不匹配,则前端无法登录,因为响应被拦截了。

保存数据

登录成功后,服务端会返回一个 token(该token的是一个能唯一标示用户身份的一个key),之后我们将token存储在本地cookie之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不用再去登录页面重新登录了。

ps:为了保证安全性,我司现在后台所有token有效期(Expires/Max-Age)都是Session,就是当浏览器关闭了就丢失了。重新打开游览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。

实现前端登录器,需要在前端判断用户的登录状态。登录状态应该被视为一个全局属性,而不应该只写在某一组件中。项目使用了Vuex,它可以把需要在各个组件中传递使用的变量、方法定义在这里。之前我一直没有使用它,所以在不同组件传值的问题上十分头疼,要写很多多余的代码来调用不同组件的值,所以推荐大家从一开始就去熟悉这种管理方式。

这里我个人建议不要为了用 vuex 而用 vuex。就拿我司的后台项目来说,它虽然比较庞大,几十个业务模块,几十种权限,但业务之间的耦合度是很低的,文章模块和评论模块几乎是俩个独立的东西,所以根本没有必要使用 vuex 来存储data,每个页面里存放自己的 data 就行。当然有些数据还是需要用 vuex 来统一管理的,如登录token,用户信息,或者是一些全局个人偏好设置等,还是用vuex管理更加的方便,具体当然还是要结合自己的业务场景的。总之还是那句话,不要为了用vuex而用vuex!

获取用户信息

permission.js中有个钩子函数 router.beforeEach(),会在访问每个路由前拦截,判断是否已获得token,没有,就转向登录,如果有 token, 就会把这个 token 返给后端去拉取 user_info去获取用户的基本信息。

就如前面所说的,我只在本地存储了一个用户的 token,并没有存储别的用户信息(如用户权限,用户名,用户头像等)。有些人会问为什么不把一些其它的用户信息也存一下?主要出于如下的考虑:

假设我把用户权限和用户名也存在了本地,但我这时候用另一台电脑登录修改了自己的用户名,之后再用这台存有之前用户信息的电脑登录,它默认会去读取本地 cookie 中的名字,并不会去拉去新的用户信息。

这样能保证用户信息是最新的。 当然如果是做了单点登录的功能的话,用户信息存储在本地也是可以的。当你一台电脑登录时,另一台会被提下线,所以总会重新登录获取最新的内容。

从代码层面我建议还是把 login和get_user_info两件事分开比较好,在这个后端全面微服务的年代,后端同学也想写优雅的代码~

跨域

当进行网络请求时,接口路径会自动和baseURL进行拼接。接下来修改它baseURL为后端的,让前端能够去访问后端的API:

  1. 查看后端的端口:

    img

  2. 添加跨域支持:打开webpack配置文档,将代码复制到根目录下的vue.config.js配置文件中的devServer下,更改target。

    • 当配置文件发生变化后,项目需要重启

    img

    img

参考:

vue-element-admin | github

vue-element-admin 的使用记录(一)

前后端交互

前后端分离的意思是前后端之间通过 RESTful API 传递 JSON 数据进行交流。在开发的时候,前端用前端的服务器(Nginx),后端用后端的服务器(Tomcat),当我开发前端内容的时候,可以把前端的请求通过前端服务器转发给后端(称为 反向代理 ),这样就能实时观察结果,并且不需要知道后端怎么实现,而只需要知道接口提供的功能,两边的开发人员(两个我)就可以各司其职啦。

在后端部分提供前端请求的内容。

打开前端项目中/src/modules/user.js文件:

login方法想要获取token,getInfo方法想要获取roles, name, avatar, introduction,那我们就在后端中提供这些内容。

打开后端项目,创建utils文件夹,创建两个类R(设置token,roles等)和ResultCode(设置响应码),在controller中创建LoginController类(和前端交互)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//R.java
package com.example.demo.utils;

import lombok.Data;
import java.util.HashMap;
import java.util.Map;

@Data //会自动为字段生成get set 方法
public class R {
private Boolean success;
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<String, Object>();

//把构造方法私有,不能new,只能调用下面的静态方法
private R() {}

//成功静态方法
public static R ok() {
R r = new R();
r.setSuccess(true); //这个方法由lombok.Data自动生成
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");
return r;
}

//失败静态方法
public static R error() {
R r = new R();

r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");
return r;
}
//下面是设置字段的方法
//使用如:类.ok().success(false); 链式编程

public R success(Boolean success){
this.setSuccess(success);
return this;
}

public R message(String message){
this.setMessage(message);
return this;
}

public R code(Integer code){
this.setCode(code);
return this;
}

public R data(String key, Object value){
this.data.put(key, value);
return this;
}


public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}

1
2
3
4
5
6
7
8
9
10
//ResultCode.java
package com.example.demo.utils;

public class ResultCode {

public static Integer SUCCESS = 20000; //成功
public static Integer ERROR = 20001; //失败

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//LoginController.java
package com.example.demo.controller;

import com.example.demo.service.TUserService;
import com.example.demo.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController //交给spring管理
@RequestMapping("/user")
public class LoginController {

@Autowired //将Service注入Web层
TUserService userService;

@PostMapping("login")
public R login(){
// UserBean userBean = userService.loginIn(name,password);
// if(userBean!=null){
// return "success";
// }else {
// return "error";
// }
return R.ok().data("token","da-jia-wan-shang-hao");
}
@GetMapping("info")
public R info(){
return R.ok().data("roles", new String[]{"admin"}).data("name","admin").data("avatar","https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%B0%8F%E5%9B%BE%E7%89%87&step_word=&hs=0&pn=19&spn=0&di=7146857200093233153&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&istype=2&ie=utf-8&oe=utf-8&in=&cl=2&lm=-1&st=-1&cs=3755132173%2C1946217785&os=3112775561%2C2508984481&simid=3755132173%2C1946217785&adpicid=0&lpn=0&ln=1638&fr=&fmq=1668228130827_R&fm=&ic=&s=undefined&hd=&latest=&copyright=&se=&sme=&tab=0&width=&height=&face=undefined&ist=&jit=&cg=&bdtype=0&oriquery=&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fi.qqkou.com%2Fi%2F2a3755132173x1946217785b26.jpg%26refer%3Dhttp%3A%2F%2Fi.qqkou.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1670820133%26t%3D3620b9f317582311d2807e4c0eab71fb&fromurl=ippr_z2C%24qAzdH3FAzdH3Fqqh57_z%26e3Bv54AzdH3Fp57xtwg2AzdH3F8d9d8bm_z%26e3Bip4s&gsm=1e&rpstart=0&rpnum=0&islist=&querylist=&nojc=undefined&dyTabStr=MCwxLDMsNiw0LDIsNSw3LDgsOQ%3D%3D").data("introduction","haha");
}
}

前端发出请求“/user/login”,后端接收之后进行路径匹配,调用LoginController.login方法(未实现对账号密码的验证),该方法返回一个类对象R,该对象中有个类型为map的字段data, 其中包含了一个pair: {"token","da-jia-wan-shang-hao"}
img

好了,启动后端项目,启动前端项目,点击登录按钮,即可跳转到首页。

遇到的错误:

  1. role.some is not a function:需要后端返回的roles是数组,见roles.some is not a function | github issue
  2. 登录之后报错:Request failed with status code 404:见下图。和本主题无关,先不管它。