友情提示:本篇博客需要具备spring、springmvc、mybatis、redis、Vue、Vuex、axios、jwt技术功底方能看懂,相关技术可以翻看之前所写博客;
Jwt比较难搞,不知道的可以看一下之前写过关于JWT的博客:Jwt
实现思路及细节
思路:
登录界面向后台请求验证码,后台就先调用随机函数生成验证码,并且根据验证码生成一张图片,以 base64 字符串的形式传到前台,这时我们还要生成verificationJwt令牌做为请求验证码客户端的区分。我们先将验证码信息存入redis。key是 verificationJwt令牌的值,value就是验证码了。并且将令牌放入到响应头。传给客户端。当客户端提交的时候将保持的verificationJwt令牌放入请求头带过来。后端根据前端传过来的 jwt令牌去redis中获取数据,将验证码拿到后和现有的验证码进行比较。看看是否相等
细节:
访问一次登录页面,生成一个verificationJwt令牌,这个令牌的有效时间是5min,验证码在redis中的有效时间是1min,无论是verificationJwt令牌超时失效,还是验证码生成后超时失效,都会造成登录失败;
简单来说就是跳转到登陆界面,需要在5min中之内完成登录,新的验证码出来后,需要在1min中之内完成登录
首先我们要导入pom依赖:
io.jsonwebtoken
jjwt
0.9.1
com.auth0
java-jwt
3.4.0
User.Java
package com.thf.model;
public class User {
private String uname;
private String pwd;
public User(String uname, String pwd) {
this.uname = uname;
this.pwd = pwd;
}
public User() {
super();
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
UserMapper.java
package com.thf.mapper;
import com.thf.model.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper {
int deleteByPrimaryKey(String uname);
int insert(User record);
int insertSelective(User record);
User selectByPrimaryKey(String uname);
int updateByPrimaryKeySelective(User record);
int updateByPrimaryKey(User record);
User login(User user);
}
UserMapper.xml
package com.thf.service;
import com.thf.model.User;
public interface UserService {
public User login(User user);
}
UserServiceImpl
package com.thf.service.impl;
import com.thf.mapper.UserMapper;
import com.thf.model.User;
import com.thf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author tanhaifang
* @site www.thf.com
* @company
* @create 2019-10-30 19:04
*/
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public User login(User user) {
return userMapper.login(user);
}
}
UserController
请求验证码图片和用户登录都是在这里了
package com.thf.controller;
import com.thf.model.User;
import com.thf.service.UserService;
import com.thf.util.ImageUtil;
import com.thf.util.JSONResult;
import com.thf.util.JwtUtils;
import com.thf.util.VerifyCodeUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
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.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Controller
@RequestMapping("/vue/user")
public class UserController {
private static final String VERIFICATION_CODE = "verificationCode_";
@Autowired
private UserService userService;
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/login")
@ResponseBody
public JSONResult login(User u, HttpServletRequest request, HttpServletResponse response){
//获取用户输入的验证码
String userVerificationCode = request.getParameter("verificationCode");
//获取验证码jwt令牌
String userJwt = request.getHeader(JwtUtils.JWT_VERIFICATION_KEY);
//获取到保存在redis中的验证码
Object redisVerificationCode = redisTemplate.opsForValue().get(VERIFICATION_CODE + userJwt) ;
// 这里存在两种情况:1、令牌超时 2、验证码超时
if(StringUtils.isEmpty(redisVerificationCode)){
return JSONResult.errorMsg("你的验证码已超时");
}
if(!redisVerificationCode.toString().equalsIgnoreCase(userVerificationCode)){
return JSONResult.errorMsg("验证码错误");
}
User user = userService.login(u);
//判断是否登录成功
if(user != null){
Map map=new HashMap();
map.put("User", user);
//这是颁发用户登录成功的jwt令牌
String jwt= JwtUtils.createJwt(map, JwtUtils.JWT_WEB_TTL);
response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
return JSONResult.ok(user);
}else {
return JSONResult.errorMsg("密码或账户错误");
}
}
/**生成图片验证码*/
@RequestMapping("/verificationCode")
@ResponseBody
public String verificationCode(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//生成验证码随机数
String word = VerifyCodeUtil.produceNumAndChar(4);
// 获取用户的jwt令牌
String userVerificationJwt = req.getHeader(JwtUtils.JWT_VERIFICATION_KEY);
//验证码令牌
Claims claims = JwtUtils.validateJwtToken(userVerificationJwt);
if(claims == null){
//如果用户令牌过期那么对应存放在redis中的数据也要清空
if(!StringUtils.isEmpty(userVerificationJwt)){
redisTemplate.expire(VERIFICATION_CODE + userVerificationJwt, 1, TimeUnit.DAYS);
}
userVerificationJwt = JwtUtils.createJwt(new HashMap() ,JwtUtils.JWT_WEB_TTL);
//将jwt令牌放入 response head中
resp.setHeader(JwtUtils.JWT_VERIFICATION_KEY, userVerificationJwt);
}
//刷新缓存,更新验证码
redisTemplate.opsForValue().set(VERIFICATION_CODE + userVerificationJwt , word,60, TimeUnit.SECONDS);
//生成图片
String code = "data:image/png;base64," + ImageUtil.createImageWithVerifyCode(word, 116,40);;
return code;
}
}
Web.xml
Archetype Created Web Application
contextConfigLocation
classpath:applicationCatext.xml
org.springframework.web.context.ContextLoaderListener
corsFilter
com.thf.util.CorsFilter
corsFilter
/*
SpringMVC
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
WEB-INF/springmvc-servlet.xml
1
true
SpringMVC
/
VerifyCodeUtil
生成验证码随机数工具类
package com.thf.util;
import java.util.Random;
public class VerifyCodeUtil {
/**生成N位数字和字母混合的验证码
* @param num 验证码位数
* @return code 生成的验证码字符串*/
public static String produceNumAndChar(int num){
Random random = new Random();
String code = "";
String ch = "ABCDEFGHIJKLMNPQRSTUVWXYZ";
String n = "123456789";
for(int i=0;i
ImageUtil
生成 base64 格式生成的验证码图片 的工具类
package com.thf.util;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
public class ImageUtil {
/**
* 根据指定的随机数 生成验证码图片 转 base64
* @param word 要生存的验证码随机字符串
* @param width 图片宽度
* @param height 图片高度
* @return base64 格式生成的验证码图片
* @throws IOException
*/
public static String createImageWithVerifyCode(String word, int width, int height) throws IOException {
String png_base64="";
//绘制内存中的图片
BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
//得到画图对象
Graphics graphics = bufferedImage.getGraphics();
//绘制图片前指定一个颜色
graphics.setColor(getRandColor(160,200));
graphics.fillRect(0,0,width,height);
//绘制边框
graphics.setColor(Color.white);
graphics.drawRect(0, 0, width - 1, height - 1);
// 步骤四 四个随机数字
Graphics2D graphics2d = (Graphics2D) graphics;
graphics2d.setFont(new Font("宋体", Font.BOLD, 18));
Random random = new Random();
// 定义x坐标
int x = 10;
for (int i = 0; i < word.length(); i++) {
// 随机颜色
graphics2d.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
// 旋转 -30 --- 30度
int jiaodu = random.nextInt(60) - 30;
// 换算弧度
double theta = jiaodu * Math.PI / 180;
// 获得字母数字
char c = word.charAt(i);
//将c 输出到图片
graphics2d.rotate(theta, x, 20);
graphics2d.drawString(String.valueOf(c), x, 20);
graphics2d.rotate(-theta, x, 20);
x += 30;
}
// 绘制干扰线
graphics.setColor(getRandColor(160, 200));
int x1;
int x2;
int y1;
int y2;
for (int i = 0; i < 30; i++) {
x1 = random.nextInt(width);
x2 = random.nextInt(12);
y1 = random.nextInt(height);
y2 = random.nextInt(12);
graphics.drawLine(x1, y1, x1 + x2, x2 + y2);
}
graphics.dispose();// 释放资源
ByteArrayOutputStream baos = new ByteArrayOutputStream();//io流
ImageIO.write(bufferedImage, "png", baos);//写入流中
byte[] bytes = baos.toByteArray();//转换成字节
BASE64Encoder encoder = new BASE64Encoder();
png_base64 = encoder.encodeBuffer(bytes).trim();
png_base64 = png_base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n
return png_base64;
}
/**设置随机颜色*/
private static Color getRandColor(int fc, int bc) {
// 取其随机颜色
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
CorsFilter
在此拦截器中要允许客户端 处理和和发送 verificationJwt 这个头
package com.thf.util;
import java.io.IOException;
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;
/**
* 配置tomcat允许跨域访问
*
* @author Administrator
*
*/
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) servletResponse;
HttpServletRequest req = (HttpServletRequest) servletRequest;
// Access-Control-Allow-Origin就是我们需要设置的域名
// Access-Control-Allow-Headers跨域允许包含的头。
// Access-Control-Allow-Methods是允许的请求方式
resp.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
// resp.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,
// Content-Type, Accept");
// 允许客户端,发一个新的请求头jwt
//允许客户端发送一个新的请求头
resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, jwt, verificationJwt");
//允许客户端处理一个新的响应头jwt
resp.setHeader("Access-Control-Expose-Headers", "jwt");
resp.setHeader("Access-Control-Expose-Headers", "verificationJwt");
// String sss = resp.getHeader("Access-Control-Expose-Headers");
// System.out.println("sss=" + sss);
// 允许请求头Token
// httpResponse.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With,
// Content-Type, Accept, Token");
// System.out.println("Token=" + req.getHeader("Token"));
if ("OPTIONS".equals(req.getMethod())) {// axios的ajax会发两次请求,第一次提交方式为:option,直接返回即可
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
JSONResult
package com.thf.util;
public class JSONResult {
// 响应业务状态
private Integer status;
// 响应消息
private String msg;
// 响应中的数据
private Object data;
private String ok; // 不使用
public static JSONResult build(Integer status, String msg, Object data) {
return new JSONResult(status, msg, data);
}
public static JSONResult ok(Object data) {
return new JSONResult(data);
}
public static JSONResult ok() {
return new JSONResult(null);
}
public static JSONResult errorMsg(String msg) {
return new JSONResult(500, msg, null);
}
public static JSONResult errorMap(Object data) {
return new JSONResult(501, "error", data);
}
public static JSONResult errorTokenMsg(String msg) {
return new JSONResult(502, msg, null);
}
public static JSONResult errorException(String msg) {
return new JSONResult(555, msg, null);
}
public JSONResult() {
}
public JSONResult(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public JSONResult(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
public Boolean isOK() {
return this.status == 200;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getOk() {
return ok;
}
public void setOk(String ok) {
this.ok = ok;
}
}
JwtUtils
package com.thf.util;
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 = 5 * 60 * 1000;
/**
* 将jwt令牌保存到header中的key
*/
public static final String JWT_HEADER_KEY = "jwt";
public static final String JWT_VERIFICATION_KEY = "verificationJwt";
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙
private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key
// private static final SecretKey JWT_VERIFICATION_KEY;// 使用JWT密匙生成的加密key
static {
byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
// 这里我偷个懒,用户登录jwt密钥,与图形验证码jwt密钥搞成同一个
// JWT_VERIFICATION_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 claims, long ttlMillis) {
// 生成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,并重新设置签发时间(为当前时间)和失效时间
*
* @param jwt
* 被复制的jwt令牌
* @param ttlMillis
* jwt的有效时间(单位毫秒),当前时间+有效时间=过期时间
* @return
*/
public static String copyJwt(String jwt, Long ttlMillis) {
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();
}
public static Claims validateJwtToken(String jwt) {
Claims claims = null;
try {
if (null != jwt) {
claims = JwtUtils.parseJwt(jwt);
}
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
}
首先要在state.js中定义一个用于保持验证码jwt的属性
verificationJwt:null, //这是用来保存用户等登录验证码jwt身份识别的
State.js
export default {
resturantName: '哈哈哈',
jwt:'',
options: [],//存放tab页的容器
activeIndex: '',//激活的tab页路由路径
showName:'show',//tab页的标题
role:"",//用来区分是否是因为左侧菜单被点击造成的路由路径发生改变,是:pass;不是:nopass
verificationJwt:null, //这是用来保存用户等登录验证码jwt身份识别的
}
Mutations.js
setVerificationJwt: (state, payload) => {
state.verificationJwt = payload.verificationJwt;
}
http.js
在http.js中要的拦截器就要在响应的时候如果有 verificationJwt 这个响应头 就要保存。并且在下次请求的时候带上这个 verificationJwt放入请求头都 。
/**
* 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) {
//设置验证码jwt令牌
let verificationJwt = window.vm.$store.getters.getVerificationJwt;
if (verificationJwt) {
config.headers['verificationJwt'] = verificationJwt;
}
var jwt = window.vm.$store.getters.getJwt;
config.headers['jwt'] = jwt;
return config;
}, function(error) {
return Promise.reject(error);
});
// 响应拦截器
axios.interceptors.response.use(function(response) {
// debugger;
//保存验证码jwt令牌
let verificationjwt = response.headers['verificationjwt'];
if (verificationjwt) {
window.vm.$store.commit('setVerificationJwt', {
verificationJwt: verificationjwt
});
}
var jwt = response.headers['jwt'];
if (jwt) {
window.vm.$store.commit('setJwt', {
jwt: jwt
});
}
return response;
}, function(error) {
return Promise.reject(error);
});
export default axios;
action.js
在 action.js中添加 验证码图片获取的请求路径映射变量
/**
* 对后台请求的地址的封装,URL格式如下:
* 模块名_实体名_操作
*/
export default {
// 'SERVER': 'http://localhost:8080/T216_SSH', //服务器
'SERVER': 'http://localhost:8080', //服务器
// 'SYSTEM_USER_DOLOGIN': '/vue/userAction_login.action', //用户登陆
'SYSTEM_USER_DOLOGIN': '/vue/user/login', //用户登陆
'VERIFICATION': '/vue/user/verificationCode', //用户登陆
'SYSTEM_USER_DOREG': '/vue/userAction_reg.action', //用户注册
'SYSTEM_MENU_TREE': '/vue/treeNodeAction.action', //左侧树形菜单加载
'SYSTEM_ARTICLE_LIST': '/vue/articleAction_list.action', //文章列表
'SYSTEM_ARTICLE_ADD': '/vue/articleAction_add.action', //文章新增
'SYSTEM_ARTICLE_EDIT': '/vue/articleAction_edit.action', //文章修改
'SYSTEM_ARTICLE_DEL': '/vue/articleAction_del.action', //文章删除
'SYSTEM_USER_GETASYNCDATA': '/vue/userAction_getAsyncData.action', //vuex中的异步加载数据
'getFullPath': k => { //获得请求的完整地址,用于mockjs测试时使用
return this.SERVER + this[k];
}
}
Login.vue
用户登录
登 录
忘记密码
用户注册