pdsys-----day02----基于token的登录验证

项目名称:私人在线文档在线预览系统

项目地址: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处理工具来进行处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值