最近做了个项目,大家都知道很多的项目都是在自己手上原本的框架内进行业务开发。但是甲方爸爸的这个项目需要交付原代码,并且要求框架逻辑简单清晰,二次开发简易上手。
那不是要重新从0到1写一套框架吗?
试着先给甲方爸爸报一下老的框架吧。于是我把框架内有的技术点和实现组件罗列了出来,甲方爸爸看后立马做了表述:框架臃肿复杂,就单单一个Spring Security必须要求切割。
没办法谁给钱,谁就是老大呗。
索性开发周期时长还算可以,那就重新弄一套,就简简单单的框架逻辑,有数据库、缓存机制就可以了,把之前的什么消息队列、加密机制、OSS、推送、短信还有大数据套件全删除掉,自己还轻松不是。
以上说的都好办,但是问题来了,如何放弃Spring Security,利用Spring Boot自身的接口来实现Token校验和长登录状态以及登录异常回馈呢。总不能一个接口下进行一次校验那么麻烦吧。
索性,咱写代码和思维逻辑还挺牛掰的,一通搞下来,其实很简单。那么来看看怎么样实现吧,
1. 了解AsyncHandlerInterceptor
AsyncHandlerInterceptor 是 Spring 框架中的一个接口,它用于处理异步请求的拦截。当一个请求被异步处理时,即请求的处理被提交到一个单独的线程中执行,标准的 HandlerInterceptor 可能不会按预期工作,因为它们通常依赖于请求和响应的生命周期。AsyncHandlerInterceptor 提供了额外的回调方法来处理异步请求的完成阶段。
来看看接口的方法:
// 当异步请求开始处理时调用。在这个阶段,主线程可能会继续处理其他请求,而异步处理则在另一个线程中继续。这个方法可以用来执行一些初始化操作,例如设置异步上下文。
afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler);
// 在请求处理之前调用。这个方法是 HandlerInterceptor 接口的一部分,也被 AsyncHandlerInterceptor 继承。
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler);
// 在请求处理之后,但在视图渲染之前调用。这个方法也是 HandlerInterceptor 接口的一部分。
postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView);
// 在整个请求完成之后调用,包括视图渲染。这个方法也是 HandlerInterceptor 接口的一部分。在异步处理的情况下,这个方法将在异步任务完成后被调用。
afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
afterConcurrentHandlingStarted当异步请求开始处理时调用。主线程可能会继续处理其他请求,而异步处理则在另一个线程中继续。这个方法可以用来执行一些初始化操作,例如:设置异步上下文。
由此方法我们可以设想,在异步请求处理时,我们可以将前端请求信息进行拦截,然后进行逻辑判断不就可以了吗?
那么问题就好解决了,我们来实现
2.接口实现
我们完成上下文的设置,首先我们先一个保存请求和返回还有用户信息的类
@Data
public class ContextHolder {
private HttpServletRequest request;
private HttpServletResponse response;
/**
* 用户信息缓存
*/
private SysUser userCache;
}
然后我们写一个ThreadLocal,将ContextHolder保存在内。
public class RequestContext {
private static ThreadLocal<ContextHolder> context = new ThreadLocal<>();
public static void setContext(ContextHolder contextHolder) {
context.set(contextHolder);
}
public static HttpServletRequest getHttpRequest() throws ExchangeException {
return getContext().getRequest();
}
public static HttpServletResponse getHttpResponse() throws ExchangeException {
return getContext().getResponse();
}
public static SysUser getAccountCache() throws ExchangeException {
return getContext().getUserCache();
}
private static ContextHolder getContext() throws ExchangeException {
ContextHolder contextHolder = context.get();
if (contextHolder == null) {
throw new ExchangeException(ResultStatusEnum.CONTEXT_ERROR);
}
return contextHolder;
}
public static void remove() {
context.remove();
}
}
最后,我们将HandlerInterceptor的方法重写并实现。
@Component
public class ApiHandlerInterceptor implements AsyncHandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 一些不需要拦截的资源
// spring静态资源处理不拦截
if (handler instanceof ResourceHttpRequestHandler) {
return true;
}
// 特殊的链接放行,如swagger
String url = request.getRequestURI();
if (url.contains(EXCLUDE_URL)) {
return true;
}
// 排除标记过的接口
// 权限配置:目录3说明
//保存请求上下文
ContextHolder holder = new ContextHolder();
holder.setRequest(request);
holder.setResponse(response);
RequestContext.setContext(holder);
// TOKEN校验 目录4说明
}
// 销毁
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
RequestContext.remove();
}
}
3. 权限接口实现
以上我们排除了一个静态资源和特殊的链接,那么我们怎么样设置权限呢,如:登录注册和特定的无Token亦可访问的接口和需要Token访问的接口怎么配置?
先看例子,我们再来将处理逻辑。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
boolean required() default true;
}
我们先一个注解@Login。
// 排除标记过的接口
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
Login login = method.getMethodAnnotation(Login.class);
if (null != ogin) {
return true;
}
}
在HandlerInterceptor方法实现内有handler参数,如果handler是HandlerMethod的实现,那么我们通过反射获取到方法上标记的注解即可标明此方法不进入TOKEN校验
接口实现如下:
@Login
@RequestMapping(path = "/register", method = RequestMethod.POST)
public ResponseData<Integer> register(@RequestBody @Valid RegistersCnd cnd) throws Exception {
return ResponseData.success(userService.register(cnd));
}
4. TOKEN校验
Token的检验,前端一般会放在请求头那么就是在HttpServletRequest内获取,来看实现:
String token = request.getHeader(UserConstruct.ACCESS_TOKEN);
// 判断header里面是否有token信息
if (StrUtil.isNotEmpty(token)) {
// 从redis中获取用户的token信息
String userCache = RedissonUtils.getBucket(UserRedisConfig.TOKEN_REDIS_PREFIX + token);
if (null == userCache || userCache.isEmpty()) {
//获取 刷新TOKEN 的信息
String refreshToken = RedissonUtils.getBucket(UserRedisConfig.TOKEN_TO_REFRESH_PREFIX + token);
// 未获取到,表示TOKEN真正过期
if (null == refreshToken || refreshToken.isEmpty()) {
throw new TokenException(ResultStatusEnum.TOKEN_EXPIRE);
}
// 未用户获取到,表示TOKEN真正过期
userCache = RedissonUtils.getBucket(UserRedisConfig.REFRESH_TOKEN_REDIS_PREFIX + refreshToken);
if (null == userCache || userCache.isEmpty()) {
throw new TokenException(ResultStatusEnum.TOKEN_EXPIRE);
} else {
//获取到了,给前端提示,需要刷新TOKEN了
throw new TokenException(ResultStatusEnum.REFRESH_TOKEN);
}
}
SysUser sysUser = JSONUtil.toObj(userCache, SysUser.class);
// 检查用户状态
checkUserStatus(sysUser);
// 存储用户信息
holder.setUserCache(sysUser);
return true;
}
以上逻辑内,包含了获取到token信息后,进行和缓存内的用户信息进行匹配检查、以及刷新token的业务逻辑,刷新Token的标记也是缓存内的获取检查。
至此,一个简单的校验权限匹配机制完成。
本人前后端全栈开发,所以约定俗称的接口调试及标记校验都是一个人完成,此种实现简化了好多好多的代码逻辑。不用臃肿复杂的Spring Security其实挺好。