作为一名只会点点点的苦逼测试,肯定都是向往自动化的,利用闲暇时间,自己写了一个简单的接口测试平台,但是既然是一个web网站,必不可少的是登录,还有登录拦截的东西,今天简单总结了下一个比较简单的方案
整体思路
首先在前端,登录页面,用户登录成功后,后端负责用uid生成对应的token,并返回给前端,前端将token存储到localStorage中,在axios中拦截request和response,拦截request,是将localStorage中的token塞到Header中的Authorization(写在这里,可以避免CROS跨域的问题)中,然后后端拦截器从Header中取出token,做校验,校验通过则放行,不通过则返回未登录的错误信息,前端拦截response。如果是未登录的错误码,则做路由跳转
准备
后端,导包:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
1、后端(Spring Boot)
① 首先添加一个JWT实体,因为是私人项目,此处使用lombok,所以不需要写get/set,团队项目不建议使用lombok
package platform.app.entity;
import lombok.Data;
@Data
public class JWTInfo {
private String uid;
public JWTInfo(String uid) {
this.uid = uid;
}
}
②添加JWTTokenUtils
package platform.app.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import platform.app.entity.JWTInfo;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
/**
* @description: JWTTokne验证
* @author: hmj
* @create: 2020-04-13 10:50
**/
public class JwtTokenUtils {
// TOKEN 7天过期
public static final int EXPIRE = 7;
public static final int FLAG = "uid";
private static Key getKeyInstance(){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] bytes = DatatypeConverter.parseBase64Binary("autoTest-user");
return new SecretKeySpec(bytes,signatureAlgorithm.getJcaName());
}
/**
* 生成token的方法
*/
public static String generatorToken(JWTInfo jwtInfo){
return Jwts.builder().claim(FLAG ,jwtInfo.getUid())
.setExpiration(DateTime.now().plusDays(EXPIRE).toDate())
.signWith(SignatureAlgorithm.HS256,getKeyInstance()).compact();
}
/**
* 根据token获取token中的信息
*/
public static JWTInfo getTokenInfo(String token){
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return new JWTInfo(claims.get(FLAG).toString());
}
}
③ 添加拦截器,如果拦截到未登录或者其他异常情况,返回错误信息,如果已登录则放行
package platform.app.config;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import platform.app.entity.JWTInfo;
import platform.app.entity.ResponseBean;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import platform.app.utils.JwtTokenUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class LoginHandlerInterceptor implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(LoginHandlerInterceptor.class);
@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 {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行Vue传过来一个OPTIONS请求
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
// logger.info("OPTIONS请求,放行");
return true;
}
String token = request.getHeader("Authorization");
// 不等于空
if (StringUtils.isNotBlank(token)){
try {
JWTInfo jwtInfo = JwtTokenUtils.getTokenInfo(token);
logger.info("id:{},正在登录",jwtInfo.getUid());
}catch (SignatureException e){
// 未登录
ResponseBean responseBean = new ResponseBean();
responseBean.setCode(40009);
responseBean.setMsg("未登录");
returnJson(response,JSON.toJSONString(responseBean));
}catch (ExpiredJwtException e){
ResponseBean responseBean = new ResponseBean();
responseBean.setCode(40009);
responseBean.setMsg("登录信息已过期,请重新登录!");
returnJson(response,JSON.toJSONString(responseBean));
}
return true;
}
ResponseBean responseBean = new ResponseBean();
responseBean.setCode(40004);
responseBean.setMsg("未登录");
returnJson(response,JSON.toJSONString(responseBean));
return false;
}
/*
* 出现错误或者未登录,响应错误信息
**/
private void returnJson(HttpServletResponse response, String json) throws Exception {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
logger.error("response error", e);
} finally {
if (writer != null)
writer.close();
}
}
}
④ 配置拦截器,将之前写好的拦截器。配置在addInterceptors方法中,并添加拦截路径和排除的拦截路径
package platform.app.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/autoTest/platform/*")
.excludePathPatterns("/autoTest/platform/login");
}
⑤ login控制层,登录成功,将token返回,可以在响应头,响应体,或者其他方式返回,这里直接在响应体中返回了
@RequestMapping("/login")
public ResponseBean login(@RequestBody User user){
logger.info("登录信息:username={} , password={}",user.getUsername(),user.getPassword());
User resultUser = userService.getUserLogin(user);
ResponseBean responseBean = new ResponseBean();
if ( resultUser != null && CommonUtils.verifyPassword(user.getPassword(),resultUser.getPassword())){
logger.info("登录成功,登录信息:" + resultUser.toString());
String token = JwtTokenUtils.generatorToken(new JWTInfo(resultUser.getId()+""));
responseBean.setMsg("登录成功");
responseBean.setCode(10000);
responseBean.setExtend(token);
return responseBean;
}
logger.info("登录失败");
responseBean.setMsg("登陆失败,请检查用户名和密码");
responseBean.setCode(9999);
return responseBean;
}
2、前端(Vue)
① 登录页,将返回的token存储到localStorage中
login() {
this.$refs.loginFormRef.validate(async valid => {
if (valid) {
const result = await this.$http.post("/login", this.form);
// console.log(result);
const data = result.data;
if (data.code == 10000) {
this.$message.success(data.msg);
// console.log("token : " + data.data.id);
window.localStorage.setItem("token", data.extend);
window.localStorage.setItem("customerName", data.data.customerName);
this.$router.push("/home");
} else {
this.$message.error(data.msg);
}
} else {
}
});
}
② main.js中,请求、响应拦截
axios.interceptors.request.use(request => {
/*判断token存在 登录拦截*/
if(window.localStorage.getItem("token")){
/*设置统一的header*/
request.headers.Authorization = window.localStorage.getItem("token");
}
return request;
});
// 响应拦截
axios.interceptors.response.use((response) =>{
if(response.data.code !== 10000){
if(response.data.code === 40009){
// 跳转到登录页面
vm.$router.push('/login');
}
}
return response;
})
总结:
由于时间紧迫,不想在登录上面浪费太多时间,很多功能化繁为简,只写了个临时版本,等其他功能弄的差不多,再对登录拦截进行优化
坑:如果你的项目中已经导入了spring-security-crypto,那么在导入io.jsonwebtoken时,就要注意了,这两个会有冲突,导致Spring找不到默认的jar,根据实际情况选择性排除即可
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.2.1.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>jjwt</artifactId>
<groupId>io.jsonwebtoken</groupId>
</exclusion>
</exclusions>
</dependency>