【SSM项目】尚筹网(四)JWT以及基于拦截器的前后端分离登录验证

引入JWT前后端交互

JsonWebToken(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT就是一段字符串,分为三段【头部、载荷、签证】。

1 后端配置

1.1 引入依赖
        <!--   JWT     -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
1.2 封装JWTUtil工具类

封装的工具类用于根据字段生成、验证token。


package com.hikaru.crowd.util;

import com.hikaru.crowd.util.constance.JWTConstant;
import io.jsonwebtoken.*;
import org.bouncycastle.util.encoders.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;

public class JWTUtil {
    /**
     *  签发创建JWT
     *
     */
    public static String createJWT(String id, String subject, long ttlMillis) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        SecretKey secretKey = generalKey();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                .setSubject(subject) // 主题
                .setIssuer(JWTConstant.JWT_USER) // 签发者
                .setIssuedAt(now) // 签发时间
                .signWith(signatureAlgorithm, secretKey); // 签名算法及秘钥
        if(ttlMillis > 0) {
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            // 设置过期时间
            builder.setExpiration(expDate);
        }
        return builder.compact();
    }
    /**
     * 解析JWT
     * @param jwtStr
     * @return
     */
    public static Claims parseJWT(String jwtStr) {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwtStr)
                .getBody();
    }

    /**
     * JWT token验证
     * @param jwtStr
     * @return
     */
    public static CheckResult validateJwt(String jwtStr) {
        CheckResult checkResult = new CheckResult();
        Claims claims = null;
        try {
            claims = parseJWT(jwtStr);
            checkResult.setSuccess(true);
            checkResult.setClaims(claims);

        } catch (ExpiredJwtException e) {
            // token 过期
            checkResult.setErrorCode(JWTConstant.JWT_ERROR_CODE_EXPIRE);
            checkResult.setSuccess(false);
        } catch (SignatureException e) {
            // token 验证失败
            checkResult.setErrorCode(JWTConstant.JWT_ERROR_CODE_FAIL);
            checkResult.setSuccess(false);
        } catch (Exception e) {
            // 其他异常
            checkResult.setErrorCode(JWTConstant.JWT_ERROR_CODE_FAIL);
            checkResult.setSuccess(false);
        }
        return checkResult;
    }

    /**
     * 生成加密的key
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodeKey = Base64.decode(JWTConstant.JWT_SECRET);
        SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
        return key;
    }

    /**
     * 根据用户名返回Jwt token
     * @param userName
     * @return
     */
    public static String getJWTToken(String userName) {
        return createJWT(userName, userName, JWTConstant.JWT_TTL);
    }

    public static void main(String[] args) throws InterruptedException {
        String token = createJWT("1", "1", 3000);
        CheckResult checkResult = validateJwt(token);
        System.out.println(checkResult.getErrorCode()); // 0

        Thread.sleep(3 * 1000);

        checkResult = validateJwt(token);
        System.out.println(checkResult.getErrorCode()); // 4001

    }
}

1.3 封装JWT CheckResult验证返回结果集
public class CheckResult {
    private int errorCode;
    private boolean success;
    private Claims claims;

    public int getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public Claims getClaims() {
        return claims;
    }

    public void setClaims(Claims claims) {
        this.claims = claims;
    }

    public CheckResult() {

    }
}
1.4 创建JWT常量类
public class JWTConstant {
    public static final Integer JWT_ERROR_CODE_NULL = 4000;  // token异地登录
    public static final Integer JWT_ERROR_CODE_EXPIRE = 4001;    // token过期
    public static final Integer JWT_ERROR_CODE_FAIL = 4002;  //token验证失败

    public static final String JWT_SECRET = "9b91643073cdda1d93507ec66591315c";
    public static final long JWT_TTL =60 * 60 * 1000;
    public static final String JWT_USER = "tod4";
}

2 前端配置

根据最开始的流程图,前端会在提交完用户名和密码之后得到后端传来的token,然后将其保存,随后每次发送请求前都需要将token放在请求头上才能成功请求服务器。

2.1 登录完成时localStorage、vuex保存token

这里以一个vue后台管理模板为例,首先提交登录表单发送登录请求,可以看到这里是向user vue模块仓库中的名为login的action派发(dispatch)的请求。

    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true
          this.$store.dispatch('user/login', this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || '/' })
            this.loading = false
          }).catch(() => {
            this.loading = false
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }

actions的login执行异步请求成功得到token之后,一方面调用工具方法setToken将token保存到浏览器的本地存储,另一方面commitmutations将数据保存到state(vuex仓库)

import Cookies from 'js-cookie'

const TokenKey = 'crowdfunding_token'

export function getToken() {
  // return Cookies.get(TokenKey)
  return localStorage.getItem(TokenKey)
}

export function setToken(token) {
  // return Cookies.set(TokenKey, token)
  return localStorage.setItem(TokenKey, token)
}

export function removeToken() {
  // return Cookies.remove(TokenKey)
  return localStorage.removeItem(TokenKey)
}

这一部分vuex的整体代码如下:

import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
import da from "element-ui/src/locale/lang/da";

const getDefaultState = () => {
  return {
    token: getToken(),
    name: '',
    avatar: ''
  }
}

const state = getDefaultState()

const mutations = {
  RESET_STATE: (state) => {
    Object.assign(state, getDefaultState())
  },
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  }
}

const actions = {
  // user login
  async login({ commit }, userInfo) {
    const { username, password } = userInfo
    // return new Promise((resolve, reject) => {
    //   login({ loginName: username.trim(), passWord: password }).then(response => {
    //     const { token } = response.data
    //     commit('SET_TOKEN', token)
    //     console.log(token)
    //     setToken(token)
    //     resolve()
    //   }).catch(error => {
    //     reject(error)
    //   })
    // })
    let res = await login({ loginName: username.trim(), passWord: password })
    const { token } = res.data
    commit('SET_TOKEN', token)
    setToken(token)
    return res
  },

  // get user info
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo().then(response => {
        const { data } = response

        if (!data) {
          return reject('Verification failed, please Login again.')
        }

        const { name, avatar } = data

        commit('SET_NAME', name)
        commit('SET_AVATAR', avatar)
        resolve(data)

      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        removeToken() // must remove  token  first
        resetRouter()
        commit('RESET_STATE')
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({ commit }) {
    return new Promise(resolve => {
      removeToken() // must remove  token  first
      commit('RESET_STATE')
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}


2.2 每次请求时都将token放在请求头上面

要完成这一点则需要借助我们重写的axios二次封装,在请求拦截器判断一下vuex仓库中有没有token,如果有的话就将其加到请求的请求头上面。

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

3 不基于SpringSecurity的前后端分离JWT登录、token验证

大致流程是首次请求登录会跳过拦截器,经过登录验证之后会由后端向前端响应一个token,然后前端得到token后在vuex进行保存,以后每次发送请求时都需要在请求头上面添加token,后端的拦截器则会拦截非登录请求判断token是否过期或非法,然后放行请求。

3.1 登录验证

UserController

    /**
     * 登录
     *
     * @param requestBody
     * @return
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public R<Object> loginHandle(@RequestBody String requestBody) {
        JSONObject jsonObject = JSON.parseObject(requestBody);
        String loginName = (String) jsonObject.get("loginName");
        String passWord = (String) jsonObject.get("passWord");
        // 验证用户名和密码
        userService.userVerification(loginName, passWord);
        // 返回token
        Map<String, String> map = userService.getTokenByLoginName(loginName);

        return R.successWithData(map);
    }

UserServiceImpl

    /**
     * 验证用户账户及密码
     *
     * @param formLoginName, formPassWord
     * @return
     */
    @Override
    public void userVerification(String formLoginName, String formPassWord) {

        // 对密码进行md5加密
        formPassWord = CrowdUtil.md5(formPassWord);

        UserExample userExample = new UserExample();
        userExample.createCriteria().andLoginNameEqualTo(formLoginName);
        List<User> users = userMapper.selectByExample(userExample);
        // 用户名不存在
        if (users.size() <= 0) {
            throw new LoginFailedException(CrowdConstant.MASSAGE_LOGIN_FAILED);
        }

        String dbPassword = users.get(0).getPassWord();
        // 密码不正确
        if (!Objects.equals(formPassWord, dbPassword)) {
            throw new LoginFailedException(CrowdConstant.MASSAGE_LOGIN_FAILED);
        }
    }
    /**
     * 根据用户名生成token
     *
     * @param loginName
     * @return
     */
    @Override
    public Map<String, String> getTokenByLoginName(String loginName) {

        String token = JWTUtil.getJWTToken(loginName);
        Map<String, String> map = new HashMap<>();
        map.put("token", token);

        return map;
    }
3.2 基于拦截器的token验证
登录拦截器的配置
package com.hikaru.crowd.mvc.interceptor;

import com.google.gson.Gson;
import com.hikaru.crowd.util.CheckResult;
import com.hikaru.crowd.util.JWTUtil;
import com.hikaru.crowd.util.R;
import com.hikaru.crowd.util.constance.JWTConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

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

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("拦截到请求:" + request.getRequestURI());
        String token = request.getHeader("X-Token");
        R<Object> resultEntity;
        // 请求头中不含token
        if(token == null) {
            resultEntity = new R<>(JWTConstant.JWT_ERROR_CODE_EXPIRE, null, null);

            Gson gson = new Gson();
            String json = gson.toJson(resultEntity);

            response.getWriter().write(json);

            return false;
        }
        CheckResult checkResult = JWTUtil.validateJwt(token);
        // token过期或者不合法
        if(!checkResult.isSuccess()) {
            int errorCode = checkResult.getErrorCode();
            resultEntity = new R<>(errorCode, null, null);

            Gson gson = new Gson();
            String json = gson.toJson(resultEntity);

            response.getWriter().write(json);

            return false;
        }
        return true;
    }
}

登录拦截器的注册
    /**
     * 拦截器注册
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("/file/saveAvatar")
                .excludePathPatterns("/test")
                .excludePathPatterns("/user/logout")
                .excludePathPatterns("/user/login");

    }

也如标题所说这是不基于SpringSecurity的前后端分离登录验证,下面介绍的基于SpringSecurity的方式则可以让我们舍弃拦截器,大大简化我们的代码。

posted @ 2022-10-04 20:12  Tod4  阅读(233)  评论(0编辑  收藏  举报