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

1 后端配置
1.1 引入依赖
| |
| <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 { |
| |
| |
| |
| |
| 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(); |
| } |
| |
| |
| |
| |
| |
| public static Claims parseJWT(String jwtStr) { |
| SecretKey secretKey = generalKey(); |
| return Jwts.parser() |
| .setSigningKey(secretKey) |
| .parseClaimsJws(jwtStr) |
| .getBody(); |
| } |
| |
| |
| |
| |
| |
| |
| 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) { |
| |
| checkResult.setErrorCode(JWTConstant.JWT_ERROR_CODE_EXPIRE); |
| checkResult.setSuccess(false); |
| } catch (SignatureException e) { |
| |
| 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; |
| } |
| |
| |
| |
| |
| |
| public static SecretKey generalKey() { |
| byte[] encodeKey = Base64.decode(JWTConstant.JWT_SECRET); |
| SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES"); |
| return key; |
| } |
| |
| |
| |
| |
| |
| |
| 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()); |
| |
| Thread.sleep(3 * 1000); |
| |
| checkResult = validateJwt(token); |
| System.out.println(checkResult.getErrorCode()); |
| |
| } |
| } |
| |
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; |
| public static final Integer JWT_ERROR_CODE_EXPIRE = 4001; |
| public static final Integer JWT_ERROR_CODE_FAIL = 4002; |
| |
| 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保存到浏览器的本地存储,另一方面commit
到mutations
将数据保存到state
(vuex仓库)
| import Cookies from 'js-cookie' |
| |
| const TokenKey = 'crowdfunding_token' |
| |
| export function getToken() { |
| |
| return localStorage.getItem(TokenKey) |
| } |
| |
| export function setToken(token) { |
| |
| return localStorage.setItem(TokenKey, token) |
| } |
| |
| export function removeToken() { |
| |
| 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 = { |
| |
| async login({ commit }, userInfo) { |
| const { username, password } = userInfo |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let res = await login({ loginName: username.trim(), passWord: password }) |
| const { token } = res.data |
| commit('SET_TOKEN', token) |
| setToken(token) |
| return res |
| }, |
| |
| |
| 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) |
| }) |
| }) |
| }, |
| |
| |
| logout({ commit, state }) { |
| return new Promise((resolve, reject) => { |
| logout(state.token).then(() => { |
| removeToken() |
| resetRouter() |
| commit('RESET_STATE') |
| resolve() |
| }).catch(error => { |
| reject(error) |
| }) |
| }) |
| }, |
| |
| |
| resetToken({ commit }) { |
| return new Promise(resolve => { |
| removeToken() |
| commit('RESET_STATE') |
| resolve() |
| }) |
| } |
| } |
| |
| export default { |
| namespaced: true, |
| state, |
| mutations, |
| actions |
| } |
| |
| |
2.2 每次请求时都将token放在请求头上面
要完成这一点则需要借助我们重写的axios二次封装
,在请求拦截器判断一下vuex仓库中有没有token,如果有的话就将其加到请求的请求头上面。
| |
| const service = axios.create({ |
| baseURL: process.env.VUE_APP_BASE_API, |
| |
| timeout: 5000 |
| }) |
| |
| |
| service.interceptors.request.use( |
| config => { |
| |
| |
| if (store.getters.token) { |
| |
| |
| |
| config.headers['X-Token'] = getToken() |
| } |
| return config |
| }, |
| error => { |
| |
| console.log(error) |
| return Promise.reject(error) |
| } |
| ) |
3 不基于SpringSecurity的前后端分离JWT登录、token验证
大致流程是首次请求登录会跳过拦截器,经过登录验证之后会由后端向前端响应一个token,然后前端得到token后在vuex进行保存,以后每次发送请求时都需要在请求头上面添加token,后端的拦截器则会拦截非登录请求判断token是否过期或非法,然后放行请求。
3.1 登录验证
UserController
| |
| |
| |
| |
| |
| |
| @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); |
| |
| Map<String, String> map = userService.getTokenByLoginName(loginName); |
| |
| return R.successWithData(map); |
| } |
UserServiceImpl
| |
| |
| |
| |
| |
| |
| @Override |
| public void userVerification(String formLoginName, String formPassWord) { |
| |
| |
| 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); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| @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; |
| |
| 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); |
| |
| 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; |
| } |
| } |
| |
登录拦截器的注册
| |
| |
| |
| |
| @Override |
| public void addInterceptors(InterceptorRegistry registry) { |
| |
| registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") |
| .excludePathPatterns("/file/saveAvatar") |
| .excludePathPatterns("/test") |
| .excludePathPatterns("/user/logout") |
| .excludePathPatterns("/user/login"); |
| |
| } |
也如标题所说这是不基于SpringSecurity的前后端分离登录验证,下面介绍的基于SpringSecurity的方式则可以让我们舍弃拦截器,大大简化我们的代码。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步