文章目录
前言
这篇博客是在我上篇发的 SpringBoot+Shiro+Redis+Mybatis-plus 实战项目 之上添加了JWT认证和前后端分离,所以这篇博客重点是贴出 JWT 学习总结的代码,希望可以帮助到大家!
JWT学习总结
什么是JWT?
JWT 全称就是 JSON WEB TOKEN,可以看作是一个获得请求资格的令牌,我们有了这个令牌,才可以访问到网站的大部分功能(接口)。
JWT的结构?
JWT 分成三段
- header
header 里面主要是放 加密的算法名和类型 - payload(负载)
payload 主要是放一些我们想传递给 token 中保存的用户部分信息字段,比如 用户名 用户ID等 - sign(核心安全信息)
sign 是JWT 的核心安全信息,它其实就是一个字符串,在公司中这个字符串一定不能被暴露出去。
以 . 号连接,有点像 IP 地址的格式
例如: header.payload.sign
JWT整合SpringBoot的依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.15.0</version>
</dependency>
JWT核心代码配置
JWTUtil
package com.jmu.shiro_demo.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.security.Signature;
import java.util.Calendar;
import java.util.Map;
public class JWTutil {
private static final String SIGN = "jiachengren"; //JWT 签名
private static final int DEFAULT_JWT_EXPIRE_DAYS = 7; //默认JWT过期天数
public static String getToken(Map<String,String> map) {
JWTCreator.Builder builder = JWT.create();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,DEFAULT_JWT_EXPIRE_DAYS);
//1.header 默认
//2.payload 遍历 map
map.forEach((k,v) -> {
builder.withClaim(k,v);
});
//3.设置过期时间和签名后生成 token
String token = builder
.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SIGN));
return token;
}
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
}
JWT拦截器
package com.jmu.shiro_demo.intercepetor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jmu.shiro_demo.utils.JWTutil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class JWTIntercepetor implements HandlerInterceptor {
private final String TOKEN_NAME = "token";
//JWT拦截器,所有请求都被这个拦截器拦截,校验header中的token,token校验通过再放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.token一般存放在 header 中,所以从 request.getHeader()中获取 token
String token = request.getHeader(TOKEN_NAME);
Map<String,Object> map = new HashMap<String, Object>();
try {
JWTutil.verify(token);
map.put("state",true);
return true;
} catch (AlgorithmMismatchException e) {
map.put("msg","JWT算法不匹配!");
} catch (SignatureVerificationException e) {
map.put("msg","JWT签名不匹配!");
} catch (TokenExpiredException e) {
map.put("msg","token(用户信息)已经过期,请重新登录!");
} catch (Exception e) {
map.put("msg","token无效!");
}
map.put("state",false);
response.setContentType("application/json");
String msg = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(msg);
return false;
}
}
全局拦截器配置
package com.jmu.shiro_demo.config;
import com.jmu.shiro_demo.intercepetor.JWTIntercepetor;
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 IntercepetorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTIntercepetor())
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/logout","/js/**","/index","/getAuthCode","/login","/toLogin","/user/**"); // 放行 登陆请求 下面的所有请求
}
}
登陆成功的时候生成JWT token 返回给前端
package com.jmu.shiro_demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jmu.shiro_demo.entity.Permission;
import com.jmu.shiro_demo.entity.Role;
import com.jmu.shiro_demo.entity.User;
import com.jmu.shiro_demo.mapper.UserMapper;
import com.jmu.shiro_demo.service.RoleService;
import com.jmu.shiro_demo.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jmu.shiro_demo.utils.JWTutil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* <p>
* 服务实现类
* </p>
*
* @author ${author}
* @since 2021-04-20
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private RoleService roleService;
@Override
public String login(String username, String password, String code, HttpSession session, HttpServletResponse response, Model model) {
//在校验登陆之前先检验验证码的正确性
String realAuthCode = (String) session.getAttribute("code");
if(!realAuthCode.equalsIgnoreCase(code)) {
model.addAttribute("msg","验证码错误!");
return "login";
}
//1.获取 Subject
Subject subject = SecurityUtils.getSubject();
//2.封装 token
UsernamePasswordToken shiroToken = new UsernamePasswordToken(username, password);
//3.调用 subject.login(token) 方法
try {
subject.login(shiroToken);
//登录成功之后返回 token 给客户端
Map<String,String> map = new HashMap<String, String>();
map.put("username",username);
String token = JWTutil.getToken(map); //登陆成功,生成JWT token
response.setHeader("token",token); // 将 token 设置在 header 里面
model.addAttribute("token",token);
}catch (UnknownAccountException e1) {
//用户名不存在
model.addAttribute("msg","用户名不存在!");
return "/login";
}catch (IncorrectCredentialsException e2) {
model.addAttribute("msg","密码错误!");
return "/login";
}
return "/index";
}
@Override
public User getUserByUserName(String username) {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.eq("username",username);
return this.baseMapper.selectOne(wrapper);
}
@Override
public List<Permission> getUserPermissionsByUserId(Integer userId) {
List<Role> roles = getUserRoleByUserId(userId);
List<Permission> permissions = new LinkedList<Permission>();
roles.stream().forEach(role -> {
permissions.addAll(roleService.getRolePermissionsByRoleId(role.getRoleid()));
});
return permissions;
}
@Override
public List<Role> getUserRoleByUserId(Integer userId) {
return this.baseMapper.getUserRoleByUserId(userId);
}
@Override
public String logout() {
SecurityUtils.getSubject().logout();
return "redirect:/index";
}
}
前端如何利用 JWT token
这是 前后端分离的登陆界面, 登陆成功后在 success 里面进行保存 token 和跳转页面
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
<script th:src="@{/js/jquery-3.6.0.slim.min.js}"></script>
</head>
<body>
<h3 style="color: red" th:text="${msg}"/>
<div>
账号: <input type="text" name="username" id="username"> <br/>
密码: <input type="password" name="password" id="password"> <br/>
验证码: <input type="text" name="code" id="code"><img id="changeImg" src="/getAuthCode" alt=""><br/>
<button id="loginBtn">登陆</button>
</div>
<script>
$(function () {
//点击更换验证码 拼接随机数
$("#changeImg").click(function(){
$("#changeImg").attr('src',"/getAuthCode?d="+Math.random());
});
$('#loginBtn').click(function () {
let username = $('#username').val()
let password = $('#password').val()
let code = $('#code').val()
$.ajax({
url:"/user/login",
data:{"username":username,"password":password,"code":code},
method:"POST",
success: function (data) {
window.localStorage.setItem("token",data.token) //将token设置在页面的 localStorage 中
window.location = data.url //跳转页面
console.log(data)
},
error: function () {
alert("error!")
}
})
})
})
</script>
</body>
</html>
项目源码(CodeChina平台)
踩过的坑
- 前端报错 $.ajax is not function
解决:将压缩版本的 jquery 更换成 非压缩的 - jquery获取不到 文本框中的值
解决:使用 $(’#id’).val() 函数 - js被拦截
解决:所有的拦截器的时候放行 /js/**
项目运行
这里主要演示前后端分离下JWT的效果,shiro 和 redis 的效果在上篇实战博客中有发
演示说明,登陆的时候会保存一个 jwt token 到 客户端(浏览器)的localstorage 中,然后每次发起一个后台请求的时候,在 请求头(header)中就会携带这个 token, 后台如果校验这个 token 通过,就响应请求,在这里我的响应请求的方式,就是简单的成功跳转页面。
小彩蛋: 写这篇博客文章的时候,博主是一边听赵雷(一个低调优秀的民谣歌手)的《鼓楼》这首民谣,真的好听!推荐大家也去听(不是广告