console.log('点个关注再走吧🍺');|

Tod4

园龄:2年11个月粉丝:21关注:0

【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 @   Tod4  阅读(258)  评论(0编辑  收藏  举报
   
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起