传统的 JavaWeb 项目中使用cookie+session来记录访问用户的身份信息,但是基于SpringBoot和Vue技术的前后端分离项目中,则是使用Token的身份验证模式。Token是服务端生成的一个字符串,作为访问用户发起请求的一个令牌。当用户第一次登录后,服务器生成一个Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码了。注意,不同的用户生成的Token字符串是不一样的。
JWT(Json Web Token)是实现token技术的一种解决方案,用于前端和服务端进行身份认证。JWT 字符串是有3个独立的字符串使用 “.” 号组合而成,类似于 “Header.Payload.Signature” 组成,前两个字符串是 Base64 编码,最后一个字符串是一个加密后的字符串。JJWT是一个提供端到端的JWT创建和验证的Java库,用于在JVM和Android上创建和验证JSON Web令牌(JWT)。官方地址为: https://github.com/jwtk/jjwt
接下来,我们创建 “SpringBootTokenDemo” 工程
然后我们修改编码格式以及Maven仓库地址,我们省略这个过程了。
接下来,我们修改 “pom.xml” 文件,添加SpringBootWeb和 JJWT依赖,如下所示
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>SpringBootTokenDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.18</version>
</plugin>
</plugins>
</build>
</project>
接下来,我们创建 Appliaction 入口类文件
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
接下来,我们创建 “TokenUtils” 类来封装一下JJWT库
package com.demo.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class TokenUtils {
// 令牌密钥
@Value("${token-secret}")
private String secret;
// 生成Token令牌,传递用户信息
public String createToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
}
// 解析Token令牌,获取用信息
public Claims parseToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}
其实就是两个方法,一个根据用户信息生成token,另一个就是解析token获取用户信息。大家可以看到,这个过程本质就是加密和解密的过程。这里我们用了密钥配置信息,我们需要在 “application.properties” 文件中声明
# 令牌密钥
token-secret=nkv4um2add4oaf3e7iou6hssf
我们可以在自己的工程里面重新定义这个 “令牌密钥” 。
接下来,我们创建一个 “LoginController” 登录控制器
package com.demo.controller;
import com.demo.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class LoginController {
@Autowired
private TokenUtils tokenUtils;
@RequestMapping("/login")
public Map<String, String> login(String username, String password){
Map<String, String> result = new HashMap();
result.put("code", "200");
String token = "";
if(username.equalsIgnoreCase("admin") && password.equalsIgnoreCase("123456")){
// 根据用户名和密码生成Token
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("username", username);
userInfo.put("password", password);
token = tokenUtils.createToken(userInfo);
}
// 返回json数据
result.put("token", token);
return result;
}
}
接下来,我们不要忘记跨域配置
package com.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Bean
public CorsFilter corsFilter() {
// 跨域配置类
CorsConfiguration config = new CorsConfiguration();
// 是否发送Cookie
config.setAllowCredentials(true);
// 放行所有域名
config.addAllowedOriginPattern("*");
// 放行所有请求方式
config.addAllowedHeader("*");
// 放行所有请求头部
config.addAllowedMethod("*");
// 添加映射路径(拦截所有请求)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的 CorsFilter
return new CorsFilter(source);
}
}
最后,我们启动工程测试一下,
我们使用浏览器访问: http://localhost:8080/login?username=admin&password=123456
接下来,我们创建Vue的前端工程
vue create vuetokendemo
cd vuetokendemo
npm install vuex@3.6.2 -save
npm install axios@1.6.0 -save
npm install vue-router@3.5.2 -save
npm install element-ui@2.15.14 -save
如果是下载的工程文件,是没有 “node_modules” 目录的,需要我们执行 “npm install” 命令安装一下。 以下是我们 “package.json” 文件的内容
{
"name": "vuetokendemo",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.6.0",
"core-js": "^3.8.3",
"element-ui": "^2.15.14",
"vue": "^2.6.14",
"vue-router": "^3.5.2",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {
"no-mixed-spaces-and-tabs": 0,
"vue/multi-word-component-names": 0
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
接下来,我们修改vue的配置文件“vue.config.js”文件,更改端口号
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: { port: 90 }
})
接下来,我们删除 “src/assets” 和 “src/components” 目录,只保留 “App.vue” 和 “main.js” 两个文件。
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app { text-align: center; color: #2c3e50; }
</style>
以上是 “App.vue” 文件内容
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
render: h => h(App)
})
接下来,我们先使用vue-router做几个简单的页面。
我们在src目录下创建views目录,里面创建“login.vue”和“home.vue”两个文件。
<template>
<div>
<p style="color: #ff0000;">登录</p>
<el-form :model="formData" :inline="true" size="mini">
<el-form-item label="用户">
<el-input v-model="formData.username" placeholder="请输入用户"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="formData.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitFormData">确定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'login',
data: function() {
return {
formData: {
username: '',
password: ''
}
}
},
methods: {
submitFormData:function(){
console.log(this.formData);
this.$router.push('/home');
}
}
}
</script>
这是 “login.vue” 文件内容
<template>
<div>
<el-table :data="tableData" stripe border style="width: 100%">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="stuId" label="编号" width="100"></el-table-column>
<el-table-column prop="stuName" label="姓名" width="200"></el-table-column>
<el-table-column prop="stuAge" label="年龄" width="100"></el-table-column>
<el-table-column prop="addTime" label="时间"></el-table-column>
<el-table-column label="操作">
<el-button size="mini">编辑</el-button>
<el-button size="mini" type="danger">删除</el-button>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'home',
data: function() {
return {
tableData: [{
stuId: '1',
stuName: '张三',
stuAge: '20',
addTime: '2024-02-02 09:00:00'
}, {
stuId: '1',
stuName: '张三',
stuAge: '20',
addTime: '2024-02-02 09:00:00'
}, {
stuId: '1',
stuName: '张三',
stuAge: '20',
addTime: '2024-02-02 09:00:00'
}, {
stuId: '1',
stuName: '张三',
stuAge: '20',
addTime: '2024-02-02 09:00:00'
}]
}
},
methods: {}
}
</script>
这是 “home.vue” 文件内容。
接下来,我们在src目录下创建“router”目录,并在该目录下创建“index.js”路由配置文件,代码如下
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// 引入vue页面文件
import login from '@/views/login.vue';
import home from '@/views/home.vue';
// 定义路由规则
const router = new Router({
routes:[
{ path:'/', redirect:'/login' },
{ path:'/login', name:'login', component:login },
{ path:'/home', name:'home', component:home },
]
});
// 导出路由器对象
export default router
接下来,我们在“终端”窗口执行“npm run serve”命令进行测试
接下来,我们使用“axios”发起ajax请求。
我们在“src”目录下创建“utils”目录,然后创建“request.js”文件。
import axios from 'axios'
// 设置默认 Content-Type 为json格式
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
baseURL: "http://localhost:8080",
timeout: 5000
})
// 添加请求拦截器
service.interceptors.request.use(
config => {
// 设置请求数据类型为表单格式
config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
// 简单打印一下请求数据
console.log("请求:" + JSON.stringify(config.data));
return config;
},
error => {
console.log(error)
return Promise.reject(error)
}
)
// 添加响应拦截器
service.interceptors.response.use(
response => {
// 简单打印一下返回数据
console.log("响应:" + JSON.stringify(response.data));
return response;
},
error => {
console.log(error)
return Promise.reject(error)
}
)
export default service
接下来,我们回到 “login.vue” 文件中修改
<script>
import request from '@/utils/request'
export default {
name: 'login',
data: function() {
return {
formData: {
username: '',
password: ''
}
}
},
methods: {
requestLoginApi: function(data) {
return request({url: '/login', method: 'post', data})
},
submitFormData:function(){
this.requestLoginApi(this.formData).then(res => {
let token = res.data.token;
if(token == ""){
this.$message("登录失败");
}else{
this.$message("登录成功");
window.localStorage.setItem("token", token);
this.$router.push('/home');
}
})
}
}
}
</script>
接下来,我们测试一下啊
我们看到接口正确返回了,我们可以查看本地 localStorage 是否存储了 token
我们也可以测试一下不正确的情况
接下来,我们要做一个“拦截器”,保证只有持有正确“token”的用户可以访问服务接口。
如果对“拦截器”不清楚了,可以参考:第19章 SpringBoot拦截器-优快云博客
package com.demo.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Writer;
public class TokenHandlerInterceptor implements HandlerInterceptor {
// 令牌标识
private final static String header = "Authorization";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader(header);
if (null == token || "".equals(token.trim())) {
// 返回json格式字符串给客户端
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
String json = "{\"code\":\"500\",\"message\":\"没有登录token,无法访问该页面!\"}";
writer.write(json);
writer.flush();
return false;
} else {
return true;
}
}
}
接下来注册该拦截器
package com.demo.config;
import com.demo.interceptor.TokenHandlerInterceptor;
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 WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenHandlerInterceptor())
.addPathPatterns("/**").excludePathPatterns("/login");;
}
}
请注意,这个拦截器除了 “/login” 接口之前,将拦截所有的请求。如果不包含token的话,则无法访问接口。 我们获取token的方式是从“请求头”中,因此我们的Vue工程发起请求的时候,需要将token放置请求头中。 接下来,我们增加上一个章节中的 学生查询接口,然后测试一下拦截器是否起效
接下来,我们回到Vue工程中 “request.js” 文件中修改
// 添加请求拦截器
service.interceptors.request.use(
config => {
// 设置请求数据类型为表单格式
config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
// 增加token
let token = window.localStorage.getItem("token");
if(token != null) config.headers['Authorization'] = token;
// 简单打印一下请求数据
console.log("请求:" + JSON.stringify(config.data));
return config;
},
error => {
console.log(error)
return Promise.reject(error)
}
)
我们将token放入到请求头中。
接下来,我们完善一下 “home.vue” 文件,增加接口的请求
<script>
import request from '@/utils/request'
export default {
name: 'home',
data: function() {
return {
tableData: []
}
},
created:function(){
this.getStudentData()
},
methods: {
requestStudentApi: function() {
return request({url: '/student', method: 'get'})
},
getStudentData: function() {
this.requestStudentApi().then(res => {
this.tableData = res.data
})
}
}
}
</script>
接下来,我们测试一下吧
我们可以使用开发者工具查看请求信息
如果我们删除本地Token
然后浏览器直接访问:http://localhost:90/#/home
返回这个信息,说明我们的拦截器起作用了。
如何从token中获取用户信息呢?
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
String token = request.getHeader("Authorization");
Claims userInfo = tokenUtils.parseToken(token);
String username = userInfo.get("username").toString();
System.out.println("username="+username);
String password = userInfo.get("password").toString();
System.out.println("password="+password);
我们测试一下吧
由于前后端分离项目中使用json作为数据传输格式,因此需要统一数据结构,方便前后端统一处理。例如,我们约定返回内容统一结构为:
{"code":"状态码","msg":"提示信息","data":"业务数据"}
我们依据“状态码”来确定接口是否调用成功,“提示信息”辅助接口调用情况(例如返回失败原因),而“业务数据”则是接口返回的真正数据,它可以是一个Bean数据对象,也可以是一个List对象。
本工程完整代码下载: https://download.youkuaiyun.com/download/richieandndsc/89953280