|
@@ -13,24 +13,42 @@ import javax.servlet.ServletResponse;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
import java.io.IOException;
|
|
import java.io.IOException;
|
|
|
-import java.util.Arrays;
|
|
|
|
|
-import java.util.HashSet;
|
|
|
|
|
-import java.util.Set;
|
|
|
|
|
|
|
+import java.net.URLDecoder;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
|
|
|
@Component
|
|
@Component
|
|
|
public class TokenAuthInterceptor extends AbstractFilter {
|
|
public class TokenAuthInterceptor extends AbstractFilter {
|
|
|
|
|
|
|
|
- private static final Set<String> NAMES = new HashSet<>(
|
|
|
|
|
- Arrays.asList(
|
|
|
|
|
- "/employee/signInformation/query",
|
|
|
|
|
- "/employee/signInformation/save",
|
|
|
|
|
- "/employee/qRcode/query",
|
|
|
|
|
- "/employee/registrationOutsiders/query",
|
|
|
|
|
- "/employee/registrationOutsiders/save",
|
|
|
|
|
- "/employee/satisfaction/save",
|
|
|
|
|
- "/employee/satisfaction/getQuestionnaireByQrCodeId"
|
|
|
|
|
- )
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ /** 旧版 /short/apply 签发的令牌值(未绑定场景),保留兼容 */
|
|
|
|
|
+ private static final String LEGACY_TOKEN_VALUE = "short-acting-token";
|
|
|
|
|
+
|
|
|
|
|
+ private static final String TOKEN_KEY_PREFIX = "white_list_access_token:";
|
|
|
|
|
+
|
|
|
|
|
+ private static final String TOKEN_HEADER = "x-authorization-access_token";
|
|
|
|
|
+
|
|
|
|
|
+ /** 场景前缀:按场景签发的令牌值形如 scene:<场景名> */
|
|
|
|
|
+ private static final String SCENE_VALUE_PREFIX = "scene:";
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 受短令牌保护的接口及所属场景。
|
|
|
|
|
+ * 按场景签发的令牌只能访问同场景接口,防止一个令牌跨接口使用。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final Map<String, String> PROTECTED_URI_SCENE = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ static {
|
|
|
|
|
+ // 外来人员登记(扫码登记)
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/registrationOutsiders/save", "registration");
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/registrationOutsiders/checkUnfinished", "registration");
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/registrationOutsiders/getRegistrationOutsidersFileConfig", "registration");
|
|
|
|
|
+ // 签到
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/signInformation/query", "sign");
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/signInformation/save", "sign");
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/qRcode/query", "sign");
|
|
|
|
|
+ // 满意度调查
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/satisfaction/save", "satisfaction");
|
|
|
|
|
+ PROTECTED_URI_SCENE.put("/employee/satisfaction/getQuestionnaireByQrCodeId", "satisfaction");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
|
@@ -39,33 +57,71 @@ public class TokenAuthInterceptor extends AbstractFilter {
|
|
|
|
|
|
|
|
httpRequest.setCharacterEncoding(StringPool.UTF_8);
|
|
httpRequest.setCharacterEncoding(StringPool.UTF_8);
|
|
|
httpResponse.setCharacterEncoding(StringPool.UTF_8);
|
|
httpResponse.setCharacterEncoding(StringPool.UTF_8);
|
|
|
- String uri = httpRequest.getRequestURI();
|
|
|
|
|
- if (NAMES.contains(uri)){
|
|
|
|
|
- // 1. 获取令牌(支持Header/参数)
|
|
|
|
|
- //String token = httpRequest.getParameter("token");
|
|
|
|
|
- String token = httpRequest.getHeader("x-authorization-access_token");
|
|
|
|
|
- // 2. 令牌不存在
|
|
|
|
|
- if (BeanUtils.isEmpty(token)) {
|
|
|
|
|
- sendError(httpResponse, "访问令牌缺失");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- // 3. 验证Redis中的令牌
|
|
|
|
|
- String key = "white_list_access_token:" + token;
|
|
|
|
|
- Boolean exists = RedisUtil.redisTemplateString.hasKey(key);
|
|
|
|
|
- if (exists == null || !exists) {
|
|
|
|
|
- sendError(httpResponse, "令牌无效或已过期");
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ String uri = normalizeUri(httpRequest.getRequestURI());
|
|
|
|
|
+ String scene = PROTECTED_URI_SCENE.get(uri);
|
|
|
|
|
+ if (scene == null){
|
|
|
|
|
+ filterChain.doFilter(servletRequest, servletResponse);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 1. 获取令牌
|
|
|
|
|
+ String token = httpRequest.getHeader(TOKEN_HEADER);
|
|
|
|
|
+ // 2. 令牌不存在
|
|
|
|
|
+ if (BeanUtils.isEmpty(token)) {
|
|
|
|
|
+ sendError(httpResponse, "访问令牌缺失");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 3. 验证Redis中的令牌
|
|
|
|
|
+ String key = TOKEN_KEY_PREFIX + token;
|
|
|
|
|
+ String value = RedisUtil.redisTemplateString.opsForValue().get(key);
|
|
|
|
|
+ if (BeanUtils.isEmpty(value)) {
|
|
|
|
|
+ sendError(httpResponse, "令牌无效或已过期");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 4. 场景校验:旧版无场景令牌放行(兼容存量前端);带场景的令牌只能访问同场景接口,其余值一律拒绝
|
|
|
|
|
+ boolean tokenAllowed = LEGACY_TOKEN_VALUE.equals(value)
|
|
|
|
|
+ || value.equals(SCENE_VALUE_PREFIX + scene);
|
|
|
|
|
+ if (!tokenAllowed) {
|
|
|
|
|
+ sendError(httpResponse, "令牌与当前接口不匹配");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 5. 删除令牌(一次性使用)
|
|
|
|
|
+ Boolean del = RedisUtil.redisTemplateString.delete(key);
|
|
|
|
|
+ if(del != null && del){
|
|
|
|
|
+ filterChain.doFilter(servletRequest, servletResponse);
|
|
|
|
|
+ }else {
|
|
|
|
|
+ sendError(httpResponse, "令牌不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 规范化请求路径后再匹配,防止尾斜杠、路径参数、URL编码、连续斜杠等变体绕过精确匹配。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String normalizeUri(String uri) {
|
|
|
|
|
+ if (BeanUtils.isEmpty(uri)) {
|
|
|
|
|
+ return "/";
|
|
|
|
|
+ }
|
|
|
|
|
+ String decoded = uri;
|
|
|
|
|
+ try {
|
|
|
|
|
+ decoded = URLDecoder.decode(uri, StringPool.UTF_8);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // 解码失败时按原始路径处理
|
|
|
|
|
+ }
|
|
|
|
|
+ String[] segments = decoded.split("/");
|
|
|
|
|
+ StringBuilder normalized = new StringBuilder();
|
|
|
|
|
+ for (String segment : segments) {
|
|
|
|
|
+ if (segment.isEmpty()) {
|
|
|
|
|
+ continue;
|
|
|
}
|
|
}
|
|
|
- // 4. 删除令牌(一次性使用)
|
|
|
|
|
- Boolean del = RedisUtil.redisTemplateString.delete(key);
|
|
|
|
|
- if(del != null && del){
|
|
|
|
|
- filterChain.doFilter(servletRequest, servletResponse);
|
|
|
|
|
- }else {
|
|
|
|
|
- sendError(httpResponse, "令牌不存在");
|
|
|
|
|
|
|
+ int semicolonIndex = segment.indexOf(';');
|
|
|
|
|
+ if (semicolonIndex >= 0) {
|
|
|
|
|
+ segment = segment.substring(0, semicolonIndex);
|
|
|
|
|
+ if (segment.isEmpty()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }else {
|
|
|
|
|
- filterChain.doFilter(servletRequest, servletResponse);
|
|
|
|
|
|
|
+ normalized.append('/').append(segment);
|
|
|
}
|
|
}
|
|
|
|
|
+ return normalized.length() == 0 ? "/" : normalized.toString();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 统一错误响应
|
|
// 统一错误响应
|
|
@@ -77,4 +133,4 @@ public class TokenAuthInterceptor extends AbstractFilter {
|
|
|
String.format("{\"code\":%d,\"message\":\"%s\"}", 401, msg)
|
|
String.format("{\"code\":%d,\"message\":\"%s\"}", 401, msg)
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+}
|