springboot+jwt+aop+异常统一处理+token验证实现登陆功能

java实现基于JWT的token登陆认证

前言

之前基于session的登录方式,是在用户登录成功后将用户信息存入到session中,这样不利于程序的横向扩展, 如果将项目部署多份,会出现session漂移的问题,并且随着登录用户的增加,会不断的占用服务端的内存资源;而现在这种基于token的登录方式,是在登录成功 后 将用户信息存入到客户端中,不会额外占用服务端的内存资源,并且通过签名和验签可以保证数据不被篡改,
又因为登录成功后是将用户的信息存入到客户端中,所以在进行横向扩展,部署多份的时候,不会产生session漂移的问题。

在项目中的使用

导入所需依赖

  
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.9.1version>
        dependency>
        <dependency>
            <groupId>com.auth0groupId>
            <artifactId>java-jwtartifactId>
            <version>3.4.0version>
        dependency>
        <dependency>
            <groupId>org.aspectjgroupId>
            <artifactId>aspectjweaverartifactId>
            <version>1.9.4version>
        dependency>

项目后台代码

登录认证相关代码

写一个简单的登陆测试一下怎么使用token

bean

public class User {
     
    private Long userId;
    private String userName;
    private String password;
}

核心方法

package com.fh.jwt;

import com.fh.util.response.ResponseServer;
import com.fh.util.response.ServerEnum;
import io.jsonwebtoken.*;
import sun.misc.BASE64Encoder;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTokenUtils {
     

    public static  String createToken(Map<String,Object> map){
     
        //jwt如何生成字符串
        //声明头部信息
        Map<String,Object> headerMap=new HashMap<String,Object>();
        headerMap.put("alg","HS256");
        headerMap.put("typ","JWT");
        //设置负载:不要放着涉密的东西比如:银行账号密码,余额,身份证号
        Map<String,Object> payload=new HashMap<String,Object>();
        payload.putAll(map);
        Long iat=System.currentTimeMillis();
        //设置jwt的失效时间 一分钟
        Long endTime = iat+60000l;

        //签名值就是我们的安全密钥
        String token=Jwts.builder()
                .setHeader(headerMap)
                .setClaims(payload)
                .setExpiration(new Date(endTime))
                .signWith(SignatureAlgorithm.HS256,getSecretKey("secretKey"))
                .compact();
        return token;
    }

    public static ResponseServer resolverToken(String token ){
     
        Claims claims=null;
        try {
     
            claims = Jwts.parser()
                    .setSigningKey(getSecretKey("secretKey"))
                    .parseClaimsJws(token)
                    .getBody();

        }catch (ExpiredJwtException exp){
     
            System.out.println("token超时,token失效了");
            return ResponseServer.error(ServerEnum.TOKEN_TIMEOUT);
        }catch (SignatureException sing){
     
            System.out.println("token解析失败");
            return ResponseServer.error(ServerEnum.SAFETY_ERROR);
        }
        return ResponseServer.success(claims);
    }
    private  static String getSecretKey(String key){
     
        return new BASE64Encoder().encode(key.getBytes());
    }

}

登录认证相关代码

控制层

package com.fh.controller;

import com.fh.jwt.JwtTokenUtils;
import com.fh.model.User;
import com.fh.service.UserService;
import com.fh.tokenauth.TokenCheckAnnotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@CrossOrigin
@RestController
@RequestMapping("user")
public class UserController {
     

    @Autowired
    public UserService service;

    @RequestMapping("login")
    public Map<String,Object> login(String userName,String password){
     
        HashMap<String, Object> map = new HashMap<>();
         User u =  service.queryUser(userName);
        if (u == null) {
     
            map.put("code",3000);
            return map;
        }

        // 生成token
        Map<String,Object> m = new HashMap<>();
        m.put("id",u.getUserId());
        String token = JwtTokenUtils.createToken(m);
        map.put("token",token);
        return map;
    }

	//@TokenCheckAnnotation 是自定义的注解,在需要进行登录认证的所有方法加上注解
    //客户端访问本方法时会拦截 
    @TokenCheckAnnotation
    @RequestMapping("test")
    public void test(){
     
        System.out.println("我被调用了");
    }
}

自定义注解

package com.fh.tokenauth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD) // 修饰范围
@Retention(RetentionPolicy.RUNTIME) // 用来描述注解的声明周期
public @interface TokenCheckAnnotation {
     
}

登录验证

附一个随便找的状态码

就用到了几个,其他的都用不到

package com.fh.util.response;

public enum ServerEnum {
     

    SUCCESS(200,"操作成功"),
    DEL_DEPT_SCUCCESS(201,"删除部门成功"),
    LOGIN_ISNULL(5000,"用户名或者密码为空"),
    PHONE_ISNULL(5007,"手机号不能为空"),
    USERNAME_NOTEXIST(5001,"用户名输入有误。"),
    PASSWORD_WRONG(5002,"密码输入错误,请检查"),
    LOGIN_SUCCESS(5003,"登陆成功"),
    LOGIN_EXPIRED(5004,"登录超时,请重新登陆"),
    SECRET_ERROR(5005,"传入的token值有误,不能通过签名验证"),
    TOKEN_TIMEOUT(5006,"登录失效,请重新登录"),
    TOKEN_ISNULL(5008,"获取到的Token值为空"),
    NO_MENU_RIGHT(6000,"没有权限访问该菜单,请联系管理员"),
    NOT_DATA(7001,"没有要导出的数据"),
    HTTP_URL_ISNULL(8002,"你传递的URL路径为空了"),
    SERVER_TIMEOUT(8004,"服务连接请求超时"),
    HTTP_ERROR(8003,"接口访问失败"),
    SERVER_STOP(8005,"服务连接不上"),
    SAFETY_ERROR(9000,"接口验签失败"),
    SAFETY_BAD(9001,"接口被非法攻击"),
    SAFETY_TIMEOUT(9002,"接口访问超时"),
    SAFETY_INVALID(9003,"签名值无效"),
    SAFETY_REPLAY_ATTACK(9004,"接口被重放攻击"),
    LOGIN_PHONEORCODE_INNULL(10000,"手机号或者验证码为空了"),
    LOGIN_CODE_ERROR(10001,"手机验证码输入有误"),
    ALL_STOCK_NULL(20001,"商品的库存都不足了"),
    NO_ORDER_TO_PAY(20002,"没有要支付的订单"),
    CRATER_PAY_ERROR(20003,"生成支付二维码失败"),
    PAY_TIMEOUT(20004,"支付超时请刷新页面"),
    ERROR(500,"操作失败");

    private ServerEnum(int code ,String msg){
     
        this.code=code;
        this.msg=msg;
    }

    private Integer code;

    private String msg;

    public Integer getCode() {
     
        return code;
    }

    public String getMsg() {
     
        return msg;
    }
}

对前端返回过来的数据进行判断

package com.fh.util.response;

public class ResponseServer {
     

    private Integer code;

    private String msg;

    private Object data;


    private ResponseServer(){
     

    }
    private ResponseServer(Integer code,String msg){
     
        this.code=code;
        this.msg=msg;

    }
    private ResponseServer(Integer code,String msg,Object data){
     
        this.code=code;
        this.msg=msg;
        this.data=data;
    }

    /**
     * 返回默认的 成功状态 200
     * @return
     */
    public static  ResponseServer success(){
     
        return new ResponseServer(ServerEnum.SUCCESS.getCode(),ServerEnum.SUCCESS.getMsg());
    }

    /**
     * 返回默认的带数据 成功状态 200
     * @param data
     * @return
     */
    public static  ResponseServer success(Object data){
     
        return new ResponseServer(ServerEnum.SUCCESS.getCode(),ServerEnum.SUCCESS.getMsg(),data);
    }

    /**
     * 其他特殊类型的成功状态,
     * @param serverEnum
     * @return
     */
    public static  ResponseServer success(ServerEnum serverEnum){
     
        return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg());
    }

    /**
     * 带返回数据的其他特殊类型的成功状态
     * @param serverEnum
     * @param data
     * @return
     */
    public static  ResponseServer success(ServerEnum serverEnum,Object data){
     
        return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg(),data);
    }


    //失败
        public static ResponseServer error(Integer code,String msg){
     
            return new ResponseServer(code,msg);
        }
    /**
+     * @return
     */
    public static  ResponseServer error(){
     
        return new ResponseServer(ServerEnum.ERROR.getCode(),ServerEnum.ERROR.getMsg());
    }

    /**
     * 返回默认的带数据 失败状态 500
     * @param data
     * @return
     */
    public static  ResponseServer error(Object data){
     
        return new ResponseServer(ServerEnum.ERROR.getCode(),ServerEnum.ERROR.getMsg(),data);
    }

    /**
     * 其他特殊类型的失败状态,
     * @param serverEnum
     * @return
     */
    public static  ResponseServer error(ServerEnum serverEnum){
     
        return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg());
    }

    /**
     * 带返回数据的其他特殊类型的失败状态
     * @param serverEnum
     * @param data
     * @return
     */
    public static  ResponseServer error(ServerEnum serverEnum,Object data){
     
        return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg(),data);
    }

    public Integer getCode() {
     
        return code;
    }

    public String getMsg() {
     
        return msg;
    }

    public Object getData() {
     
        return data;
    }

    public void setCode(Integer code) {
     
        this.code = code;
    }

    public void setMsg(String msg) {
     
        this.msg = msg;
    }

    public void setData(Object data) {
     
        this.data = data;
    }
}

异常处理

验证异常

package com.fh.exception;

import com.fh.util.response.ServerEnum;

public class AuthenticateException extends RuntimeException{
     
    private Integer code;
    public AuthenticateException(ServerEnum serverEnum) {
     
        super(serverEnum.getMsg());
        this.code=serverEnum.getCode();
    }
    public Integer getCode() {
     
        return code;
    }

}	

全局异常处理

package com.fh.controller;

import com.fh.exception.AuthenticateException;
import com.fh.util.response.ResponseServer;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestControllerAdvice
public class GlobalExceptionHandler {
     

    @ExceptionHandler(AuthenticateException.class)
    public ResponseServer authenticateException(AuthenticateException e, HttpServletRequest request, HttpServletResponse response){
     
        return ResponseServer.error(e.getCode(),e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseServer exceptionHandler(Exception e,HttpServletRequest request, HttpServletResponse response){
     
            e.printStackTrace();
        return ResponseServer.error();

    }
}

登录拦截的AOP 将拦截器横切到程序中

package com.fh.tokenauth;


import com.fh.exception.AuthenticateException;
import com.fh.jwt.JwtTokenUtils;
import com.fh.util.response.ResponseServer;
import com.fh.util.response.ServerEnum;
import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;

@Aspect // 作用是把当前类标识为一个切面供容器读取
@Component // 把当前类交给spring管理
public class TokenAuthenticateAop {
     
	//切点表达式 aop横切的是 com.fh.controller 包下的 任意类 任意方法 任意参数
    @Around(value = "execution(* com.fh.controller.*.*(..)) && @annotation(tokenCheckAnnotation)")
    public Object tokenAuth(ProceedingJoinPoint joinPoint, TokenCheckAnnotation tokenCheckAnnotation){
     
        Object proceed = null;
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
        String token = request.getHeader("Authorization-token");
        // 验证token是否为空
        if (!StringUtils.isNotBlank(token)){
     
            throw new AuthenticateException(ServerEnum.TOKEN_ISNULL);
        }

        // 验证token是否失效
        ResponseServer responseServer = JwtTokenUtils.resolverToken(token);
        if (responseServer.getCode() != 200){
     
            throw new AuthenticateException(ServerEnum.LOGIN_EXPIRED);
        }

        // 执行目标方法
        try {
     
            proceed = joinPoint.proceed();
        } catch (Throwable throwable) {
     
            throwable.printStackTrace();
        }

        return proceed;
    }
}

前端

前端采用vue+Element Ui

使用安装命令安装vuex:

npm install vuex --save

新建vuex文件

在新建一个store文件夹

新建index.js

import  Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
//声明一个token常量
const store={
     
  state:{
     
    'token':""
  }
}
export default new Vuex.Store({
     
  store,
  mutations:{
     
    //给常量赋值
    set_token(state,token){
     
      state.token=token;
      localStorage.setItem("token",token);
    }
  }
  ,getters:{
     
    token : state => state.token
  }
})

让vuex生效,在main.js中

import store from './store'
new Vue({
     
  el: '#app',
  router,
  store, // 使vuex生效
  components: {
      App },
  template: ''
})

新建登录页面

新建一个Login文件夹 在里面建一个Login.vue

前端对密码进行了MD5加密,所有需要提前在main.js把MD5导入进来

import md5 from 'js-md5'; // 导入MD5
Vue.prototype.$md5 = md5;
<template>
    <div >
      <el-form ref="form" :model="form" :rules="rules" label-width="80px" class="login-box demo-ruleForm" >
        <h3 class="login-title">欢迎登录</h3>
        <el-form-item label="用户名" prop="userName"> <!-- prop:将该字段设置为需检验 -->
          <el-input v-model="form.userName"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input type="password" v-model="form.password"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit('form')">登录</el-button>
          <el-button @click="resetForm('form')">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
</template>

<script>
    export default {
     
      name: "Login",
      data(){
     
        return{
     
          form:{
     
            userName:'',
            password:'',
          },
          rules: {
     
            userName: [
              {
      required: true, message: '请输入用户名', trigger: 'blur' },
              {
      min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
            ],
            password: [
              {
      required: true, message: '请输入密码', trigger: 'blur' }
            ],
          }
        }
      },
      mounted(){
     
        //绑定事件
        window.addEventListener('keydown',this.keyDown);
      },
      methods:{
     
        keyDown(e){
     
          //如果是回车则执行登录方法
          if(e.keyCode == 13){
     
            this.onSubmit('form');
          }
        },
        onSubmit(formName){
     
          this.$refs[formName].validate((valid) => {
     
            var aa = this
            if (valid) {
     
              this.form.password = this.$md5(this.form.password)
              this.$axios.post("http://localhost:80/user/login",this.$qs.stringify(this.form)).then(res=>{
     
                if(res.data.code != 3000){
     

                  // console.log(res.data.token)
                  // 登录成功以后将token放到vuex中
                  aa.$store.commit("set_token",res.data.token);

                  aa.$router.push("/house");
                }else{
     
                  aa.$alert("用户名或密码不正确!")
                }
              }).catch(err => {
     

              })
            } else {
     
              console.log('error submit!!');
              return false;
            }
          });
        },
        resetForm(form){
     
          this.$refs[form].resetFields();
        }
      },
    }
</script>

<style scoped>
  .login-box{
     
    width: 600px;
    border: 1px solid #DCDFE6;
    margin: 180px auto;
    padding: 35px 35px 15px 35px;
    border-radius: 30px;
    box-shadow: 0 0 10px aqua;
  }
  .login-title{
     
    text-align: center;
    margin: 0 auto 30px auto;
    color: #00FFFF;
  }
</style>

新建test页面

新建house文件夹 创建一个登录成功之后跳转的页面index.vue

<template>
    <div>
      <h1>我是House</h1>
        <el-button type="primary" @click="onSubmit()">点我测试</el-button>
    </div>
</template>
<script>
    export default {
     
        name: "index",
        methods:{
     
          onSubmit(){
     
            this.axios({
     
              method:"get",
              url: "http://localhost:80/user/test",
            })
          }
        }
    }
</script>
<style scoped>
</style>

配置这两个页面的路由

在router中配置路由

import Vue from 'vue'
import Router from 'vue-router'
import House from '@/views/house/index'
import Login from '@/views/login/Login'
import store from '../store'

Vue.use(Router)

const router = new Router({
     
  routes: [
    {
     
      path: '/',
      name: 'Login',
      component: () => import('@/views/login/Login'),
    },
    {
     
      path: '/house',
      name: 'House',
      component: () => import('@/views/house/index'),
    }
  ]
})

vue在路由中验证用户是否登录

import Vue from 'vue'
import Router from 'vue-router'
import House from '@/views/house/index'
import Login from '@/views/login/Login'
import store from '../store'

Vue.use(Router)

if(localStorage.getItem("token")){
     
  store.commit("set_token",localStorage.getItem("token"));
}

const router = new Router({
     
  routes: [
    {
     
      path: '/',
      name: 'Login',
      component: () => import('@/views/login/Login'),
    },
    {
     
      path: '/house',
      name: 'House',
      component: () => import('@/views/house/index'),
      meta:{
     
        requireAuth:true
      }
    }
  ]
})

//store.state.token
router.beforeEach((to,form,next)=>{
     
  if(to.matched.some(r=>r.meta.requireAuth)){
     
    //console.log(store.state.token);
    //if(store.state.token){
     
    if(localStorage.getItem("token") != null){
     
      next();
    }else{
     
      next({
     
          //跳转到登陆页面
        path:"/"
      })
    }
  }else{
     
    next();
  }
})
export default router;

在需要进行登录认证的方法加上自定义注解(后端,上面已经写好了)

 @LoginAnnotation
@TokenCheckAnnotation
@RequestMapping("test")
 public void test(){
     
     System.out.println("我被调用了");
 }

vue自定义axios的前置拦截器Main.js

对登录的请求进行拦截,判断是否登录

通过查看token里面是否有从后端传过来的token值,如果有就说明已登录,可以继续访问

如果值为空,就是没登陆,重定向的登录页面

同时也要判断token是否已经失效,如果失效也需要重定向到登录页面

// 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'
import App from './App'
import router from './router'
import axios from 'axios'//导入axios
import VueAxios from 'vue-axios'
import ElementUI from 'element-ui'//导入elementui
import md5 from 'js-md5'; // 导入MD5
import 'element-ui/lib/theme-chalk/index.css'
import moment from 'moment'
import qs from 'qs'
import store from './store'

Vue.use(VueAxios,axios)
Vue.use(ElementUI)


// 发送axios请求之前将token设置到请求的头信息里
axios.defaults.headers.common["Authorization-token"] = store.getters.token

// 在请求拦截器中判断头信息中有没有token,防止token没有传到后台
axios.interceptors.request.use(config=>{
     
  alert(store.state.token)
  if(store.state.token){
     
    config.headers.common['Authorization-token']=store.state.token;
  }
  return config;
})

// 判断后台返回的状态码,token失效后跳转到登录页面
axios.interceptors.response.use(response => {
     
  var code = response.data.code;
  if(code == 5004 || code == 5006){
     
    router.replace({
     
      path: '/',
      query: {
     redirect: router.currentRoute.fullPath}//登录成功后跳入浏览的当前页面
    })
  }
  return response;
})


Vue.prototype.$axios=axios
Vue.prototype.$qs=qs
Vue.prototype.$md5 = md5;

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
     
  el: '#app',
  router,
  store,
  components: {
      App },
  template: ''
})

End

你可能感兴趣的:(springboot+jwt+aop+异常统一处理+token验证实现登陆功能)