JWT其实是一套用来登录的经典方式了,写这篇文章之前,我也看了网上的文章,其实有很多人都写过相关教程,并且也都有与各种登录方式进行对比,基本上看个两篇三篇左右,就可以懂了。但是好像没看到有完整的前后端代码,这里写一个前后端完整实现,记录一下。
关键词:JWT的前后端方式,guava,前端http拦截器,服务端拦截器,自定义注解@interface
1、JWT介绍
jwt其实就是当用户与服务器通信时,客户在请求中下发token,服务器仅依赖于这个token对象来标识用户。 为了防止用户篡改数据,服务器将在生成对象时添加签名。 服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
2、JWT的数据结构
A - header 头信息
B - payload (有效荷载,用于记录用户非隐私数据)
C - Signature 签名
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VyTmFtZVwiOlwicXp3XCIsXCJ1c2VySWRcIjpcImZlNmE0OGQ4ODVjMDQ3MGE4YjZlZTc2M2YwM2NlNTZjXCJ9IiwiZXhwIjoxNjcwMTU5NDY1fQ.TrbRfNIr4Z1lCMRUedFsPazX1TsnrIFpRJ21Hsh1yuA
3、实现流程图
4、项目实战
4.1、先引入jwt包
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>24.0-jre</version>
</dependency>
4.2、前端使用vue开发,并且http使用Axios
对于前端来说,在登录的时候,前端需要提交用户名和密码,服务端需要生成token,并且下发。前端请求的时候,需要监听request,并且塞入token,初次之外,前端还需要监听服务端传来的返回码,并且与后端约定好特定的服务码进行token过期跳转。
//监听请求参数,只要有token,就把token放入请求头
axios.interceptors.request.use(
(config) => {
// Do something before request is sent
var accessToken = localStorage.getItem('token');
if (accessToken) {
// 添加headers
config.headers.common['token'] = accessToken // token
}
return config
},
(error) => {
// Do something with request error
return Promise.reject(error)
}
);
//监听返回参数,如果服务端返回3200,则表示token已过期,并且跳转登录页
axios.interceptors.response.use(
(response) => {
if(response.data.respCode === "3200") {
window.location.href = "/login";
}
return response.data;
}
);
前端登录接口,也可以设置过期时间,但是我这里没有去做,严格来讲,是需要的呦!!!
login() {
this.$http.post("/sys/userInfo/login", this.$qs.stringify({
。。。
})).then((rs) => {
if (rs.respCode === '1000') {
// 关闭tab页,数据消失
sessionStorage.setItem("token", rs.token);
localStorage.setItem("userId", rs.userId);
//把用户名存储在localstorege
localStorage.setItem("token", rs.token);
// 关闭浏览器,数据消失
// document.cookie = "token=" + JSON.stringify(rs.token);
// 这里需要登录跳转
}
}).catch((rs) => {
})
}
4.3、对于服务端,这里使用的是guava缓存,理论上用redis会更好,但是我这是针对与单体项目,使用guava就不需要再起一个redis服务了,而且guava有两种过期方式:写过期和访问过期。
这里需要使用的是访问过期,即规定时间内无访问,则过期。
@Component
public class GuavaCacheConfig {
// expireAfterAccess(long, TimeUnit)
// 缓存在给定的时间内没有被访问则被回收
// expireAfterWrite(long, TimeUnit)
// 缓存没有在给定的时间内被写访问(创建或者覆盖)则被回收。
private static final Cache<String, Object> CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(1800, TimeUnit.SECONDS).maximumSize(1800).build();
public Object get(String key) {
return CACHE.getIfPresent(key);
}
public void set(String key, Object value) {
CACHE.put(key, value);
}
}
对于token过期校验属于通用模块,可以使用切面或者拦截器来进行处理,这样解耦相当一部分代码,如果需要修改,也不用每个接口都修改了。
这里可以需要定义一个自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateToken {
boolean required() default true;
}
然后针对需要登录校验的接口,只需要加上注解,就会进入token校验,例如下面查询定时任务的接口
@PostMapping("/list")
@ValidateToken
public String list(@Validated(value = QuartzForm.ListQuartz.class)QuartzForm form) {
return JsonUtils.objectToJsonString(quartzService.list(form));
}
对于校验逻辑,需要先设置一个拦截器,并且配置好拦截方式,就可以实现拦截了
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenHandler())
.addPathPatterns("/**");
}
@Bean
public TokenInterceptor tokenHandler() {
return new TokenInterceptor();
}
}
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private GuavaCacheConfig guavaCacheConfig;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 从 http 请求头中取出 token
String token = request.getHeader("token");
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//检查是否有 validateToken 注释,有则跳过认证
if (method.isAnnotationPresent(ValidateToken.class)) {
ValidateToken passToken = method.getAnnotation(ValidateToken.class);
if (passToken.required()) {
if (token == null){
throw new RespException(ResponseEnum.ERR_HANDLER_TOKEN_EXPAIRE);
}
JsonObject tokenJson = JsonUtils.stringToJsonObject(token);
String userId = JsonUtils.getAsString(tokenJson, Params.USER_ID);
this.validateUserInfo(userId);
。。。
return true;
}
}
return true;
}
private void validateUserInfo(String userId) {
String token = (String) guavaCacheConfig.get(Utils.getTokenKey(userId));
if (token == null) {
throw new RespException(ResponseEnum.ERR_HANDLER_TOKEN_ERROR);
}
}
}
就这样就可以实现过期后自动跳转登录页面了