diff --git a/src/main/java/cn/soul2/jyjc/admin/JavaJyjcAdminApplication.java b/src/main/java/cn/soul2/jyjc/admin/JavaJyjcAdminApplication.java index 3b83517..f7af8a4 100644 --- a/src/main/java/cn/soul2/jyjc/admin/JavaJyjcAdminApplication.java +++ b/src/main/java/cn/soul2/jyjc/admin/JavaJyjcAdminApplication.java @@ -3,6 +3,7 @@ package cn.soul2.jyjc.admin; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; /** * @author Soul2 @@ -10,6 +11,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; */ @SpringBootApplication @MapperScan("cn.soul2.jyjc.admin.mapper") +@ComponentScan("cn.soul2.jyjc.admin.config") public class JavaJyjcAdminApplication { public static void main(String[] args) { diff --git a/src/main/java/cn/soul2/jyjc/admin/annotation/SkinEncrypt.java b/src/main/java/cn/soul2/jyjc/admin/annotation/SkinEncrypt.java new file mode 100644 index 0000000..00c70de --- /dev/null +++ b/src/main/java/cn/soul2/jyjc/admin/annotation/SkinEncrypt.java @@ -0,0 +1,18 @@ +package cn.soul2.jyjc.admin.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 跳过加解密 + * + * @author Soul2 + * @date 2024-04-08 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SkinEncrypt { + +} diff --git a/src/main/java/cn/soul2/jyjc/admin/config/FilterConfig.java b/src/main/java/cn/soul2/jyjc/admin/config/FilterConfig.java new file mode 100644 index 0000000..8b03e54 --- /dev/null +++ b/src/main/java/cn/soul2/jyjc/admin/config/FilterConfig.java @@ -0,0 +1,41 @@ +package cn.soul2.jyjc.admin.config; + +import cn.soul2.jyjc.admin.filter.ReplaceStreamFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.Filter; + +/** + * @author Soul2 + * @description 过滤器配置类 + * @date 2024-04-08 + *

照抄自 Shao duo

+ */ +@Configuration +public class FilterConfig { + /** + * 注册过滤器 + * + * @return FilterRegistrationBean + */ + @Bean + public FilterRegistrationBean someFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(replaceStreamFilter()); + registration.addUrlPatterns("/*"); + registration.setName("streamFilter"); + return registration; + } + + /** + * 实例化StreamFilter + * + * @return Filter + */ + @Bean(name = "replaceStreamFilter") + public Filter replaceStreamFilter() { + return new ReplaceStreamFilter(); + } +} diff --git a/src/main/java/cn/soul2/jyjc/admin/config/WebMvcConfig.java b/src/main/java/cn/soul2/jyjc/admin/config/WebMvcConfig.java index a8e0b1c..1aa4736 100644 --- a/src/main/java/cn/soul2/jyjc/admin/config/WebMvcConfig.java +++ b/src/main/java/cn/soul2/jyjc/admin/config/WebMvcConfig.java @@ -1,12 +1,12 @@ package cn.soul2.jyjc.admin.config; -import cn.soul2.jyjc.admin.interceptor.TokenInterceptor; +import cn.soul2.jyjc.admin.interceptor.FinallyInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** - * 注册请求拦截器 + * 注册拦截器 * * @author Soul2 * @date 2024-04-02 @@ -17,7 +17,7 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new TokenInterceptor()) + registry.addInterceptor(new FinallyInterceptor()) // 拦截所有路径 .addPathPatterns("/**"); } diff --git a/src/main/java/cn/soul2/jyjc/admin/filter/ReplaceStreamFilter.java b/src/main/java/cn/soul2/jyjc/admin/filter/ReplaceStreamFilter.java new file mode 100644 index 0000000..8d37d85 --- /dev/null +++ b/src/main/java/cn/soul2/jyjc/admin/filter/ReplaceStreamFilter.java @@ -0,0 +1,40 @@ +package cn.soul2.jyjc.admin.filter; + +import lombok.extern.slf4j.Slf4j; +import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * @author shaoduo + * @description 替换HttpServletRequest + * @since 1.0 + **/ +@Slf4j +public class ReplaceStreamFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + log.info("StreamFilter初始化..."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + //如果是文件上传则会报错可以判断是否是文件上传不读取流即可 + if (ServletFileUpload.isMultipartContent((HttpServletRequest) request)) { + chain.doFilter(request, response); + return; + } else { + ServletRequest requestWrapper = new ShaoduoRequestWrapper((HttpServletRequest) request); + chain.doFilter(requestWrapper, response); + return; + } + + } + + @Override + public void destroy() { + log.info("StreamFilter销毁..."); + } +} diff --git a/src/main/java/cn/soul2/jyjc/admin/filter/ShaoduoRequestWrapper.java b/src/main/java/cn/soul2/jyjc/admin/filter/ShaoduoRequestWrapper.java new file mode 100644 index 0000000..1de493e --- /dev/null +++ b/src/main/java/cn/soul2/jyjc/admin/filter/ShaoduoRequestWrapper.java @@ -0,0 +1,134 @@ +package cn.soul2.jyjc.admin.filter; + +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author shaoduo + * @description 包装HttpServletRequest,目的是让其输入流可重复读 + * @since 1.0 + **/ +@Slf4j +public class ShaoduoRequestWrapper extends HttpServletRequestWrapper { + /** + * 存储body数据的容器 + */ + private byte[] body; + + public ShaoduoRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + + // 将body数据存储起来 + String bodyStr = getBodyString(request); + body = bodyStr.getBytes(Charset.defaultCharset()); + } + + /** + * 获取请求Body + * + * @param request request + * @return String + */ + public String getBodyString(final ServletRequest request) { + try { + return inputStream2String(request.getInputStream()); + } catch (IOException e) { + log.error("", e); + throw new RuntimeException(e); + } + } + + /** + * 获取请求Body + * + * @return String + */ + public String getBodyString() { + InputStream inputStream = new ByteArrayInputStream(body); + + return inputStream2String(inputStream); + } + + /** + * 修改body 将json 重新设置成body + * + * @param val + */ + public void setBody(String val) { + + body = val.getBytes(StandardCharsets.UTF_8); + } + + /** + * 将inputStream里的数据读取出来并转换成字符串 + * + * @param inputStream inputStream + * @return String + */ + private String inputStream2String(InputStream inputStream) { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + + try { + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + log.error("", e); + throw new RuntimeException(e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.error("", e); + } + } + } + + + return sb.toString(); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + + return new ServletInputStream() { + @Override + public int read() throws IOException { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } +} diff --git a/src/main/java/cn/soul2/jyjc/admin/interceptor/FinallyInterceptor.java b/src/main/java/cn/soul2/jyjc/admin/interceptor/FinallyInterceptor.java new file mode 100644 index 0000000..5d0c3f4 --- /dev/null +++ b/src/main/java/cn/soul2/jyjc/admin/interceptor/FinallyInterceptor.java @@ -0,0 +1,128 @@ +package cn.soul2.jyjc.admin.interceptor; + +import cn.soul2.jyjc.admin.annotation.SkinEncrypt; +import cn.soul2.jyjc.admin.annotation.SkinLogin; +import cn.soul2.jyjc.admin.config.UserLoginStatusBean; +import cn.soul2.jyjc.admin.filter.ShaoduoRequestWrapper; +import cn.soul2.jyjc.admin.utils.EncryptUtils; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.MediaType; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; + +/** + * Token拦截器 + * + * @author Soul2 + * @date 2024-04-02 15:31 + */ +@Slf4j +public class FinallyInterceptor implements HandlerInterceptor { + + @Autowired + @Lazy + private ApplicationContext context; + @Resource + @Lazy + private UserLoginStatusBean userLoginStatusBean; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + boolean pass = false; + // 允许OPTIONS请求通过 + if ("OPTIONS".equals(request.getMethod()) && request.getHeader("Origin") != null) { + return true; + } + // 如果不是映射到方法直接通过 + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + String httpMethod = request.getMethod(); + SkinEncrypt skinEncrypt = handlerMethod.getMethodAnnotation(SkinEncrypt.class); + // 检查方法上是否存在SkinLogin注解 + boolean hasSkinLogin = handlerMethod.getMethodAnnotation(SkinLogin.class) != null; + // 从请求头中获取 token + String token = request.getHeader("jyjc-Token"); + try { + /* + 加解密 + */ + // 不拦截get请求 + if ("GET".equals(httpMethod)) { + pass = true; + // 如果是post请求且是json + } else if ("POST".equals(httpMethod)) { + // 如果类型为空就放行 + if (request.getContentType() == null) { + pass = true; + } + // 如果类型不是json 就放行 + if (!(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE))) { + pass = true; + } + } + + // 跳过使用SkinEncrypt注解的情况 + if (skinEncrypt != null) { + pass = true; + } + ShaoduoRequestWrapper shaoduoRequestWrapper; + if (request instanceof ShaoduoRequestWrapper) { + shaoduoRequestWrapper = (ShaoduoRequestWrapper) request; + } else { + shaoduoRequestWrapper = new ShaoduoRequestWrapper(request); + request = shaoduoRequestWrapper; + } + String jsonParamBody = shaoduoRequestWrapper.getBodyString(); + JSONObject obj = JSON.parseObject(EncryptUtils.decrypt(jsonParamBody)); + String afterBody = JSONObject.toJSONString(obj); + + System.out.println("加密前 " + jsonParamBody); + System.out.println("解密后 " + afterBody); + shaoduoRequestWrapper.setBody(afterBody); + String temp = new ShaoduoRequestWrapper(shaoduoRequestWrapper).getBodyString(); + System.out.println("过滤器中缓存 " + temp); + + /* + token验证 + */ + + if (hasSkinLogin) { + // 如果存在,绕过拦截器 + pass = true; + } else { + // 验证token + if (userLoginStatusBean == null) { + userLoginStatusBean = context.getBean(UserLoginStatusBean.class); + } + // 检查 token 是否存在并且有效 + if (token == null) { + // 没有Token,拒绝请求 + response.setStatus(40401); + pass = false; + } else if (userLoginStatusBean != null && !userLoginStatusBean.containsToken(token)) { + // Token 无效,拒绝请求,可以返回特定的响应状态码,例如 401 Unauthorized + response.setStatus(40401); + pass = false; + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + return pass; + } + +} diff --git a/src/main/java/cn/soul2/jyjc/admin/interceptor/TokenInterceptor.java b/src/main/java/cn/soul2/jyjc/admin/interceptor/TokenInterceptor.java deleted file mode 100644 index 69d7d60..0000000 --- a/src/main/java/cn/soul2/jyjc/admin/interceptor/TokenInterceptor.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.soul2.jyjc.admin.interceptor; - -import cn.soul2.jyjc.admin.annotation.SkinLogin; -import cn.soul2.jyjc.admin.config.UserLoginStatusBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Soul2 - * @date 2024-04-02 15:31 - */ - -public class TokenInterceptor implements HandlerInterceptor { - - @Autowired - private UserLoginStatusBean userLoginStatusBean; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - // 允许OPTIONS请求通过 - if (isCorsRequest(request)) { - return true; - } - - // 跳过登录 - // 如果处理器是一个方法处理器 - if (handler instanceof HandlerMethod) { - HandlerMethod handlerMethod = (HandlerMethod) handler; - // 检查方法上是否存在SkinLogin注解 - if (handlerMethod.getMethod().isAnnotationPresent(SkinLogin.class)) { - // 如果存在,绕过拦截器 - return true; - } - } - // 验证token - // 从请求头中获取 token - String token = request.getHeader("jyjc-Token"); - // 检查 token 是否存在并且有效,这里可以根据实际情况自行实现验证逻辑 - if (token != null && isValidToken(token)) { - // Token 有效,允许请求通过 - return true; - } else { - // Token 无效,拒绝请求,可以返回特定的响应状态码,例如 401 Unauthorized - response.setStatus(40401); - return false; - } - } - - private boolean isValidToken(String token) { - // 实现 token 验证逻辑,例如验证 token 的签名或者在数据库中验证 token 的有效性 - return userLoginStatusBean.containsToken(token); - // 返回 true 表示 token 有效,返回 false 表示 token 无效 - } - - private boolean isCorsRequest(HttpServletRequest request) { - // 检查请求头中是否包含 Origin 字段,如果存在,则认为是跨域请求 - return request.getHeader("Origin") != null; - } -} diff --git a/src/main/java/cn/soul2/jyjc/admin/utils/AesUtils.java b/src/main/java/cn/soul2/jyjc/admin/utils/AesUtils.java index a4dcf04..21593c8 100644 --- a/src/main/java/cn/soul2/jyjc/admin/utils/AesUtils.java +++ b/src/main/java/cn/soul2/jyjc/admin/utils/AesUtils.java @@ -1,7 +1,5 @@ package cn.soul2.jyjc.admin.utils; -import org.springframework.beans.factory.annotation.Value; - import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; @@ -13,31 +11,30 @@ import java.util.Base64; public class AesUtils { - @Value("${encrypt.keys.aes}") - private String aesKey; + private static final String AES_KEY = "98478f8a45887eda446501bcb44010520c3f53c7b45ff36215e86ce4f049b0f2"; private static final String AES_ALGORITHM = "AES"; - private SecretKeySpec generateKey(String key) { + private static SecretKeySpec generateKey(String key) { return new SecretKeySpec(key.getBytes(), AES_ALGORITHM); } - public String encrypt(String source) throws Exception { - return encrypt(source, aesKey); + public static String encrypt(String source) throws Exception { + return encrypt(source, AES_KEY); } - public String decrypt(String encrypted) throws Exception { - return decrypt(encrypted, aesKey); + public static String decrypt(String encrypted) throws Exception { + return decrypt(encrypted, AES_KEY); } - private String encrypt(String source, String key) throws Exception { + public static String encrypt(String source, String key) throws Exception { Cipher cipher = Cipher.getInstance(AES_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, generateKey(key)); byte[] encryptedBytes = cipher.doFinal(source.getBytes()); return Base64.getEncoder().encodeToString(encryptedBytes); } - private String decrypt(String encrypted, String key) throws Exception { + public static String decrypt(String encrypted, String key) throws Exception { Cipher cipher = Cipher.getInstance(AES_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, generateKey(key)); byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encrypted)); @@ -45,7 +42,6 @@ public class AesUtils { } public static void main(String[] args) { - } diff --git a/src/main/java/cn/soul2/jyjc/admin/utils/EncryptUtils.java b/src/main/java/cn/soul2/jyjc/admin/utils/EncryptUtils.java new file mode 100644 index 0000000..212d360 --- /dev/null +++ b/src/main/java/cn/soul2/jyjc/admin/utils/EncryptUtils.java @@ -0,0 +1,132 @@ +package cn.soul2.jyjc.admin.utils; + +import org.apache.tomcat.util.codec.binary.Base64; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.SecretKeySpec; + +/** + * @author Soul2 + * @date 2024-04-08 15:00 + *

照抄自 Shao duo

+ */ + +public class EncryptUtils { + + private static final String KEY = "620b8d3c3be1e725"; + + //参数分别代表 算法名称/加密模式/数据填充方式 + private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding"; + + /** + * 加密 + * + * @param content 加密的字符串 + * @param encryptKey key值 + * @return {@link String} + */ + public static String encrypt(String content, String encryptKey) throws Exception { + KeyGenerator kgen = KeyGenerator.getInstance("AES"); + kgen.init(128); + Cipher cipher = Cipher.getInstance(ALGORITHMSTR); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES")); + byte[] b = cipher.doFinal(content.getBytes("utf-8")); + // 采用base64算法进行转码,避免出现中文乱码 + return Base64.encodeBase64String(b); + } + + /** + * 解密 + * + * @param encryptStr 解密的字符串 + * @param decryptKey 解密的key值 + * @return {@link String} + */ + public static String decrypt(String encryptStr, String decryptKey) throws Exception { + KeyGenerator kgen = KeyGenerator.getInstance("AES"); + kgen.init(128); + Cipher cipher = Cipher.getInstance(ALGORITHMSTR); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes("utf-8"), "AES")); + // 采用base64算法进行转码,避免出现中文乱码 + + //byte[] b = hex2Bytes(encryptStr) ; + byte[] encryptBytes = Base64.decodeBase64(encryptStr); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + } + + /** + * @param content 加密的字符串 + * @return {@link String} + */ + public static String encrypt(String content) throws Exception { + return encrypt(content, KEY); + } + + /** + * @param encryptStr 解密的字符串 + * @return {@link String} + */ + public static String decrypt(String encryptStr) throws Exception { + return decrypt(encryptStr, KEY); + } + + +/* public static void main(String[] args) throws Exception { + Map map=new HashMap(); + map.put("key","value"); + map.put("中文","汉字"); + String content = JSONObject.toJSONString(map); + System.out.println("加密前:" + content); + String encrypt = encrypt(content, KEY); + System.out.println("加密后:" + encrypt); + String decrypt = decrypt(encrypt, KEY); + System.out.println("解密后:" + decrypt); + }*/ + + /** + * byte数组 转换成 16进制小写字符串 + */ + public static String bytes2Hex(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + + StringBuilder hex = new StringBuilder(); + + for (byte b : bytes) { + hex.append(HEXES[(b >> 4) & 0x0F]); + hex.append(HEXES[b & 0x0F]); + } + + return hex.toString(); + } + + /** + * 16进制字符串 转换为对应的 byte数组 + */ + public static byte[] hex2Bytes(String hex) { + if (hex == null || hex.length() == 0) { + return null; + } + + char[] hexChars = hex.toCharArray(); + // 如果 hex 中的字符不是偶数个, 则忽略最后一个 + byte[] bytes = new byte[hexChars.length / 2]; + + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) Integer.parseInt("" + hexChars[i * 2] + hexChars[i * 2 + 1], 16); + } + + return bytes; + } + + private static final char[] HEXES = { + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'a', 'b', + 'c', 'd', 'e', 'f' + }; + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 396d044..659f4c8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,8 +4,6 @@ server: #上下文 servlet.context-path: /thli/jyjc/api -encrypt.keys.aes: 98478f8a45887eda446501bcb44010520c3f53c7b45ff36215e86ce4f049b0f2 - spring: application.name: jyjc-admin profiles.include: datasource,mybatis-plus,cors