Springboot+JWT+Vue实现登录功能
一、前言
最近在写一个Springboot+Vue的前后端分离项目,并且刚学了JWT的功能和原理,正好拿来练练手,在开发过程中也遇到了很多坑点,主要是对vue和springboot不够熟练导致的,因此写篇文章来记录分享。
二、JWT
1.介绍
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
2.请求流程
1. 用户使用账号发出请求; 2. 服务器使用私钥创建一个jwt; 3. 服务器返回这个jwt给浏览器; 4. 浏览器将该jwt串在请求头中像服务器发送请求; 5. 服务器验证该jwt; 6. 返回响应的资源给浏览器。
3.JWT的主要应用场景
身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
4.JWT的结构
JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品,存放我们指定的信息)
Signature 签名/签证
将这三段信息文本用.连接一起就构成了JWT字符串。
就像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
三、Springboot集成JWT
1. Maven添加JWT依赖项
<!--token-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2. 封装Token的生成函数
public String getToken(User user, long time) {
Date start = new Date();//token起始时间
long currentTime = System.currentTimeMillis() + time;
Date end = new Date(currentTime);//token结束时间
String token = "";
token = JWT.create()
.withAudience(user.getLevel().toString()+user.getId().toString()) //存放接收方的信息
.withIssuedAt(start)//token开始时间
.withExpiresAt(end)//token存活截止时间
.sign(Algorithm.HMAC256(user.getPassword()));//加密
return token;
}
3. 编写注解类
1.UserLoginToken 需要登录才能进行操作的注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
2.PassToken 用来跳过验证的
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
4. 编写权限拦截器(AuthenticationInterceptor)
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
//通过所有OPTION请求
if(httpServletRequest.getMethod().toUpperCase().equals("OPTIONS")){
return true;
}
String token = httpServletRequest.getHeader("Authorization");// 从 http 请求头中取出 token
String refreshToken = httpServletRequest.getHeader("freshToken");// 从 http 请求头中取出 token
// 如果该请求不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
// 获取 token 中的 用户信息
String userValue = null;
try {
userValue = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
Map<String, Object> map = new HashMap<>();
map.put("level", (userValue).substring(0,1));
map.put("id", (userValue).substring(1));
User user = userService.findUser(map);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
Date oldTime = JWT.decode(token).getExpiresAt();
Date refreshTime = JWT.decode(refreshToken).getExpiresAt();
long oldDiff = oldTime.getTime() - new Date().getTime();//这样得到的差值是毫秒级别
long refreshDiff = refreshTime.getTime() - new Date().getTime();//这样得到的差值是毫秒级别
if (oldDiff <= 0) {
if (refreshDiff <= 0) {
httpServletResponse.sendError(401);
throw new RuntimeException("401");
}
}
String newToken = userService.getToken(user, 60* 60 * 1000);
String newRefToken = userService.getToken(user, 24*60*60*1000);
// 更新token
httpServletResponse.setHeader("Authorization", newToken);
httpServletResponse.setHeader("freshToken", newRefToken);
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) { // 是否使用@UserLoginToken注解
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("=== 无token,请重新登录 ===");
}
// 利用用户密码,解密验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
System.out.println("=== token验证失败 ===");
httpServletResponse.sendError(401);
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
5. 配置跨域请求和权限拦截器
/**
* Description 解决vue+spring boot跨域问题
**/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public CorsFilter corsFilter()
{
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 是否允许请求带有验证信息
config.setAllowCredentials(true);
// 允许访问的客户端域名
// (springboot2.4以上的加入这一段可解决 allowedOrigins cannot contain the special value "*"问题)
List<String> allowedOriginPatterns = new ArrayList<>();
allowedOriginPatterns.add("*");
config.setAllowedOriginPatterns(allowedOriginPatterns);
// 设置访问源地址
// config.addAllowedOrigin("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
// 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加自定义拦截器和拦截路径,此处对所有请求进行拦截,除了登录界面和登录接口
registry.addInterceptor(appInterceptor())
.addPathPatterns("/api/sms/**")//添加拦截路径,拦截所有
.excludePathPatterns("/login"); // 排除的拦截路径
WebMvcConfigurer.super.addInterceptors(registry);
}
@Bean
public HandlerInterceptor appInterceptor(){
return new AuthenticationInterceptor();
}
}
四、Vue配置JWT
1. 配置axios拦截器
axiosHelper.js
import axios from 'axios';
import {Message} from 'element-ui';
// axios.defaults.timeout = 10000; //超时终止请求
axios.defaults.baseURL = 'http://localhost:8443/'; //配置请求地址
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
axios.defaults.withCredentials = true;
// var loadingInstance
axios.interceptors.request.use(config => {
//Ajax请求执行该方法,请求带上token
var token = localStorage.getItem('userToken');
const refreshToken = localStorage.getItem('refreshToken');
if (token !== null && token !== undefined && token !== '') {
config.headers.Authorization = token;
var host = window.location.host;
config.headers['appHost'] = host;
}
if (refreshToken !== null && refreshToken !== undefined && refreshToken !== '') {
config.headers.freshToken = refreshToken;
}
//全局配置,get请求加时间戳
if (config.method.toLowerCase() === "get") {
config.url += config.url.match(/\\?/) ? "&" : "?";
config.url += "_dc=" + new Date().getTime();
}
return config;
}, error => { //请求错误处理
// loadingInstance.close()
Message.error({
message: '加载超时'
});
return Promise.reject(error);
});
//
var count = 0;
axios.interceptors.response.use(response => {
return response;
},
error => {
if (error.response.status === 401) {
if (count === 0) {
count = count + 1;
} else if (count > 0) {
return null;
}
// debugger
Message.error("身份信息超时,请重新登录!", { icon: 1, time: 2000 });
$cookies.remove('userToken');
setTimeout(function () {
window.location.href = '/#/login';
}, 3000);
return Promise.reject(error);
}
}
);
export default axios; //暴露axios实例
然后在main.js中配置
import axiosHelper from '../src/axios/axiosHelper'
Vue.prototype.axiosHelper = axiosHelper;
2. axios接收Token, 并放入localStorage中
只需在拿到后端数据data后, 添加以下代码
let obj = {
username: data.username,
level: data.level
}
localStorage.setItem('userToken', data.token)
localStorage.setItem('refreshToken', data.refreshToken)
五、总结
至此,我们就配置好了前后端的JWT使用环境,之后还会继续更新我在学习技术时总结的干货,希望大家多多点赞支持哦!