第23章 SpringBoot集成Token

传统的 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值