一、背景
筆者工作中遇到一個需求,需要開發(fā)一個注解,放在controller層的類或者方法上,用以校驗請求參數(shù)中(不管是url還是body體內(nèi),都要檢查,有token參數(shù),且符合校驗規(guī)則就放行)是否傳了一個token的參數(shù),并且token符合一定的生成規(guī)則,符合就不予攔截,放行請求,否則攔截請求。
用法如下圖所示
可以看到?@TokenCheck?注解既可以放在類上,也可以放在方法上 ,放在類上則對該類中的所有的方法進行攔截校驗。
注意:是加了注解才會校驗是否攔截,不加沒有影響。
整個代碼都是使用的最新springboot版本開發(fā)的,所以servlet相關的類都是使用jakarta
如果你的springboot版本比較老?,請使用javax
先引入以下依賴(javax不飄紅不用引入)
<dependency>
? ? ? <groupId>javax.servlet</groupId>
? ? ? <artifactId>javax.servlet-api</artifactId>
? ? ? <version>4.0.1</version>
? ? ? <scope>provided</scope>
</dependency>
?
我用到的第三方依賴
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.24</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.0.11</version> </dependency>
二、TokenCheck注解
package com.example.demo.interceptorToken; import java.lang.annotation.*; /** * 是否有token */ @Documented @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TokenCheck { }
三、請求包裝器RequestWrapper
主要是對request請求包裝下,因為攔截器會攔截request,會讀取其中的參數(shù)流,而流只能讀一次,后續(xù)再用到流的讀取會報錯,所以用一個包裝器類處理下,把流以字節(jié)形式讀出來,重寫了getInputStream(),后續(xù)可以重復使用。
package com.example.demo.interceptorToken; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import org.apache.commons.lang3.StringUtils; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; /** * @author hulei * @date 2024/1/11 19:48 * @Description 由于 request中getReader()和getInputStream()只能調(diào)用一次 導致在Controller @ResponseBody的時候獲取不到 null 或 Stream closed * 在項目中,可能會出現(xiàn)需要針對接口參數(shù)進行校驗等問題 * 構建可重復讀取inputStream的request */ public class RequestWrapper extends HttpServletRequestWrapper { // 將流保存下來 private final byte[] requestBody; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); requestBody = readBytes(request.getReader()); } @Override public ServletInputStream getInputStream() { final ByteArrayInputStream basic = new ByteArrayInputStream((requestBody != null && requestBody.length >0) ? requestBody : new byte[]{}); return new ServletInputStream() { @Override public int read() { return basic.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream())); } /** * 通過BufferedReader和字符編碼集轉(zhuǎn)換成byte數(shù)組 */ private byte[] readBytes(BufferedReader br) throws IOException { String str; StringBuilder retStr = new StringBuilder(); while ((str = br.readLine()) != null) { retStr.append(str); } if (StringUtils.isNotBlank(retStr.toString())) { return retStr.toString().getBytes(StandardCharsets.UTF_8); } return null; } }
四、過濾器RequestFilter
自定義請求過濾器,把請求用自定義的包裝器RequestWrapper包裝下,往調(diào)用下文傳遞,也是為了讓request請求的流能多次讀取
package com.example.demo.interceptorToken; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import org.springframework.core.annotation.Order; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.support.StandardServletMultipartResolver; import java.io.IOException; /** * @author hulei * @date 2024/1/11 19:48 * 自定義請求過濾器 */ //排序優(yōu)先級,最先執(zhí)行的過濾器 @Order(0) public class RequestFilter extends OncePerRequestFilter { //spring6.0版本后刪除了CommonsMultipartResolver,使用StandardServletMultipartResolver //如果是spring6.0版本,此行代碼不報錯請使用如下 // private CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); private final StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); /** * */ @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { //請求參數(shù)有form_data的話,防止request.getHeaders()報已使用,單獨處理 if (request.getContentType() != null && request.getContentType().contains("multipart/form-data")) { MultipartHttpServletRequest multiReq = multipartResolver.resolveMultipart(request); filterChain.doFilter(multiReq, response); }else{ ServletRequest requestWrapper; requestWrapper = new RequestWrapper(request); filterChain.doFilter(requestWrapper, response); } } }
五、請求過濾器配置類TokenFilterConfig
這個很好理解,把自定義配置類注入spring容器
package com.example.demo.interceptorToken; import jakarta.servlet.Filter; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletContext; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Enumeration; /** * @author hulei * @date 2024/1/11 19:48 * 將過濾器注入spring容器中 */ @Configuration public class TokenFilterConfig implements FilterConfig { @Bean Filter bodyFilter() { return new RequestFilter(); } @Bean public FilterRegistrationBean<RequestFilter> filters() { FilterRegistrationBean<RequestFilter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter((RequestFilter) bodyFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setName("requestFilter"); //多個filter的時候order的數(shù)值越小 則優(yōu)先級越高 //filterRegistrationBean.setOrder(0); return filterRegistrationBean; } @Override public String getFilterName() { return null; } @Override public ServletContext getServletContext() { return null; } @Override public String getInitParameter(String s) { return null; } @Override public Enumeration<String> getInitParameterNames() { return null; } }
?六、核心類RequestInterceptor攔截器
注意如果你的springboot版本也是低于3.0,請繼承HandlerInterceptorAdapter類,實現(xiàn)其中方法,基本不用改動類中的內(nèi)容,只需要 把implements HandlerInterceptor 改為extends HandlerInterceptorAdapter即可。
package com.example.demo.interceptorToken; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import org.apache.commons.lang3.StringUtils; import org.springframework.util.StreamUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.support.StandardServletMultipartResolver; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * @author hulei * @date 2024/1/11 19:48 * 自定義請求攔截器(spring boot 3.0以下的版本,需要繼承HandlerInterceptorAdapter類,實現(xiàn)對應得方法) */ public class RequestInterceptor implements HandlerInterceptor { /** * 需要從請求里驗證的關鍵字參數(shù)名 */ private static final String TOKEN_STR = "token"; /** * 進入攔截的方法前觸發(fā) * 這里主要從打了注解請求中查找有沒有token關鍵字,并且token的值是否符合一定的生成規(guī)則,是就放行,不是就攔截 */ @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { if (handler instanceof HandlerMethod handlerMethod) { //獲取token注解 TokenCheck tokenCheck = getTokenCheckAnnotation(handlerMethod); //請求參數(shù)有form_data的話,防止request.getHeaders()或request.getInputStream()報已使用錯誤,單獨處理 if (request.getContentType() != null && request.getContentType().contains("multipart/form-data")) { //判斷當前注解是否存在 if (tokenCheck != null) { final StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); MultipartHttpServletRequest multipartHttpServletRequest = multipartResolver.resolveMultipart(request); String urlOrBodyToken = "", tokenFromHeaders, tokenFromCookies; //獲取全部參數(shù),不管是params里的還是body(form_data)里的 urlOrBodyToken = getTokenFromUrlOrBody(multipartHttpServletRequest); //從headers獲取token tokenFromHeaders = getTokenFromHeaders(multipartHttpServletRequest); //從cookies獲取token tokenFromCookies = getTokenFromCookies(multipartHttpServletRequest); if (tokenRuleValidation(urlOrBodyToken) || tokenRuleValidation(tokenFromHeaders) || tokenRuleValidation(tokenFromCookies)) { return true; } else { returnJson(response, "token校驗失敗"); return false; } } } else { //判斷當前注解是否存在 if (tokenCheck != null) { // 獲取請求方式,如果需要根據(jù)請求方式做不同處理,則獲取后自行判斷即可 //String requestMethod = request.getMethod(); request = new RequestWrapper(request); //token關鍵字,分別是來自url、headers、cookies、body中的token String tokenFromUrl, tokenFromHeaders, tokenFromCookies, tokenFromBody; //url獲取token請求參數(shù) tokenFromUrl = getTokenFromUrl(request); //從headers獲取token tokenFromHeaders = getTokenFromHeaders(request); //從cookies獲取token tokenFromCookies = getTokenFromCookies(request); //從body體獲取token參數(shù) tokenFromBody = getTokenFromBody(request); //token校驗判斷 if (tokenRuleValidation(tokenFromUrl) || tokenRuleValidation(tokenFromHeaders) || tokenRuleValidation(tokenFromCookies) || tokenRuleValidation(tokenFromBody)) { return true; } else { returnJson(response, "token校驗失敗"); return false; } } } return true; } return true; } /** * 獲取TokenCheck注解(先從類上優(yōu)先獲取) */ private TokenCheck getTokenCheckAnnotation(HandlerMethod handler) { Method method = handler.getMethod(); //獲取方法所屬的類,并獲取類上的@TokenCheck注解 Class<?> clazz = method.getDeclaringClass(); TokenCheck tokenCheck = null; if (clazz.isAnnotationPresent(TokenCheck.class)) { tokenCheck = clazz.getAnnotation(TokenCheck.class); } //類上沒有注解,則從方法上再獲取@TokenCheck tokenCheck = tokenCheck == null ? method.getAnnotation(TokenCheck.class) : tokenCheck; return tokenCheck; } //=============================================================================================================================== /** * form_data參數(shù)形式,從url和body中獲取符合生成規(guī)則條的token */ private String getTokenFromUrlOrBody(HttpServletRequest multipartHttpServletRequest) { String urlOrBodyToken = ""; Map<String, String[]> urlAndBodyParam = multipartHttpServletRequest.getParameterMap(); String[] tokenFromUrlOrBody = urlAndBodyParam.get(TOKEN_STR); for (String tokenStr : tokenFromUrlOrBody) { if (tokenRuleValidation(tokenStr)) { urlOrBodyToken = tokenStr; break; } } return urlOrBodyToken; } //=============================================================================================================================== /** * 從headers獲取token */ private String getTokenFromHeaders(HttpServletRequest request) { Map<String, String> headerMap = new HashMap<>(); Enumeration<String> enumeration = request.getHeaderNames(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); String value = request.getHeader(name); headerMap.put(name, value); } return headerMap.get(TOKEN_STR) == null ? "" : String.valueOf(headerMap.get(TOKEN_STR)); } //=============================================================================================================================== /** * 從cookies獲取token */ private String getTokenFromCookies(HttpServletRequest request) { Map<String, String> cookieMap = new ConcurrentHashMap<>(); Cookie[] cookies = request.getCookies(); if (null != cookies) { Arrays.stream(cookies).forEach(element -> cookieMap.put(element.getName(), element.getValue()) ); } return cookieMap.get(TOKEN_STR) == null ? "" : String.valueOf(cookieMap.get(TOKEN_STR)); } //=============================================================================================================================== /** * 從請求體獲取token參數(shù) */ private String getTokenFromBody(HttpServletRequest request) throws Exception { String bodyParamsStr = this.getPostParam(request); String tokenFromBody = ""; //判斷是否是json數(shù)組 boolean isJsonArray = JSONUtil.isTypeJSONArray(bodyParamsStr); if (!isJsonArray) { tokenFromBody = JSONUtil.parseObj(bodyParamsStr).getStr(TOKEN_STR); } else { JSONArray jsonArray = JSONUtil.parseArray(bodyParamsStr); Set<String> tokenSet = new HashSet<>(); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); if (StringUtils.isNotEmpty(jsonObject.getStr(TOKEN_STR))) { tokenSet.add(jsonObject.getStr(TOKEN_STR)); } } if (!tokenSet.isEmpty()) { tokenFromBody = tokenSet.stream().filter(this::tokenRuleValidation).findFirst().orElse(""); } } return tokenFromBody; } private String getPostParam(HttpServletRequest request) throws Exception { RequestWrapper readerWrapper = new RequestWrapper(request); return StringUtils.isEmpty(getBodyParams(readerWrapper.getInputStream(), request.getCharacterEncoding())) ? "{}" : getBodyParams(readerWrapper.getInputStream(), request.getCharacterEncoding()); } private String getBodyParams(ServletInputStream inputStream, String charset) throws Exception { String body = StreamUtils.copyToString(inputStream, Charset.forName(charset)); if (StringUtils.isEmpty(body)) { return ""; } return body; } //=============================================================================================================================== /** * 如果是get請求,則把url中的請求參數(shù)獲取到,轉(zhuǎn)換為map */ public String getTokenFromUrl(HttpServletRequest request) throws UnsupportedEncodingException { //獲取當前請求的編碼方式,用于參數(shù)value解碼 String encoding = request.getCharacterEncoding(); String urlQueryString = request.getQueryString(); Map<String, String> queryMap = new HashMap<>(); String[] arrSplit; if (urlQueryString == null) { return ""; } else { //每個鍵值為一組 arrSplit = urlQueryString.split("&"); for (String strSplit : arrSplit) { String[] arrSplitEqual = strSplit.split("="); //解析出鍵值 if (arrSplitEqual.length > 1) { queryMap.put(arrSplitEqual[0], URLDecoder.decode(arrSplitEqual[1], encoding)); } else { if (!"".equals(arrSplitEqual[0])) { queryMap.put(arrSplitEqual[0], ""); } } } } return queryMap.get(TOKEN_STR) == null ? "" : String.valueOf(queryMap.get(TOKEN_STR)); } /** * token 規(guī)則校驗 * * @param token token關鍵字 */ private boolean tokenRuleValidation(String token) { return "AAABBB".equals(token); } /** * 離開攔截的方法后觸發(fā) */ @Override public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, ModelAndView modelAndView) { } /** * 返回response的json信息 */ private void returnJson(HttpServletResponse response, String json) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); try (PrintWriter writer = response.getWriter()) { writer.print(json); } } }
七、攔截器注冊InterceptorRegister
一個配置類,把自定義的攔截器注入spring
package com.example.demo.interceptorToken; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author hulei * @date 2024/1/11 19:48 * 將攔截注入spring容器 */ @Configuration public class InterceptorRegister implements WebMvcConfigurer { @Bean public RequestInterceptor tokenInterceptor() { return new RequestInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor()); } }
?八、總結(jié)
本例主要是自定義注解,完成請求參數(shù)的攔截校驗,實際中可根據(jù)需求進行修改,如記錄日志,攔截校驗其他參數(shù),修改RequestInterceptor中的攔截前方法和攔截后方法的邏輯即可
gitee地址:?Token-Check-Demo: 自定義注解攔截request請求文章來源:http://www.zghlxwxcb.cn/news/detail-798201.html
注: 創(chuàng)作不易,轉(zhuǎn)載請標明原作地址文章來源地址http://www.zghlxwxcb.cn/news/detail-798201.html
到了這里,關于Controller層自定義注解攔截request請求校驗的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!