目录
一. jwt的简介
JWT全称为JSON Web Token,是一种用于身份验证和授权的开放标准。它是一个紧凑且安全的方式,通过在不同系统之间传递可信任的信息来实现用户身份验证。JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
头部包含了描述令牌类型和所使用的加密算法的元数据。常见的加密算法有HMAC、RSA等。载荷包含了要传输的数据,比如用户ID、角色和其他相关的声明信息。载荷可以自定义,但需要遵循一定的规范。
签名是对头部和载荷进行签名生成的,用于验证令牌的真实性和完整性。它使用头部中指定的加密算法和密钥进行签名,并将签名与头部和载荷一起编码为一个字符串形式的JWT。
JWT的精髓在于:“去中心化”,数据是保存在客户端的。
JWT的工作流程通常如下:当用户登录时,服务器会生成一个JWT并返回给客户端。客户端在后续的请求中将这个JWT放在请求的头部或参数中发送给服务器。服务器在接收到请求后会解析JWT,并根据其中的信息进行身份验证和授权。
JWT的优点是可以避免在服务器端存储session信息,使得服务端无状态化,提高了系统的可扩展性。同时,JWT还具有跨域支持和自包含性的特点,使得它在分布式系统中广泛应用。
需要注意的是,JWT仅仅对于信息的完整性和真实性提供了保护,但并不加密具体的数据。因此,在使用JWT时,应避免在载荷中包含敏感信息,或者对敏感信息进行加密处理。另外,由于JWT是基于Token的认证方式,为了确保安全性,应采取适当的措施保护JWT的传输过程和存储过程。
二. jwt工具类介绍
JWT工具类是一个用于生成、解析和验证JSON Web Token的辅助类。它提供了一系列方法来简化JWT的处理过程,使得开发者可以方便地在应用中使用JWT进行身份验证和授权。
JWT工具类通常包括以下功能:
-
生成JWT:提供方法用于生成JWT,传入有效载荷和加密密钥等参数,生成符合规范的JWT字符串。
-
解析JWT:提供方法用于解析JWT,将JWT字符串解析成头部和载荷的JSON对象,并对签名进行验证。
-
验证JWT:提供方法用于验证JWT的有效性,包括验证签名是否正确、验证令牌是否过期等。
-
获取载荷信息:提供方法用于获取JWT中的载荷信息,比如用户ID、角色等。
-
刷新JWT:提供方法用于刷新JWT,生成新的JWT并返回给客户端,用于延长认证的有效期。
-
配置参数:提供方法用于配置JWT的相关参数,比如加密算法、密钥、过期时间等。
使用JWT工具类可以简化JWT的处理逻辑,减少代码重复性,并增加系统的安全性。通过封装好的方法,开发者可以方便地在不同的场景中使用JWT进行身份验证和授权,提高开发效率。但需要注意,在使用JWT工具类时,应确保密钥的安全性,避免泄露导致令牌被篡改或伪造。
package com.zking.ssm.jwt;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* JWT验证过滤器:配置顺序 CorsFilte->JwtUtilsr-->StrutsPrepareAndExecuteFilter
*
*/
public class JwtUtils {
/**
* JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认30分钟
*/
public static final long JWT_WEB_TTL = 30 * 60 * 1000;
/**
* 将jwt令牌保存到header中的key
*/
public static final String JWT_HEADER_KEY = "jwt";
// 指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了。
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙
private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key
static {
byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
private JwtUtils() {
}
/**
* 解密jwt,获得所有声明(包括标准和私有声明)
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJwt(String jwt) {
Claims claims = Jwts.parser()
.setSigningKey(JWT_KEY)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
/**
* 创建JWT令牌,签发时间为当前时间
*
* @param claims
* 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
* @param ttlMillis
* JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间
* @return jwt令牌
*/
public static String createJwt(Map<String, Object> claims,
long ttlMillis) {
// 生成JWT的时间,即签发时间 2021-10-30 10:02:00 -> 30 10:32:00
long nowMillis = System.currentTimeMillis();
//链式语法:
// 下面就是在为payload添加各种标准声明和私有声明了
// 这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
// 可以在未登陆前作为身份标识使用
.setId(UUID.randomUUID().toString().replace("-", ""))
// iss(Issuser)签发者,写死
.setIssuer("zking")
// iat: jwt的签发时间
.setIssuedAt(new Date(nowMillis))
// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
// .setSubject("{}")
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(SIGNATURE_ALGORITHM, JWT_KEY)
// 设置JWT的过期时间
.setExpiration(new Date(nowMillis + ttlMillis));
return builder.compact();
}
/**
* 复制jwt,并重新设置签发时间(为当前时间)和失效时间
*
* @param jwt
* 被复制的jwt令牌
* @param ttlMillis
* jwt的有效时间(单位毫秒),当前时间+有效时间=过期时间
* @return
*/
public static String copyJwt(String jwt, Long ttlMillis) {
//解密JWT,获取所有的声明(私有和标准)
//old
Claims claims = parseJwt(jwt);
// 生成JWT的时间,即签发时间
long nowMillis = System.currentTimeMillis();
// 下面就是在为payload添加各种标准声明和私有声明了
// 这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
// 可以在未登陆前作为身份标识使用
//.setId(UUID.randomUUID().toString().replace("-", ""))
// iss(Issuser)签发者,写死
// .setIssuer("zking")
// iat: jwt的签发时间
.setIssuedAt(new Date(nowMillis))
// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
// .setSubject("{}")
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(SIGNATURE_ALGORITHM, JWT_KEY)
// 设置JWT的过期时间
.setExpiration(new Date(nowMillis + ttlMillis));
return builder.compact();
}
}
三. jwt集成spa项目
后台要求:
1. 用户登录方法,放开用户信息生成jwt串保存到响应头中
package com.zking.ssm.controller;
import com.zking.ssm.service.IUserService;
import com.zking.ssm.util.JsonResponseBody;
import com.zking.ssm.util.PageBean;
import com.zking.ssm.vo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.zking.ssm.jwt.*;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@RequestMapping("/userLogin")
@ResponseBody
public JsonResponseBody<?> userLogin(UserVo userVo, HttpServletResponse response){
if(userVo.getUsername().equals("admin")&&userVo.getPassword().equals("123")){
//私有要求claim
Map<String,Object> json=new HashMap<String,Object>();
json.put("username", userVo.getUsername());
// 生成JWT,并设置到response响应头中
String jwt=JwtUtils.createJwt(json, JwtUtils.JWT_WEB_TTL);
response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
return new JsonResponseBody<>("用户登陆成功!",true,0,null);
}else{
return new JsonResponseBody<>("用户名或密码错误!",false,0,null);
}
}
@RequestMapping("/userRegister")
@ResponseBody
public JsonResponseBody<?> userRegistered(UserVo userVo, HttpServletRequest request){
int insertSelective = userService.insertSelective(userVo);
if(insertSelective>0){
return new JsonResponseBody<>("用户注册成功!",true,0,null);
}else{
return new JsonResponseBody<>("注册失败,请稍后!",false,0,null);
}
}
@RequestMapping("/queryUserPager")
@ResponseBody
public JsonResponseBody<List<Map<String,Object>>>
queryUserPager(UserVo userVo, HttpServletRequest request){
try {
PageBean pageBean=new PageBean();
pageBean.setRequest(request);
List<Map<String, Object>> users = userService.queryUserPager(userVo, pageBean);
return new JsonResponseBody<>("OK",true,pageBean.getTotal(),users);
} catch (Exception e) {
e.printStackTrace();
return new JsonResponseBody<>("分页查询用户信息失败!",false,0,null);
}
}
}
2. 关闭JwtFilter中的OFF开关,开启jwt验证
package com.zking.ssm.jwt;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import io.jsonwebtoken.Claims;
/**
* * JWT验证过滤器,配置顺序 :CorsFilter-->JwtFilter-->struts2中央控制器
*
* @author Administrator
*
*/
public class JwtFilter implements Filter {
// 排除的URL,一般为登陆的URL(请改成自己登陆的URL)
private static String EXCLUDE = "^/user/userLogin?.*$";
private static Pattern PATTERN = Pattern.compile(EXCLUDE);
private boolean OFF = false;// true关闭jwt令牌验证功能
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//获取当前请求路径。只有登录的请求路径不进行校验之外,其他的URL请求路径必须进行JWT令牌校验
//http://localhost:8080/ssh2/bookAction_queryBookPager.action
//req.getServletPath()==/bookAction_queryBookPager.action
String path = req.getServletPath();
if (OFF || isExcludeUrl(path)) {// 登陆直接放行
chain.doFilter(request, response);
return;
}
// 从客户端请求头中获得令牌并验证
//token=头.载荷.签名
String jwt = req.getHeader(JwtUtils.JWT_HEADER_KEY);
Claims claims = this.validateJwtToken(jwt);
//在这里请各位大哥大姐从JWT令牌中提取payload中的声明部分
//从声明部分中获取私有声明
//获取私有声明中的User对象 -> Modules
Boolean flag=false;
if (null == claims) {
// resp.setCharacterEncoding("UTF-8");
resp.sendError(403, "JWT令牌已过期或已失效");
return;
} else {
//1.获取已经解析后的payload(私有声明)
//2.从私有声明中当前用户所对应的权限集合List<String>或者List<Module>
//3.循环权限(Module[id,url])
// OK,放行请求 chain.doFilter(request, response);
// NO,发送错误信息的JSON
// ObjectMapper mapper=new ObjectMapper()
// mapper.writeValue(response.getOutputStream(),json)
String newJwt = JwtUtils.copyJwt(jwt, JwtUtils.JWT_WEB_TTL);
resp.setHeader(JwtUtils.JWT_HEADER_KEY, newJwt);
chain.doFilter(request, response);
}
}
/**
* 验证jwt令牌,验证通过返回声明(包括公有和私有),返回null则表示验证失败
*/
private Claims validateJwtToken(String jwt) {
Claims claims = null;
try {
if (null != jwt) {
//该解析方法会验证:1)是否过期 2)签名是否成功
claims = JwtUtils.parseJwt(jwt);
}
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 是否为排除的URL
*
* @param path
* @return
*/
private boolean isExcludeUrl(String path) {
Matcher matcher = PATTERN.matcher(path);
return matcher.matches();
}
// public static void main(String[] args) {
// String path = "/sys/userAction_doLogin.action?username=zs&password=123";
// Matcher matcher = PATTERN.matcher(path);
// boolean b = matcher.matches();
// System.out.println(b);
// }
}
3. web.xml中要配置corsfilter,允许jwt使用请求头及响应头
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Archetype Created Web Application</display-name>
<!--1.实现Spring与Web集成,实现Spring上下文的初始化工作-->
<!-- spring上下文配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<!-- 读取Spring上下文的监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--2.配置中文乱码过滤器,使用Spring自带的过滤器-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--CrosFilter跨域过滤器-->
<filter>
<filter-name>corsFilter</filter-name>
<filter-class>com.zking.ssm.util.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>corsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--JwtFilter-->
<filter>
<filter-name>jwtFilter</filter-name>
<filter-class>com.zking.ssm.jwt.JwtFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>jwtFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--Spring MVC核心控制器-->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--此参数可以不配置,默认值为:/WEB-INF/springmvc-servlet.xml-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!--web.xml 3.0的新特性,是否支持异步-->
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
前端代码:
state.js
export default{
wxName:'派大星',
jwt:''
}
mutations.js
export default{
// state指的是state.js文件中导出的对象
// payload就是vue文件传递过来的参数
setWxName:(state,payload)=>{
state.wxName = payload.wxName
},
setJwt:(state,payload)=>{
state.jwt = payload.jwt
}
}
getters.js
export default{
getWxName:(state)=>{
return state.wxName;
},
getJwt:(state)=>{
return state.jwt;
}
}
http.js
/**
* vue项目对axios的全局配置
*/
import axios from 'axios'
import qs from 'qs'
//引入action模块,并添加至axios的类属性urls上
import action from '@/api/action'
axios.urls = action
// axios默认配置
axios.defaults.timeout = 10000; // 超时时间
// axios.defaults.baseURL = 'http://localhost:8080/j2ee15'; // 默认地址
axios.defaults.baseURL = action.SERVER;
//整理数据
// 只适用于 POST,PUT,PATCH,transformRequest` 允许在向服务器发送前,修改请求数据
axios.defaults.transformRequest = function(data) {
data = qs.stringify(data);
return data;
};
// 请求拦截器
axios.interceptors.request.use(function(config) {
let jwt = window.vm.$store.getters.getJwt;
if(jwt){
config.headers['jwt']=jwt;
}
return config;
}, function(error) {
return Promise.reject(error);
});
// 响应拦截器
axios.interceptors.response.use(function(response) {
let jwt = response.headers['jwt'];
if(jwt){
// 要将响应头中的jwt串放入到state.js中
window.vm.$store.commit('setJwt',{
jwt:jwt
});
}
return response;
}, function(error) {
return Promise.reject(error);
});
export default axios;
main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
//开发环境下才会引入mockjs
// process.env.MOCK && require('@/mock')
// 新添加1
import ElementUI from 'element-ui'
// 新添加2,避免后期打包样式不同,要放在import App from './App';之前
import 'element-ui/lib/theme-chalk/index.css'
import App from './App'
import router from './router'
import store from './store'
// 新添加3
Vue.use(ElementUI)
Vue.config.productionTip = false
import axios from '@/api/http'
import VueAxios from 'vue-axios'
Vue.use(VueAxios,axios)
/* eslint-disable no-new */
window.vm = new Vue({
el: '#app',
router,
store,
data(){
return {
Bus:new Vue()
}
},
components: { App },
template: '<App/>'
})