简介
demo在这里
https://github.com/tao1993/spring-boot-demos/tree/main/demo4
jwt的机制大概是后端生成一个token,前端自己每次请求带上这个token来给后端做认证
JWT流程
-》 用户登录
-》 服务器根据用户信息生成一个token作为令牌
-》令牌保存在客户端(一般是localStorage或sessionStorage),服务端不做保存
-》客户端之后每次请求都要带该令牌 (一般是http的header的authorization)
-》 服务端只需要验证该令牌 (签名是否正确,token是否过期,token的接收方是否是自己 等等)
验证ok就放行,不ok就不放行
三个组成部分就是三个json对象,最后会被整合成一个字符串
头部 header
负载 payload
签名 signature
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL90TQkhGayQGMV1GZwhnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2AjMzUTOwcTMzITMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
参考资料
jwt的部分我是跟着b站的视频教程学的,参考这个,通俗易懂
【编程不良人】JWT认证原理、流程整合springboot实战应用,前后端分离认证的解决方案!
前端部分,vue和element ui 用来做界面,axios用来发请求,都是第一次用,前台代码我乱写的
vue文档
axios中文文档
element ui 的NavMenu 导航菜单
Window localStorage 属性
环境
spring boot 2.3.7
jwt 3.12.0
前端效果
未登录
登录后
访问被保护资源
目录结构
集成过程
1.添加依赖
jwt用这个
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
2.封装JWT
src/main/java/cc/mm/demo4/UtilJWT.java
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class UtilJWT {
//加密用到的私钥
private static final String SECRET_KEY = "[email protected]#$$";
public static String createToken(Map<String, String> map) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, 24 * 7); //token 设定7天过期
JWTCreator.Builder builder = JWT.create();
//通过withClaim()放一些用户信息,这些信息是可以被轻松解码解密的,不要放敏感信息在这里
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
String token = builder
.withExpiresAt(calendar.getTime()) //设定token 过期信息
.sign(Algorithm.HMAC384(SECRET_KEY)); //指定加密算法,注意创建token用到的加解密算法要和后面的verify()验证 统一
return token;
}
//返回 ok 表示验证成功
//返回其他的表示错误信息
public static String verify(String token) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC384(SECRET_KEY)).build();
try {
DecodedJWT verify = jwtVerifier.verify(token);
return "ok";
} catch (SignatureVerificationException e) {
return "无效签名";
} catch (TokenExpiredException e) {
return "token过期";
} catch (AlgorithmMismatchException e) {
return "token算法不一致";
} catch (Exception e) {
return "无效签名(其他错误)";
}
}
public static DecodedJWT getDecodedJWT(String token) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC384(SECRET_KEY)).build();
DecodedJWT verify = jwtVerifier.verify(token);
return verify;
}
}
3.配置拦截器
src/main/java/cc/mm/demo4/interceptors/JwtInterceptor.java
import cc.mm.demo4.UtilJWT;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
String verifyResult = UtilJWT.verify(token);
if(verifyResult.equals("ok")) {
//验证成功 放行
return true;
} else {
//验证失败 返回一个json
JSONObject json = new JSONObject();
json.put("result","fail");
json.put("info",verifyResult);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
}
src/main/java/cc/mm/demo4/ConfigInterceptor.java
import cc.mm.demo4.interceptors.JwtInterceptor;
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 ConfigInterceptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/profile"); //测试demo,仅对 /profile 路径做token验证
//.excludePathPatterns("/")
}
}
4.controller和enity
src/main/java/cc/mm/demo4/controller/HomeController.java
import cc.mm.demo4.UtilJWT;
import cc.mm.demo4.enity.User;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Controller
public class HomeController {
@GetMapping("/")
String index(){
return "index";
}
//我的资料 数据,在拦截器里面配置了要带token的请求才能访问
@PostMapping ("/profile")
@ResponseBody
Object profile(HttpServletRequest req){
String token = req.getHeader("token");
String strPayload = UtilJWT.getDecodedJWT(token).getPayload();
strPayload = new String(Base64.getDecoder().decode(strPayload));
//测试,简单返回一些内容
JSONObject json = JSON.parseObject(strPayload);
json.put("info","哈哈哈");
return json;
}
// 接收前台的用户登录请求,验证账号和密码
// 一开始发现 axios发的请求参数 我spring boot后台接收不到,解决办法参考:
// https://blog.csdn.net/qq_36208461/article/details/103079514
// 我是通过后台加 @RequestBody 解决的,前台axios不做改动,使用默认配置
@RequestMapping ("/login")
@ResponseBody
Object login(@RequestBody User user){
JSONObject json = new JSONObject();
//这里因为测试demo,简单模拟验证过程
if(user.getUserName().equals("abc") && user.getUserPass().equals("123")) {
//账号密码对的上说明登陆成功,开始用jwt生成一个token返回给前端,让前端自己去把token保存到localStorage
//可以把一些用户信息放进token,但不要放敏感信息
Map<String,String> map = new HashMap<>();
map.put("userId","11122"); //因为测试,简单写死
map.put("userName",user.getUserName());
String token = UtilJWT.createToken(map);
System.out.println(token);
//给前端返回信息
json.put("token",token);
json.put("result","success");
}else {
json.put("result","fail"); //登录失败
}
return json;
}
}
src/main/java/cc/mm/demo4/enity/User.java
import lombok.Data;
@Data
public class User {
String userName;
String userPass;
}
5.前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 引入element ui -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!-- 引入axios -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-menu
:default-active="activeIndex2"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<el-menu-item index="1">首页</el-menu-item>
<!-- <el-menu-item v-if="isLogined" >-->
<el-menu-item @click="postProfileWithToken">
我的资料(带token请求)
</el-menu-item>
<el-menu-item @click="postProfileWithoutToken">
我的资料(不带token请求)
</el-menu-item>
<!--登录时显示-->
<el-menu-item v-if="isLogined" @click="onLogout">注销</el-menu-item>
<!--未登录时显示-->
<el-menu-item v-if="!isLogined" index="4">
<el-form :inline="true" :model="userData" class="demo-form-inline">
<el-form-item>
<el-input style="margin-top: 7px;" v-model="userData.userName" placeholder="账号"></el-input>
</el-form-item>
<el-form-item>
<el-input style="margin-top: 7px;" v-model="userData.userPass" placeholder="密码"></el-input>
</el-form-item>
<el-form-item>
<el-button style="margin-top: 7px;" type="primary" @click="onSubmit">登录</el-button>
</el-form-item>
</el-form>
</el-menu-item>
<!--登录时显示-->
<el-menu-item v-if="isLogined">欢迎用户: {{ currentUserName }}</el-menu-item>
</el-menu>
</div>
</body>
<script>
//Base64工具,可以用来前台解码token,拿到token的payload的用户信息
Base64 = { _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", encode: function(e) { var t = ""; var n, r, i, s, o, u, a; var f = 0; e = Base64._utf8_encode(e); while (f < e.length) { n = e.charCodeAt(f++); r = e.charCodeAt(f++); i = e.charCodeAt(f++); s = n >> 2; o = (n & 3) << 4 | r >> 4; u = (r & 15) << 2 | i >> 6; a = i & 63; if (isNaN(r)) { u = a = 64 } else if (isNaN(i)) { a = 64 } t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a) } return t }, decode: function(e) { var t = ""; var n, r, i; var s, o, u, a; var f = 0; e = e.replace(/[^A-Za-z0-9+/=]/g, ""); while (f < e.length) { s = this._keyStr.indexOf(e.charAt(f++)); o = this._keyStr.indexOf(e.charAt(f++)); u = this._keyStr.indexOf(e.charAt(f++)); a = this._keyStr.indexOf(e.charAt(f++)); n = s << 2 | o >> 4; r = (o & 15) << 4 | u >> 2; i = (u & 3) << 6 | a; t = t + String.fromCharCode(n); if (u != 64) { t = t + String.fromCharCode(r) } if (a != 64) { t = t + String.fromCharCode(i) } } t = Base64._utf8_decode(t); return t }, _utf8_encode: function(e) { e = e.replace(/rn/g, "n"); var t = ""; for (var n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) { t += String.fromCharCode(r) } else if (r > 127 && r < 2048) { t += String.fromCharCode(r >> 6 | 192); t += String.fromCharCode(r & 63 | 128) } else { t += String.fromCharCode(r >> 12 | 224); t += String.fromCharCode(r >> 6 & 63 | 128); t += String.fromCharCode(r & 63 | 128) } } return t }, _utf8_decode: function(e) { var t = ""; var n = 0; var r = c1 = c2 = 0; while (n < e.length) { r = e.charCodeAt(n); if (r < 128) { t += String.fromCharCode(r); n++ } else if (r > 191 && r < 224) { c2 = e.charCodeAt(n + 1); t += String.fromCharCode((r & 31) << 6 | c2 & 63); n += 2 } else { c2 = e.charCodeAt(n + 1); c3 = e.charCodeAt(n + 2); t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63); n += 3 } } return t } }
</script>
<script>
var app = new Vue({
el: '#app',
data: {
activeIndex: '1',
activeIndex2: '1',
userData: {
userName: '',
userPass: ''
}
},
methods: {
handleSelect(key, keyPath) {
//console.log(key, keyPath);
},
onSubmit() {
//用户登录,post请求向后台 验证账号和密码
let user = this.userData;
axios.post('/login', {
userName: user.userName,
userPass: user.userPass
}).then(function (response) {
//console.log(response);
if (response.data.result === 'fail') {
app.$message('登录失败');
} else if (response.data.result === 'success') {
console.log(response.data.token)
//登录成功,拿到token,放入localStorage
localStorage.setItem("token", response.data.token)
//刷新页面
location.reload();
}
}).catch(function (error) {
console.log(error);
});
},
onLogout() { //注销操作
//清除localStorage里面的token,然后刷新页面
localStorage.removeItem("token");
location.href = "/";
},
postProfileWithToken(){ //带上token访问 /profile
axios({
url:'/profile',
method:'post',
headers: {'token': localStorage.getItem("token")},
data:{},
}).then(function (response) {
app.$message(JSON.stringify(response.data));
console.log(response);
}).catch(function (error) {
console.log(error);
});
},
postProfileWithoutToken(){ // 不带token访问 /profile
axios({
url:'/profile',
method:'post',
data:{},
}).then(function (response) {
app.$message(JSON.stringify(response.data));
console.log(response);
}).catch(function (error) {
console.log(error);
});
}
},
computed: {
isLogined() {
//判断当前是否有用户已登录状态,从localstorage里面找token
//没有token说明未登录
if (localStorage.getItem("token")) {
return true;
}
return false;
},
currentUserName(){
//解析localStorage里面的token,获得payload部分的用户信息userName
let token = localStorage.getItem("token");
let user = decodeURIComponent(escape(window.atob(token.split('.')[1])));
let userName = JSON.parse(user).userName;
return userName;
}
}
})
</script>
</html>