采用Vue3编写,采用Axiozidingzs进行Ajax通信,后端采用jwt进行校验,将token放在请求头部
利用自定义注解控制权限,这一部分只讲解jwt的验证及头部存放token进行axios通信
讲解基本的依赖引入及jwt的生成和验证
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<!-- 基本的spring boot、jwt 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
jwt工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Calendar;
/**
* @author bbyh
* @date 2022/10/7 0007 19:43
* @description
*/
public class JwtUtils {
private static final String SING = "Hello";
public static String getToken() {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE, 30); // 可以在这里设置过期时间,也可以结合redis使用
JWTCreator.Builder builder = JWT.create();
return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));
}
public static void verify(String token) {
JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
}
}
拦截器部分(考虑到只是验证头部请求是否能够正确接收到,所以拦截器设置的很简单)
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author bbyh
* @date 2022/10/14 0014 15:11
* @description
*/
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("token");
System.out.println("JwtInterceptor: " + token);
// 这里的验证部分,也可以采用多一些异常捕获,细致一些
try {
JwtUtils.verify(token);
System.out.println("token正确");
return true;
} catch (Exception e) {
System.out.println("token错误");
}
return false;
}
}
添加配置类及添加拦截器(这里的拦截路径也只是为了方便进行后续操作)
另外附带说明:如果在前面的拦截器内使用了StringRedisTemplate或RedisTemplate
则需要将下面的初始化 new MyInterceptor() 改为@Bean引入
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author bbyh
* @date 2022/10/14 0014 15:12
* @description
*/
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/getToken");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 这里可以配置允许接收请求的IP,例如:http://localhost:8080
// .allowedOrigins("http://localhost:8080")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
使用了 StringRedisTemplate 的写法
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author bbyh
* @date 2022/10/14 0014 15:12
* @description
*/
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Bean
public MyInterceptor myInterceptor(){
return new MyInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/getToken");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
后端控制器部分(只是为了验证头部携带token能否被成功获取,所以请求非常简单)
import com.boot.config.JwtUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* @author bbyh
* @date 2022/10/14 0014 14:58
* @description
*/
@RestController
// @CrossOrigin(maxAge = 3600, origins = "*")
// 这里没有采用跨域,在配置类里面配好了,在这里配比较麻烦,需要每个控制器都加
public class Test {
@GetMapping("/get")
public String test1(HttpServletRequest request) {
System.out.println(request.getHeader("token"));
return "get";
}
@PostMapping("/post")
public String test2(HttpServletRequest request) {
System.out.println(request.getHeader("token"));
return "post";
}
@DeleteMapping("/delete")
public String test3(HttpServletRequest request) {
System.out.println(request.getHeader("token"));
return "delete";
}
@PutMapping("/put")
public String test4(HttpServletRequest request) {
System.out.println(request.getHeader("token"));
return "put";
}
@GetMapping("/getToken")
public String getToken() {
return JwtUtils.getToken();
}
}
自定义注解
RoleEnum.java
package com.boot.annotation;
/**
* @author bbyh
* @date 2022/10/13 0013 12:46
* @description
*/
public enum RoleEnum {
/**
* Admin表示管理员,Guest表示未登录游客,User表示登陆的一般用户
*/
Admin,
Guest,
User
}
Roles.java
package com.boot.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author bbyh
* @date 2022/10/13 0013 12:42
* @description
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Roles {
RoleEnum value();
}
MyConfig.java
package com.boot.config;
import com.boot.annotation.RoleEnum;
import com.boot.annotation.Roles;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author bbyh
* @date 2022/10/13 0013 12:54
* @description
*/
public class MyConfig implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
boolean assignableFrom = handler.getClass().isAssignableFrom(HandlerMethod.class);
System.out.println("此方法是否存在注解:" + assignableFrom);
String token = request.getHeader("token");
if (assignableFrom) {
Roles methodAnnotation = ((HandlerMethod) handler).getMethodAnnotation(Roles.class);
System.out.println("当前方法的注解是:" + methodAnnotation);
if (methodAnnotation == null) {
System.out.println("当前没有添加注解");
} else {
System.out.println(methodAnnotation.value());
System.out.println("当前添加了注解");
if (methodAnnotation.value() == RoleEnum.Guest) {
return true;
} else if (methodAnnotation.value() == RoleEnum.User) {
}
}
}
return true;
}
}
WebConfig.java
package com.boot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author bbyh
* @date 2022/10/13 0013 13:01
* @description
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyConfig()).addPathPatterns("/**");
}
}
TestController.java
package com.boot.controlller;
import com.boot.annotation.RoleEnum;
import com.boot.annotation.Roles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author bbyh
* @date 2022/10/13 0013 12:48
* @description
*/
@RestController
public class TestController {
@GetMapping("/test")
@Roles(RoleEnum.Guest)
public String test() {
return "test";
}
}
采用vue3,引入了axios,采用 npm i axios 安装
请求封装的api.js(为了方便获取token,我就放在了sessionStorage里面,也可以放在Vuex里面,
但是Vuex我还没有进行试验,我猜也是可以获取到数据的)
附注:让我感到为难的,是安全性的问题,这个token一旦被获取就没有办法保证接口不被坏人使用了
尽管可以在后端角色内加上限制,但对于单个用户,它的安全性,只存在于密码和token,感觉安全性很空洞
import axios from "axios";
const apiURL = 'http://127.0.0.1:9001';
export const getRequest = (url, params) => {
return axios({
headers: {
token: sessionStorage.getItem('token')
},
method: 'get',
url: apiURL + url,
params: params
})
}
export const postRequest = (url, params) => {
return axios({
headers: {
token: sessionStorage.getItem('token')
},
method: 'post',
url: apiURL + url,
data: params,
})
}
export const putRequest = (url, params) => {
return axios({
headers: {
token: sessionStorage.getItem('token')
},
method: 'put',
url: apiURL + url,
data: params,
})
}
export const deleteRequest = (url, params) => {
return axios({
headers: {
token: sessionStorage.getItem('token')
},
method: 'delete',
url: apiURL + url,
data: params,
})
}
App.vue文件内容(这里直接进行axios请求,尝试向后端发送附带token的请求)
<template>
<div id="app">
<button @click="testGet">testGet</button>
<br><br>
<button @click="testPost">testPost</button>
<br><br>
<button @click="testPut">testPut</button>
<br><br>
<button @click="testDelete">testDelete</button>
<br><br>
<button @click="getToken">getToken</button>
</div>
</template>
<script>
import { deleteRequest, getRequest, postRequest, putRequest } from './utils/api';
export default {
name: "App",
setup(){
function testGet(){
getRequest('/get').then((res)=>{
console.log(res);
})
}
function testPost(){
postRequest('/post').then((res)=>{
console.log(res);
})
}
function testPut(){
putRequest('/put').then((res)=>{
console.log(res);
})
}
function testDelete(){
deleteRequest('/delete').then((res)=>{
console.log(res);
})
}
function getToken(){
getRequest('/getToken').then((res)=>{
console.log(res.data);
sessionStorage.setItem('token', res.data)
console.log(res);
})
}
return {
testGet,
testPost,
testPut,
testDelete,
getToken
}
}
};
</script>
结果说明:验证发现,可以通过上述封装将headers内的内容发送到后端,请求成功
附注:有时候请求会出现被跨域拦截,原因是一个请求分裂为两个请求,然后那个preflight被拦截了,后面那个请求就一直拿不到token,但当只有一个请求的时候是不会被跨域拦截的,因为我们后端设置好了跨域
关于preflight请求的详细内容,可以参考这两篇博客:博客一 博客二
下面也给出简单的处理方式,即过滤该请求,在拦截器中稍微修改一下判断流程即可
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author bbyh
* @date 2022/10/14 0014 15:11
* @description
*/
@Component
public class MyInterceptor implements HandlerInterceptor {
private static final String PREFLIGHT = "OPTIONS";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("token");
// 这里对请求方式进行判断,preflight采用OPTIONS方式请求,就可以直接过滤,选择不拦截
// 这样在preflight的下个请求中就可以成功在头部获得token
if (PREFLIGHT.equals(request.getMethod())) {
System.out.println("触发了一次" + PREFLIGHT + "请求");
return true;
}
try {
JwtUtils.verify(token);
System.out.println("token正确");
return true;
} catch (Exception e) {
System.out.println("token错误");
}
return false;
}
}
目前难缠的跨域问题将告一段落,最后也推荐这篇跨域文章(跨域资源共享 CORS 详解)
经过试验得出,非简单请求确实会触发preflight请求,但当头部的token被后端获取过后,再次刷新了token,之后便不会再进行preflight请求检验,这里面还需要再研究研究。