简介
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
参考资料
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 = "xxxabc!@#$$";
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>