项目名称:私人在线文档在线预览系统
项目地址:https://github.com/MrLi2018/pdsys
模块名称:用户注册与登录
技术栈:vue+spring boot +mysql
数据模型:根据上篇文章中提到的数据结构进行建表,使用mybatis的generator工具逆向生成实体类
成果截图:
后端核心技术点:
基于jwt的token验证以及干扰图片的验证码的验证登录。
核心思路:
1. 前端用户在注册完成进行登录时,初始化验证码,点击验证码可进行切换。
2.验证码由验证码生成工具类来处理。并且以用户ip地址作为key放入redis中,并设置key的过期时间。
3. 自定义token验证注解来标明此次请求是否需要经过token验证。
4. 后端自定义请求拦截器,拦截需要token验证的请求来验证token。
5.用户在登录页面进行登录时,在后端经过密码比对以及验证码验证通过后生成token返回给前端
6. token由token工具类来生成。
7. redis操作由StringRedisTemplate来完成操作。
8.前端拿到token后将token存放在localstore中,并且设置前端请求拦截器以及响应拦截器。
9. 前端请求拦截器负责为所有的请求携带上token。
10. 请求发送至后端时由后端拦截器来处理。
具体实现如下:
token工具类代码:
package com.demo.pdsys.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.time.DateUtils;
import java.util.Date;
import java.util.HashMap;
/**
* @author MrLi2019
* @date 20200407
* token的相关工具方法
* <p>
* JWT 由3部分组成: header(Map集合),playload(负载,也可以把它看做请求体body,也是一个map集合),signature(签名,有header和playload加密后再跟secrect加密生成)
* header:有2个值,一个是类型,一个是算法,类型就是JWT,不会变,算法有2种选择,HMAC256和RS256,基本选择HMAC256
* playload:类似于post请求的请求体,是一个map集合,可以存很多很多值,如存用户的信息
* signature:由header(Base64加密后)和playload(Base64加密后)再加上secrect(秘钥生成)
* Base64加密是可逆的,所以存在header和playload的数据不能是敏感数据
* * playload有一些值定义:
* <p>
* iss: jwt签发者
* <p>
* sub: jwt所面向的用户
* <p>
* aud: 接收jwt的一方
* <p>
* exp: jwt的过期时间,这个过期时间必须要大于签发时间
* <p>
* nbf: 定义在什么时间之前,该jwt都是不可用的.
* <p>
* iat: jwt的签发时间
* <p>
* jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
*/
public class TokenUtils {
/**
* 过期时间
* 单位:s
*/
private static final int EXPIRY_TIME = 5;
private static final String SECRET_KEY = "xxxxxxxxxxxxxxxxxxxxxxx";
/**
* @param userid 用户id
* @return 生成的令牌 token
*/
public static String buildToken(String userid) {
Date date = new Date();
//过期时间
Date expiryDate = DateUtils.addSeconds(date, EXPIRY_TIME);
//填充头部
HashMap<String, Object> headMap = new HashMap<>();
headMap.put("type", "JWT");
headMap.put("alg", "HS256");
//填充载荷
JWTCreator.Builder builder = JWT.create();
builder.withHeader(headMap);
builder.withClaim("userid", userid);
builder.withIssuedAt(date);
builder.withExpiresAt(expiryDate);
return builder.sign(Algorithm.HMAC256(SECRET_KEY.getBytes()));
}
/**
* @param token 生成后的token
* @return 解码的jwt对象信息
*/
public static DecodedJWT decodeToken(String token) {
DecodedJWT decodedJwt = null;
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY.getBytes())).build();
decodedJwt = jwtVerifier.verify(token);
return decodedJwt;
} catch (JWTVerificationException e) {
System.out.println("token已过期");
return null;
}
}
public static String getUserId(DecodedJWT decodedJWT) {
String payload = decodedJWT.getPayload();
JSONArray objects = JSON.parseArray(payload);
return (String) objects.get(0);
}
}
验证码工具类:
package com.demo.pdsys.utils;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* @author MrLi2019
* 验证码工具类
*/
public class CaptchaUtils {
public static String drawCaptcha(int width, int height, BufferedImage bufferedImage,int lines) {
Graphics2D graphics2D = (Graphics2D) bufferedImage.getGraphics();
//画笔颜色
graphics2D.setColor(Color.WHITE);
graphics2D.fillRect(0, 0, width, height);
graphics2D.setFont(new Font("微软雅黑", Font.BOLD, 40));
String baseEums = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
StringBuilder stringBuilder = new StringBuilder();
int x = 10;
String ch = "";
Random random = new Random();
for (int i = 0; i < 4; i++) {
graphics2D.setColor(getRandomColor());
//设置字体旋转角度
int degree = random.nextInt() % 30; //角度小于30度
int dot = random.nextInt(baseEums.length());
ch = baseEums.charAt(dot) + "";
stringBuilder.append(ch);
//正向旋转
graphics2D.rotate(degree * Math.PI / 180, x, 45);
graphics2D.drawString(ch, x, 45);
//反向旋转
graphics2D.rotate(-degree * Math.PI / 180, x, 45);
x += 48;
}
//画干扰线
for (int i = 0; i < 6+lines; i++) {
// 设置随机颜色
graphics2D.setColor(getRandomColor());
// 随机画线
graphics2D.drawLine(random.nextInt(width), random.nextInt(height),
random.nextInt(width), random.nextInt(height));
}
//添加噪点
for (int i = 0; i < 30; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
graphics2D.setColor(getRandomColor());
graphics2D.fillRect(x1, y1, 2, 2);
}
return stringBuilder.toString();
}
/**
* 随机取色
*/
private static Color getRandomColor() {
Random ran = new Random();
return new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));
}
}
自定义验证注解,默认不需要验证。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthByToken {
boolean value() default false;
}
验证拦截器:
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisClient redisClient;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
return true;
}
AuthByToken annotation = handler.getClass().getAnnotation(AuthByToken.class);
if (null != annotation) {
//验证token
String keyinfo = request.getHeader("authorization");
if (null != keyinfo && !"".equals(keyinfo)) {
DecodedJWT decodedJWT = TokenUtils.decodeToken(keyinfo);
if (null != decodedJWT) {
String userid = TokenUtils.getUserId(decodedJWT);
String value = redisClient.getByKey(userid + "_token");
if (keyinfo.equals(value)) {
ResponseMessage responseMessage = new ResponseMessage(true);
responseMessage.setCode(200);
return true;
}
}
}
}
ResponseMessage responseMessage = new ResponseMessage(false);
responseMessage.setCode(402);
responseMessage.setMessage("token认证失败!");
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(responseMessage));
return false;
}
@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 {
}
}
跨域配置类:
@Configuration
public class CorsConfig extends WebMvcConfigurationSupport {
@Autowired
private LoginInterceptor loginInterceptor;
/* @Override
protected void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedHeaders("*").allowedMethods("*");
}*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
/*super.addInterceptors(registry);*/
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("*/login");
}
@Bean
public CorsConfiguration cors() {
return new CorsConfiguration();
}
private CorsConfiguration corsConfiguration() {
cors().addAllowedMethod("*");
cors().addAllowedOrigin("*");
cors().addAllowedHeader("*");
cors().setAllowCredentials(true);
return cors();
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration());
return new CorsFilter(source);
}
}
前端核心技术点:
引入vuex来对localstore来保存token以及登录的用户信息。用axios来进行跨域请求的访问与处理。对axios来进行封装。页面
采用element-ui来进行处理
localstore的存储:
import Vue from 'vue'
import Vuex from 'vuex'
import common from './modules/common'
import getters from './getters'
//vue store的持久化组件
import VuexPersistence from 'vuex-persist'
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
user: {
username: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).username
},
token: "",
isLogin: false
},
getters,
mutations: {
login(state, user) {
state.user = user;
window.localStorage.setItem('user', JSON.stringify(user));
},
setToken(state, token) {
state.token = token;
},
setIsLogin(state, flag) {
state.isLogin = true;
}
},
modules: {
common,
}
});
const vuexLocal = new VuexPersistence({
//创建本地持久化对象
storage: window.localStorage,
reducer: state => {
state.token
}
});
export default store
axios的封装:
import axios from 'axios'
import store from '../store'
import qs from 'qs'
import router from '../router'
import {Message, Loading, MessageBox, Alert} from "element-ui";
import Global from "../components/common/Global";
let send = axios.create({
baseURL: Global.baseurl,
//10秒
timeout: 10000,
//设置请求头
/* headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
'Content-Type': 'application/json',
},*/
//允许携带cookie信息保证session的一致性
//withCredentials: true
//qxios默认传json数据在payload里,后台只能通过requestbody注解解析,此处用qs模块来进行数据转化使后台更好解析
/*transformRequest: [function (data, header,) {
data = data || {};
data = qs.stringify(data);
//this.headers = header;
return data;
}]*/
});
//request请求拦截器,将每一个请求都携带token,除登录请求外
send.interceptors.request.use(config => {
config.data = config.data ? config.data : {};
config.headers["authorization"] = store.state.token;
if (config.method === "get") {
Message.info("测试中")
} else {
Message.info("测试中")
}
if (router.currentRoute && router.currentRoute.meta.requireAuth) {
if (localStorage.getItem("user")) {
return Promise.reject("请登录后操作")
}
return config
}
return config
});
// respone拦截器
send.interceptors.response.use(
response => {
let data = response.data;
let msg = data.message || '';
if (response.status === 200) {
if (response.data.Result) {
if (response.data.Result.Code && response.data.Result.Code !== 0) {
Message.error("测试中");
return Promise.resolve(response.data)
} else {
return Promise.resolve(response.data)
}
} else {
return Promise.resolve(response.data)
}
} else {
Message.error("请求失败,,,");
return Promise.reject(response)
}
},
err => {
console.log(err);
if (err.response.data.state) {
Message.error("kkkk");
if (err.response.status === 401 || (err.response.data.Error && err.response.data.Error.Code)) {
console.log(router);
store.commit("setToken", '');
router.push({
path: "/login",
query: {
redirect: router.currentRoute.fullPath
}
});
}
} else {
Message.error("卡卡卡卡")
}
return Promise.reject(err)
});
export default send
本模块开发中遇到的问题:
1,在后端进行跨域配置时,如果已经配置了拦截器则跨域注册器的路径映射不管用,需要用过滤器来进行处理corsfilter
2.在使用mysql8.0以上的驱动时,数据库路径地址后面必须加上时区信息。
3 前端中使用axios进行跨域请求访问时,默认发送的是Json对象在payload中,后端只能通过@requestbody注解来映射实体或者用第三方的json处理工具来进行处理。