SSM+JWT实现前后端分离的token验证
前言
以前写的web项目都是没有前后端分离的,都是写的jsp,或者说前后端分离也没有使用token,都是使用session,后来发现这种使用session的方式非常不好,而且在前后端分离的情况下也不太适用,所以学习一下基于JWT的token验证。
什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
基于Token的验证流程
- 用户使用用户名、密码进行登录
- 服务器验证用户信息
- 验证通过,服务器给客户端发送一个token
- 客户端保存这个token,保存方法可以存在cookie中
- 此后客户端每次请在请求头上加上此token,后端会验证token,并根据是否验证通过而返回不同的数据
token一般是一个比较长的字符串,如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJmanoiLCJpYXQiOjE2MDgzODk3MDEsInN1YiI6IkZqeiIsImlzcyI6IkplcnNleS1TZWN1cml0eS1CYXNpYyIsImV4cCI6MTYwODM5MTUwMX0.23EogcNb-_OsQCPZqXc9Eh6OQi101XQODyw5DGqsT3Y
JWT的Token实现
后端部分
- 先创建一个基于Maven的SSM项目,不懂的小伙伴可以参考一下我以前的博客(不用Maven也行,只要导入下面说的相应的jar包就行)
附上Maven搭建SSM的博客链接: https://blog.csdn.net/weixin_44215175/article/details/108642595 - 在pom.xml加入除SSM的基本依赖外的一下json依赖以及JWT的依赖
<!--jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
<!-- Jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<!-- slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
- 编写JWT工具类,网上有很多,我这里也是从网上抄了一个
原文链接:https://blog.csdn.net/qq_37380557/article/details/97375948?utm_source=app
package pers.kuroko.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
public class JWTUtil {
// token加密时使用的秘钥,一旦得到此秘钥也就可以伪造token了
public static String secretKey = "KurokoJwtSecretKey";
// 代表token的有效时间
public final static long KEEP_TIME = 1800000;
/**
* JWT由3个部分组成,分别是 头部Header,载荷Payload一般是用户信息和声明,签证Signature一般是密钥和签名
* 当头部用base64进行编码后一般都会呈现eyJ...形式,而载荷为非强制使用,签证则包含了哈希算法加密后的数据,
* 包括转码后的header,payload和secretKey
*
* @param id 用户id
* @param issuer 签发者
* @param subject 一般用户名
* @return String
*/
public static String generateToken(String id, String issuer, String subject) {
long ttlMillis = KEEP_TIME;
// 使用Hash256算法进行加密
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 获取当前时间戳
long nowMills = System.currentTimeMillis();
Date now = new Date(nowMills);
// 将秘钥转成base64形式再转成字节码
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secretKey);
// 对其使用Hash256进行加密
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// JWT生成类,此时设置iat, 以及根据传入的id设置token
JwtBuilder builder = Jwts.builder().setId(id).setIssuedAt(now);
// 由于payload(有效载荷)是非必须的,所以这时加入检测
if (subject != null) {
builder.setSubject(subject);
}
if (issuer != null) {
builder.setIssuer(issuer);
}
// 进行签名,生成signature
builder.signWith(signatureAlgorithm, signingKey);
if (ttlMillis >= 0) {
long expMills = nowMills + ttlMillis;
Date exp = new Date(expMills);
builder.setExpiration(exp);
}
// 返回最终的token结果
return builder.compact();
}
/**
* 此函数用于更新token
* @param token token
* @return String
*/
public static String updateToken(String token) {
// Claims 就是包含了我们的payload信息类
Claims claims = verifyToken(token);
String id = claims.getId();
String subject = claims.getSubject();
String issuer = claims.getIssuer();
// 生成新的token,根据现在的时间
return generateToken(id, issuer, subject);
}
/**
* 将token解析,将payload信息包装成Claims类再返回
* @param token token
* @return Claims
*/
private static Claims verifyToken(String token) {
Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(secretKey))
.parseClaimsJws(token).getBody();
return claims;
}
}
- 编写拦截器,进行token的验证
package pers.kuroko.interceptor;
import com.alibaba.fastjson.JSON;
import org.apache.log4j.Logger;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import pers.kuroko.dto.ResponseData;
import pers.kuroko.utils.JWTUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HeaderTokenInterceptor implements HandlerInterceptor {
private static final Logger LOG = Logger.getLogger(HeaderTokenInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
ResponseData responseData = null;
// 获取请求头中的token验证字符串
String headerToken = request.getHeader("token");
// 检测当前页面,设置当前页不是登录页面时就对其进行拦截
// 具体方法就是检测URL中有没有login字符串
if (!request.getRequestURI().contains("login")) {
if (headerToken == null) {
// 如果没有token,返回错误信息
responseData = ResponseData.customerError();
}
try {
// 对token更新与验证
headerToken = JWTUtil.updateToken(headerToken);
LOG.debug("token验证通过,并且续期了");
} catch (Exception e) {
LOG.debug("token验证出现异常!");
// 这里的ResponseData类自定义的返回信息类
responseData = ResponseData.customerError();
}
}
// 如果有错误信息
if (responseData != null) {
response.getWriter().write(JSON.toJSONString(responseData));
return false;
} else {
// 将token加入返回的header中
response.setHeader("token", headerToken);
return true;
}
}
}
- 编写controller、service、dao
UserController
package pers.kuroko.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.kuroko.dto.ResponseData;
import pers.kuroko.entity.User;
import pers.kuroko.service.UserService;
import pers.kuroko.utils.JWTUtil;
@RestController("/userController")
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseData login(User user) {
User login = userService.login(user);
ResponseData responseData = ResponseData.ok();
if (login != null) {
// 生成token
String token = JWTUtil.generateToken(login.getUid(), "Kuroko-Security-Basic", login.getUname());
// 向浏览器返回token,客户端收到此token后存入cookie中,或者h5的本地存储
responseData.putDataValue("token", token);
login.setUpwd("******");
responseData.putDataValue("user", login);
} else {
// 用户名或者密码错误
responseData = ResponseData.customerError();
}
return responseData;
}
}
service(只贴出实现类)
package pers.kuroko.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pers.kuroko.dao.UserDao;
import pers.kuroko.entity.User;
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User login(User user) {
return userDao.login(user);
}
}
dao,对应的mapper也只是简单的查询数据库,这里就不贴出来了
package pers.kuroko.dao;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import pers.kuroko.entity.User;
@Repository("userDao")
@Mapper
public interface UserDao {
User login(User user);
}
IndexController
package pers.kuroko.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.kuroko.dto.ResponseData;
@RestController("indexController")
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/index")
public ResponseData index() {
return ResponseData.ok();
}
}
- 编写处理跨域的拦截器
package pers.kuroko.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HttpInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 允许跨域,这里是运行所有的ip请求
response.setHeader("Access-Control-Allow-Origin", "*");
// 设置请求头允许的请求方式
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
// 允许自定义请求头token(允许head跨域)
response.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
- 在springmvc的配置文件中配置拦截器
<mvc:interceptors>
<!-- 允许跨域 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="pers.kuroko.interceptor.HttpInterceptor"/>
</mvc:interceptor>
<!-- 检验token -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<!-- <mvc:exclude-mapping path="/index/index"/>-->
<bean class="pers.kuroko.interceptor.HeaderTokenInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
前端部分
- login.html
<body>
用户id:<input type="text" name="uid"><br>
密 码:<input type="password" name="upwd"><br>
<input type="submit" onclick="login()" value="登录">
</body>
<script src="./js/jquery-1.4.2.js"></script>
<script src="./js/jquery.cookie.js"></script>
<script type="text/javascript">
function login() {
let uid = $("input[name='uid']").val();
let upwd = $("input[name='upwd']").val();
$.ajax({
url: "http://localhost:8081/SSMJWTDemo/user/login",
type: "POST",
dataType: "json",
data: {
uid: uid,
upwd: upwd
},
success: function(res) {
if(res.code == 200) {
console.log(res);
//保存token用来判断用户是否登录,和身份是否属实
$.cookie('token', res.data.token);
$.cookie('uname', res.data.user.uname);
$.cookie('user', res.data.user);
//转向主页面
location="index.html";
} else {
alert("用户名或者密码错误!");
}
},
error: function(err) {
console.log(err);
}
})
}
</script>
- index.html
<body>
欢迎你:<span id="user"></span>
<br>
<button onclick="logout()">注销</button>
</body>
<script src="./js/jquery-1.4.2.js"></script>
<script src="./js/jquery.cookie.js"></script>
<script type="text/javascript">
/**
* 请求数据的ajax,需要从cookie读取token放入head传给后台。
*/
loadDeptTree();
function loadDeptTree() {
console.log('获取到的token = ' + $.cookie('token'));
$.ajax({
// 自定义的headers字段,会出现option请求,在GET请求之前,后台要记得做检验。
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("token", $.cookie('token'));
},
url: "http://localhost:8081/SSMJWTDemo/index/index",
type: 'GET',
dataType: 'json',
success : function (result) {
$('#user').html($.cookie('uname'));
console.log(result);
if(result.code==200){
alert("加载到的数据:"+result+",并进行渲染页面.....");
}else{
alert("异常,非法token,这里不直接判断是否token不对,实际开发需要各种判断返回码。");
}
}
})
}
/**
* 注销,清空所有cookie(或者只清空保存着token的Cookie就行)
*/
function logout() {
var keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if(keys) {
for(var i = keys.length; i--;)
document.cookie = keys[i] + '=0;expires=' + new Date(0).toUTCString()
}
//返回登录页面或者主页
window.location.href = "login.html";
}
</script>
测试
登录
登录成功后
此时的Cookie为:
点击注销后:
不进行登录直接请求index.html页面
项目完整地址
本demo源码已经上传到GitHub上,地址为:
https://github.com/Fjz-Kuroko/SSMJWTDemo