目录
一、JWT介绍
二、前端配置
三、后端配置
四、实战
一、JWT介绍
1.1 什么是jwt
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以安全的方式传输信息。JWT 是一种紧凑、自包含的信息载体,可以被解码和验证。它通常用于身份验证和授权服务,特别是在无状态的 Web 应用程序中,比如那些基于 REST 的 API。
1.2 jwt的结构
JWT 由三部分组成,每一部分都由点号(.)分隔开:
-
头部 (Header): 包含关于类型和签名算法的信息。例如:
{"alg":"HS256","typ":"JWT"}
这个头部通常表明使用 HMAC SHA-256 算法签名。
-
负载 (Payload): 也称为“声明”(Claims),包含了要传输的信息。这些信息可以是任意的 JSON 数据,但通常包括一些标准的字段,例如:
{"sub":"1234567890","name":"John Doe","admin":true}
这里 "sub"
是主题(Subject),"name"
是姓名,"admin"
是权限声明。
-
签名 (Signature): 用于验证数据的完整性和确认发送者的身份。签名是通过一个密钥对头部和负载进行加密得到的。
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret
)
如果使用非对称加密,则密钥可以是公钥或私钥。
1.3 jwt工作流程
-
创建和签发: 服务器创建一个 JWT,其中包含用户的身份信息和/或其他数据,然后使用一个秘密密钥或私钥对其进行签名。
-
传输: JWT 通过网络发送给客户端,通常作为 HTTP Authorization header 的一部分。
-
验证和使用: 当客户端向服务器发送请求时,它将 JWT 作为身份验证的一部分。服务器验证 JWT 的签名,以确保它没有被篡改,并从中读取信息。
-
过期: JWT 可以设置一个过期时间,在此之后,它将不再有效。
下面将通过Vue + SpringBoot 实现一个jwt鉴权的项目
二、前端配置
2.1 引入axios
npm install axios
通过添加前端拦截器配置axios
在src下创建一个utils包,再创建一个axios.js文件
import axios from 'axios';// 创建axios实例
const instance = axios.create();// 添加请求拦截器
instance.interceptors.request.use(function (config) {// 在这里添加token到请求头const token = localStorage.getItem('token') || ''; // 从本地存储获取tokenif (token) {config.headers.Authorization = `${token}`;}return config;},function (error) {// 请求错误时的处理return Promise.reject(error);}
);export default instance;
在main.js中配置应用axios
import axios from './utils/axios';Vue.prototype.$axios = axios;
2.3 使用axios
在配置全局后,使用axios就并不需要单独引入axios了,直接使用this.$axios即可调用
this.$axios.get('/api/forum/getAllForumPost', {params: {pageSize: 1,pageNumber: 10}}).then((response) => {console.log(response.data.data);this.posts = response.data.data;});
创建一个TestView.vue测试发送请求时候是否会携带请求头
<template><div><!-- 测试是否会携带请求头 --><button @click="Test"> 发送测试</button></div>
</template><script>
export default {data() {return {};},methods: {Test(){// 假设有登录成功后的tokenlocalStorage.setItem('token', '1234567890');this.$axios.get('/api/Test').then((response) => {console.log(response.data.data);});}},};
</script>
在控制台的网络中查看是否有对应的请求头
已经成功携带,并且名称为Authorization
三、后端配置
3.1 引入依赖
<!-- JWT依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version></dependency>
3.3 由于jwt需要三个属性 密钥 有效期 Token的名称
所以需要配置对应的资源类
@Component
@ConfigurationProperties(prefix = "paitool.jwt")
@Data
public class JwtProperties {private String SecretKey;private long Ttl;private String TokenName;}
application.yml:
paitool:jwt:secret-key: Alphamilkttl: 10800000token-name: Authorization
3.4 创建配置Jwt的工具类 实现快速创建Jwt与解密Jwt方法
public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘钥** @param secretKey jwt秘钥* @param ttlMillis jwt过期时间(毫秒)* @param claims 设置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定签名的时候使用的签名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的时间long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 设置过期时间.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个* @param token 加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}
3.5 通过ThreadLocal实现后端存储用户信息
public class BaseContext {public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();}public static void removeCurrentId() {threadLocal.remove();}}
3.6 配置jwt的拦截器
注意:这里的HandlerMehtod是org.springframework.web.method包下的
@Component
@Slf4j
public class JwtTokenInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校验jwt** @param request* @param response* @param handler* @return* @throws Exception*/public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法,直接放行return true;}//1、从请求头中获取令牌String token = request.getHeader(jwtProperties.getTokenName());//2、校验令牌try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);// 获取JWT的过期时间并转换为可读格式Date expirationDate = claims.getExpiration();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String formattedExpiration = sdf.format(expirationDate);log.info("JWT过期时间:{}", formattedExpiration);Long userId = Long.valueOf(claims.get("userId").toString());log.info("当前用户id:", userId);//通过ThreadLocal保存员工idBaseContext.setCurrentId(userId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}
3.7 将配置好的拦截器加入到webMvc配置中(由于本次实战通过用户登陆获取token,记得排除用户登陆时候进行校验的过程)
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenInterceptor jwtTokenInterceptor;@Overrideprotected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login").excludePathPatterns("/user/GetCaptcha");}
四、实战
1.创建User表单
create table paitool.user
(id int auto_incrementprimary key,account varchar(255) not null,password varchar(255) not null,phone varchar(20) null,address varchar(255) null,isVip tinyint(1) default 0 null,email varchar(255) null,registration_date datetime default CURRENT_TIMESTAMP null,last_login datetime null,status enum ('active', 'inactive') default 'active' null,constraint account_UNIQUEunique (account),constraint email_UNIQUEunique (email),constraint phone_UNIQUEunique (phone)
);
通过MyBatisPlusX自动生成架构
2.创建返回结果实体类
//结果类
public class Result<T> {// 状态码常量public static final int SUCCESS = 200;public static final int ERROR = 500;private int code; // 状态码private String message; // 消息private T data; // 数据// 构造函数,用于创建成功的结果对象private Result(int code, String message, T data) {this.code = code;this.message = message;this.data = data;}// 成功结果的静态方法public static <T> Result<T> success(T data) {return new Result<>(SUCCESS, "Success", data);}// 错误结果的静态方法public static <T> Result<T> error(String message) {return new Result<>(ERROR, message, null);}// 错误结果的静态方法,可以传入自定义的状态码public static <T> Result<T> error(int code, String message) {return new Result<>(code, message, null);}// 获取状态码public int getCode() {return code;}// 设置状态码public void setCode(int code) {this.code = code;}// 获取消息public String getMessage() {return message;}// 设置消息public void setMessage(String message) {this.message = message;}// 获取数据public T getData() {return data;}// 设置数据public void setData(T data) {this.data = data;}// 用于转换为Map类型的方法,方便序列化为JSONpublic Map<String, Object> toMap() {Map<String, Object> map = new HashMap<>();map.put("code", code);map.put("message", message);map.put("data", data);return map;}
}
3.创建验证码(防止密码爆破)工具类 与 Md5加密与解密工具类(防止数据库密码信息泄露)
public class CaptchaUtil {private static final int WIDTH = 200;private static final int HEIGHT = 75;private static final int FONT_SIZE = 36;private static final String DEFAULT_FONT = "Arial";/*** 生成验证码图像.** @param captchaText 验证码原始文本* @return Base64编码的图像字符串*/public static String generateCaptchaImage(String captchaText) {if (captchaText == null || captchaText.isEmpty()) {throw new IllegalArgumentException("Captcha text cannot be null or empty.");}// 创建图像和图形上下文BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);Graphics2D g = (Graphics2D) image.getGraphics();// 设置背景颜色g.setColor(Color.WHITE);g.fillRect(0, 0, WIDTH, HEIGHT);// 绘制验证码文本g.setFont(new Font(DEFAULT_FONT, Font.BOLD, FONT_SIZE));g.setColor(getRandomColor());g.drawString(captchaText, 45, 50);// 添加随机线条作为干扰addNoiseLines(g);// 关闭图形上下文g.dispose();// 将图像转换为Base64编码的字符串try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {ImageIO.write(image, "png", baos);return Base64.getEncoder().encodeToString(baos.toByteArray());} catch (Exception e) {throw new RuntimeException("Error generating captcha image", e);}}private static void addNoiseLines(Graphics2D g) {for (int i = 0; i < 5; i++) {g.setColor(getRandomColor());g.drawLine(getRandomNumber(WIDTH),getRandomNumber(HEIGHT),getRandomNumber(WIDTH),getRandomNumber(HEIGHT));}}private static Color getRandomColor() {return new Color((int) (Math.random() * 255),(int) (Math.random() * 255),(int) (Math.random() * 255));}private static int getRandomNumber(int bound) {return (int) (Math.random() * bound);}
}
public final class MD5Util {/*** 使用MD5算法对字符串进行加密。** @param input 待加密的字符串* @return 加密后的MD5散列值字符串*/public static String encryptToMD5(String input) {try {MessageDigest md = MessageDigest.getInstance("MD5");byte[] hashInBytes = md.digest(input.getBytes());// 将字节数组转换成十六进制字符串StringBuilder sb = new StringBuilder();for (byte b : hashInBytes) {sb.append(String.format("%02x", b));}return sb.toString();} catch (NoSuchAlgorithmException e) {throw new RuntimeException("MD5 algorithm not found", e);}}public static void main(String[] args) {String originalString = "Hello World";String encryptedString = encryptToMD5(originalString);System.out.println("Original: " + originalString);System.out.println("Encrypted: " + encryptedString);}
}
4.创建数据传输与视图的实体类
登陆时候,前端传入数据
@Data
public class LoginDTO {private String account;private String password;// 验证码private String captcha;}
验证通过后传给前端的数据
@Data
public class loginVo {private Integer id;private String account;private Integer isvip;private Object status;private String token;}
4.UserController实现登陆功能
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {@AutowiredUserService userService;@Autowiredprivate JwtProperties jwtProperties;// 登陆时候获取验证码@ApiOperation("获取验证码功能")@GetMapping("/GetCaptcha")public String GetCaptcha(HttpSession session) {// 随机生成四位验证码原始数据String allowedChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";String randomString = generateRandomString(allowedChars, 4);System.out.println("captchaCode " + randomString);// 将验证码保存到session中session.setAttribute("captcha", randomString); // 使用方法参数sessionString ImageByBase64 = CaptchaUtil.generateCaptchaImage(randomString);return ImageByBase64;}// 实现登陆功能@ApiOperation("用户登陆功能")@PostMapping("/login")public Result<loginVo> Login(@RequestBody LoginDTO loginDTO, HttpSession session) { // 使用同一个HttpSession参数String captcha = (String) session.getAttribute("captcha");log.info("用户调用login方法");if (loginDTO.getCaptcha() == null || !loginDTO.getCaptcha().equalsIgnoreCase(captcha)) {session.removeAttribute("captcha");return Result.error("验证码出错了噢!");}// 对密码进行md5加密String encryptToMD5 = MD5Util.encryptToMD5(loginDTO.getPassword());LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(User::getAccount, loginDTO.getAccount()).eq(User::getPassword, encryptToMD5);User user = userService.getOne(lambdaQueryWrapper);if (user == null) {return Result.error("很抱歉,查不到此用户");}loginVo loginVo = new loginVo();BeanUtils.copyProperties(user,loginVo);Map<String,Object> claims = new HashMap<>();claims.put("userId",user.getId());String token = JwtUtil.createJWT(jwtProperties.getSecretKey(), jwtProperties.getTtl(), claims);loginVo.setToken(token);return Result.success(loginVo);}
}
前端账户操作View.vue:
<template><div id="Header"><h3>--PaiTool--</h3><div class="header-avatar"><el-popover placement="bottom" :visible-arrow="false" :visible.sync="showUserInfo"><div class="userInfo"><p>用户名:{{ account }}</p><p>邮箱:{{ email }}</p><p>是否是vip: {{ isVip }}</p><p>账号状态:{{ status }}</p><!-- 登录按钮 --><el-button type="primary" @click="showDialog">登录/注册</el-button><!-- 退出按钮 --><el-button type="text" @click="confirmQuit">退出</el-button><!-- 登录对话框 --><el-dialog title="登录与注册" :visible.sync="dialogLoginVisible" width="30%" @close="resetLoginForm" append-to-body:modal-append-to-body="false"><el-tabs v-model="activeName" @tab-click="handleClick"><el-tab-pane label="登陆" name="first"><el-form :model="loginForm" ref="loginFormRef" label-width="80px"><el-form-item label="用户名:"><el-input v-model="loginForm.account"></el-input></el-form-item><el-form-item label="密码:"><el-input v-model="loginForm.password" show-password></el-input></el-form-item><el-form-item label="验证码"><el-input v-model="loginForm.captcha" style="width: 20%;"></el-input><img :src="captchaImageUrl" alt="验证码" @click="refreshCaptcha" id="captchaImage"></el-form-item></el-form></el-tab-pane><el-tab-pane label="注册" name="second"><el-form :model="loginForm" ref="registerFormRef" label-width="80px"><el-form-item label="注册用户:"><el-input v-model="registerFormRef.account"></el-input></el-form-item><el-form-item label="注册密码:"><el-input v-model="registerFormRef.password" show-password></el-input></el-form-item><el-form-item label="验证码"><el-input v-model="registerFormRef.captcha" style="width: 20%;"></el-input><img :src="captchaImageUrl" alt="验证码" @click="refreshCaptcha" id="captchaImage"></el-form-item></el-form></el-tab-pane></el-tabs><span slot="footer" class="dialog-footer"><el-button @click="dialogLoginVisible = false">取消</el-button><el-button type="primary" @click="submitLogin">登录|注册</el-button></span></el-dialog><!-- 退出确认对话框 --><el-dialog title="确认退出" :visible.sync="dialogConfirmVisible" width="30%" @close="dialogConfirmVisible = false"append-to-body :modal-append-to-body="false"><span>您确定要退出吗?</span><span slot="footer" class="dialog-footer"><el-button @click="dialogConfirmVisible = false">取消</el-button><el-button type="primary" @click="quit">确定退出</el-button></span></el-dialog></div><el-avatar slot="reference" :src="circleUrl" :size="40" class="clickable-avatar"></el-avatar></el-popover></div></div>
</template><script>
import axios from 'axios';
import Cookies from 'js-cookie';export default {data() {return {showUserInfo: false, // 控制个人信息弹窗的显示状态circleUrl: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",isVip: '否',account: '未登录',status: '正常',email: 'none',activeName: 'first',loginOrRegistFlag: true,dialogLoginVisible: false,dialogConfirmVisible: false,loginForm: {username: '',password: '',},registerFormRef: {username: '',password: '',},captchaImageUrl: '', // 初始化为一个空字符串}},mounted() {this.loadUserDataFromCookie();},methods: {loadUserDataFromCookie() {// 从cookie中读取accountconst account = Cookies.get('account');if (account) {this.account = account;}// 从cookie中读取isVipconst isVip = Cookies.get('isVip');if (isVip !== undefined) {// 注意:从cookie读取的数据是字符串类型,需要转换成布尔型this.isVip = isVip === 'true';}// 从cookie中读取statusconst status = Cookies.get('status');if (status) {this.status = status;}// 从cookie中读取emailconst email = Cookies.get('email');if (email) {this.email = email;}},// 打开登录对话框open() {this.dialogLoginVisible = true;},resetLoginForm() {this.$refs.loginFormRef.resetFields();},// 提交登录submitLogin() {// 判断是注册还是登录if (this.loginOrRegistFlag == true) {// 这里添加验证逻辑(如果需要)console.log('登录表单提交:', this.loginForm);this.dialogLoginVisible = false;// 将this.loginForm作为参数上传axios.post("/api/user/login", this.loginForm).then(response => {console.log(response.data);if (response.data.code === 500) {// 重新获取验证码this.refreshCaptcha();this.$message.error(response.data.message);} else if (response.data.code === 200) {this.$message({showClose: true,message: '登陆成功!',type: 'success'});// 设置cookie,可以设置过期时间Cookies.set('account', response.data.data.account, { expires: 7 });Cookies.set('isVip', response.data.data.isVip, { expires: 7 });Cookies.set('status', response.data.data.status, { expires: 7 });Cookies.set('email', response.data.data.email, { expires: 7 });Cookies.set('userId', response.data.data.id, { expires: 7 })localStorage.setItem('token', response.data.data.token);this.account = response.data.data.account;this.isVip = response.data.data.isVip;this.status = response.data.data.status;this.email = response.data.data.email;}}).catch(error => {// 处理错误响应console.error('登录失败:', error);this.$message.error('登陆错了哦,这是一条错误消息')});} else {axios.post('/api/user/register', this.registerFormRef).then(response => {if (response.data.code === 200) {this.$message({showClose: true,message: '注册成功!',type: 'success'});this.dialogLoginVisible = false;} else {this.$message.error(response.data.message);}});}},// 打开退出确认对话框confirmQuit() {this.dialogConfirmVisible = true;},// 执行退出操作quit() {// 这里执行实际的退出逻辑console.log('执行退出操作');this.dialogConfirmVisible = false;// 将Cookie所有字段删除Cookies.remove('account');Cookies.remove('isVip');Cookies.remove('status');Cookies.remove('email');Cookies.remove('userId');this.account = '未登录';this.isVip = '否';this.status = '离线';this.email = 'none';this.$message({showClose: true,message: '退出成功!',type: 'success'});},// 刷新验证码的示例函数refreshCaptcha() {// 实现刷新验证码的逻辑console.log('刷新验证码');this.fetchCaptcha();},fetchCaptcha() {axios.get('/api/user/GetCaptcha').then(response => {this.captchaImageUrl = 'data:image/png;base64,' + response.data;}).catch(error => {console.error('获取验证码失败:', error);});},showDialog() {this.fetchCaptcha(); // 先获取验证码this.dialogLoginVisible = true; // 然后显示登录对话框},handleClick(tab) {if (tab.name === 'first') {this.loginOrRegistFlag = true;} else {this.loginOrRegistFlag = false;}}}
}
</script><style scoped>
h3 {color: #E9EEF3;float: left;width: 1307px;height: 60px;margin-left: 15%;
}.header-avatar {position: relative;/* 为绝对定位的子元素提供上下文 */float: right;z-index: 1000;/* 设置一个较高的 z-index 值以确保其位于其他元素之上 */margin-top: 10px;
}.clickable-avatar {/* 添加点击手势效果 */cursor: pointer;
}.userInfo {text-align: left;padding: 10px;
}#captchaImage {cursor: pointer;width: 136px;height: 45px;border: 1px solid black;float: right;margin-right: 54%;
}
</style>
数据库创建用户与(123456)加密后的密码
account: admin
password: e10adc3949ba59abbe56e057f20f883e
进入前端并进行登陆
查看返回结果的token,前端的login函数已经自动存入了token中了
使用其它功能,查看是否有效
这里看到,后端正常识别到并解析出来了。