Compare commits

...

4 Commits

  1. 18
      src/main/java/cn/soul2/jyjc/admin/annotation/SkinEncrypt.java
  2. 11
      src/main/java/cn/soul2/jyjc/admin/bean/UserLoginStatusBean.java
  3. 28
      src/main/java/cn/soul2/jyjc/admin/config/CorsConfig.java
  4. 41
      src/main/java/cn/soul2/jyjc/admin/config/FilterConfig.java
  5. 49
      src/main/java/cn/soul2/jyjc/admin/config/WebMvcConfig.java
  6. 60
      src/main/java/cn/soul2/jyjc/admin/filter/ReplaceStreamFilter.java
  7. 130
      src/main/java/cn/soul2/jyjc/admin/filter/ShaoduoRequestWrapper.java
  8. 120
      src/main/java/cn/soul2/jyjc/admin/filter/Soul2ResponseWrapper.java
  9. 140
      src/main/java/cn/soul2/jyjc/admin/interceptor/FinallyInterceptor.java
  10. 63
      src/main/java/cn/soul2/jyjc/admin/interceptor/TokenInterceptor.java
  11. 9
      src/main/java/cn/soul2/jyjc/admin/repository/IUserLoginOutRepository.java
  12. 29
      src/main/java/cn/soul2/jyjc/admin/repository/impl/UserLoginOutRepositoryImpl.java
  13. 10
      src/main/java/cn/soul2/jyjc/admin/service/IUserService.java
  14. 8
      src/main/java/cn/soul2/jyjc/admin/service/impl/UserServiceImpl.java
  15. 20
      src/main/java/cn/soul2/jyjc/admin/utils/AesUtils.java
  16. 132
      src/main/java/cn/soul2/jyjc/admin/utils/EncryptUtils.java
  17. 2
      src/main/resources/application-cors.yml
  18. 2
      src/main/resources/application.yml
  19. 127
      src/main/resources/logback-spring.xml

@ -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 {
}

@ -1,10 +1,11 @@
package cn.soul2.jyjc.admin.config;
package cn.soul2.jyjc.admin.bean;
import cn.soul2.jyjc.admin.entity.UserLoginOutDO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ -12,6 +13,12 @@ public class UserLoginStatusBean {
private ConcurrentHashMap<String, UserLoginOutDO> loginStatusMap = new ConcurrentHashMap<>();
public void loadUserLoginStatus(Collection<UserLoginOutDO> caches) {
for (UserLoginOutDO cache : caches) {
loginStatusMap.put(cache.getId(), cache);
}
}
/**
* 缓存登录信息
*
@ -80,7 +87,7 @@ public class UserLoginStatusBean {
return Boolean.FALSE;
}
UserLoginOutDO loginStatus = loginStatusMap.getOrDefault(token, null);
if (loginStatus.getLapseTime() != null) {
if (loginStatus != null && loginStatus.getLapseTime() != null) {
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(loginStatus.getLapseTime())) {
logout(token);

@ -1,28 +0,0 @@
package cn.soul2.jyjc.admin.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Soul2
* @date 2024-03-25
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Value("${cors.allow-origin}")
private String[] allowOrigin;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedOrigins(allowOrigin)
.allowCredentials(true)
.allowedMethods("POST")
.maxAge(3600);
}
}

@ -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
* <p>照抄自 <a href="https://blog.csdn.net/shaoduo/article/details/122322578">Shao duo</a></p>
*/
@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();
}
}

@ -1,12 +1,17 @@
package cn.soul2.jyjc.admin.config;
import cn.soul2.jyjc.admin.interceptor.TokenInterceptor;
import cn.soul2.jyjc.admin.interceptor.FinallyInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 注册请求拦截器
* 注册拦截器
*
* @author Soul2
* @date 2024-04-02
@ -15,9 +20,47 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${cors.allow-origin}")
private String[] allowOrigin;
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 设置允许跨域请求的域名
for (String url : allowOrigin) {
config.addAllowedOrigin(url);
}
// 是否允许证书 不再默认开启
// config.setAllowCredentials(true);
// 设置允许的方法
config.addAllowedMethod("*");
// 允许任何头
config.addAllowedHeader("*");
config.addExposedHeader("token");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedHeaders("*")
// .allowedOrigins(allowOrigin)
// .allowCredentials(true)
// .allowedMethods("POST")
// .maxAge(3600);
// }
@Bean
FinallyInterceptor createFinallyInterceptor() {
return new FinallyInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor())
registry.addInterceptor(createFinallyInterceptor())
// 拦截所有路径
.addPathPatterns("/**");
}

@ -0,0 +1,60 @@
package cn.soul2.jyjc.admin.filter;
import cn.soul2.jyjc.admin.utils.EncryptUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @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);
} else if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) request).getMethod())) {
chain.doFilter(request, response);
} else {
ServletRequest requestWrapper = new ShaoduoRequestWrapper((HttpServletRequest) request);
// System.out.printf("ReplaceStreamFilter触发, Method: %s, URI: %s%n", ((HttpServletRequest) request).getMethod(), ((HttpServletRequest) request).getRequestURI());
HttpServletResponse httpResponse = (HttpServletResponse) response;
Soul2ResponseWrapper soul2ResponseWrapper = new Soul2ResponseWrapper(httpResponse);
chain.doFilter(requestWrapper, soul2ResponseWrapper);
try {
byte[] data = soul2ResponseWrapper.getResponseData();
log.debug("原始返回 -> " + new String(data));
String encryptBody = EncryptUtils.encrypt(new String(data));
log.debug("加密返回 -> " + encryptBody);
soul2ResponseWrapper.setHeader("encrypt", "1");
soul2ResponseWrapper.setHeader("Access-Control-Expose-Headers", "encrypt");
PrintWriter out = response.getWriter();
out.print(encryptBody);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void destroy() {
log.info("StreamFilter销毁...");
}
}

@ -0,0 +1,130 @@
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 <a href="https://blog.csdn.net/shaoduo/article/details/122322578">shaoduo</a>
**/
@Slf4j
public class ShaoduoRequestWrapper extends HttpServletRequestWrapper {
/**
* 存储body数据的容器
*/
private byte[] body;
public ShaoduoRequestWrapper(HttpServletRequest request) {
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() throws IOException {
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) throws IOException {
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() {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}

@ -0,0 +1,120 @@
package cn.soul2.jyjc.admin.filter;
import lombok.SneakyThrows;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
/**
* @author Soul2
* @description 包装ServletResponse, 照抄自 <a href="https://blog.csdn.net/temp_44/article/details/107762290">temp_44</a>
* @date 2024-04-15
*/
public class Soul2ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = null;
private ServletOutputStream out = null;
private PrintWriter writer = null;
public Soul2ResponseWrapper(HttpServletResponse resp) throws IOException {
super(resp);
// 真正存储数据的流
buffer = new ByteArrayOutputStream();
out = new WapperedOutputStream(buffer);
writer = new PrintWriter(new OutputStreamWriter(buffer,
this.getCharacterEncoding()));
}
/**
* 重载父类获取outputstream的方法
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
/**
* 重载父类获取writer的方法
*/
@Override
public PrintWriter getWriter() throws UnsupportedEncodingException {
return writer;
}
/**
* The default behavior of this method is to call
* setCharacterEncoding(String charset) on the wrapped response object.
*
* @param charset
* @since 2.4
*/
@SneakyThrows
@Override
public void setCharacterEncoding(String charset) {
super.setCharacterEncoding(charset);
writer = new PrintWriter(new OutputStreamWriter(buffer,
this.getCharacterEncoding()));
}
/**
* 重载父类获取flushBuffer的方法
*/
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void reset() {
buffer.reset();
}
/**
* 将outwriter中的数据强制输出到WapperedResponse的buffer里面否则取不到数据
*/
public byte[] getResponseData() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
/**
* 内部类对ServletOutputStream进行包装
*/
private class WapperedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = null;
public WapperedOutputStream(ByteArrayOutputStream stream)
throws IOException {
bos = stream;
}
@Override
public void write(int b) throws IOException {
bos.write(b);
}
@Override
public void write(byte[] b) throws IOException {
bos.write(b, 0, b.length);
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
// TODO Auto-generated method stub
}
}
}

@ -0,0 +1,140 @@
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.bean.UserLoginStatusBean;
import cn.soul2.jyjc.admin.filter.ShaoduoRequestWrapper;
import cn.soul2.jyjc.admin.service.IUserService;
import cn.soul2.jyjc.admin.utils.EncryptUtils;
import cn.soul2.jyjc.admin.vo.base.Back;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.io.PrintWriter;
import java.lang.reflect.Method;
/**
* Token拦截器
*
* @author Soul2
* @date 2024-04-02 15:31
*/
@Slf4j
public class FinallyInterceptor implements HandlerInterceptor {
@Resource
private UserLoginStatusBean userLoginStatusBean;
@Autowired
private IUserService userService;
private boolean load = true;
private void loadUserLoginStatus() {
userLoginStatusBean.loadUserLoginStatus(userService.getLoginedUser());
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (load) {
loadUserLoginStatus();
load = false;
}
boolean pass = false;
// 允许OPTIONS请求通过
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
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;
}
}
// 跳过使用 @SkinEncrypt 的情况
if (skinEncrypt != null) {
pass = true;
}
if (!pass) {
ShaoduoRequestWrapper shaoduoRequestWrapper;
try {
shaoduoRequestWrapper = (ShaoduoRequestWrapper) request;
} catch (Exception e) {
System.out.printf("request.ClassTypeError: %s%n", request.getClass().getName());
return true;
}
String sourceParamBody = shaoduoRequestWrapper.getBodyString();
JSONObject obj = JSON.parseObject(EncryptUtils.decrypt(sourceParamBody));
String afterBody = JSONObject.toJSONString(obj);
shaoduoRequestWrapper.setBody(afterBody);
log.debug(String.format("解密: %s -> %s", sourceParamBody, afterBody));
}
/* token验证 */
if (hasSkinLogin) {
// 跳过使用 @SkinLogin 的情况
pass = true;
} else {
// 验证token
// 检查 token 是否存在并且有效
if (token == null) {
// 没有Token,拒绝请求
response.setStatus(40401);
pass = false;
} else if (userLoginStatusBean != null) {
if (!userLoginStatusBean.containsToken(token)) {
// Token 无效,拒绝请求
response.setContentType("application/json");
Back<String> back = new Back<String>().setCode(40401).setMessage("Token invalid!");
// 转换为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper();
String responseBody = objectMapper.writeValueAsString(back);
// 输出错误信息到响应中
PrintWriter writer = response.getWriter();
writer.print(responseBody);
writer.flush();
pass = false;
} else {
// token存在, 通过拦截器
pass = true;
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return pass;
}
}

@ -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;
}
}

@ -8,6 +8,8 @@ import cn.soul2.jyjc.admin.entity.UserLoginOutDO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* <p>
* 用户登入登出表 服务类
@ -59,4 +61,11 @@ public interface IUserLoginOutRepository extends IService<UserLoginOutDO> {
*/
Boolean removeByUserId(String userId);
/**
* 重建当前登录的用户
*
* @return {@link List}<{@link UserLoginOutDO}>
*/
List<UserLoginOutDO> getLoginedUser();
}

@ -1,6 +1,6 @@
package cn.soul2.jyjc.admin.repository.impl;
import cn.soul2.jyjc.admin.config.UserLoginStatusBean;
import cn.soul2.jyjc.admin.bean.UserLoginStatusBean;
import cn.soul2.jyjc.admin.dto.UserLoginDTO;
import cn.soul2.jyjc.admin.dto.UserLoginOutPageDTO;
import cn.soul2.jyjc.admin.dto.UserLogoutDTO;
@ -12,6 +12,7 @@ import cn.soul2.jyjc.admin.utils.base.PageUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -19,6 +20,12 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* <p>
@ -96,4 +103,24 @@ public class UserLoginOutRepositoryImpl extends ServiceImpl<UserLoginOutMapper,
update.eq(UserLoginOutDO::getUserId, userId);
return super.remove(update);
}
@Override
public List<UserLoginOutDO> getLoginedUser() {
LocalDateTime now = LocalDateTime.now();
LambdaQueryWrapper<UserLoginOutDO> query = Wrappers.lambdaQuery();
query
.isNull(UserLoginOutDO::getLogoutTime)
.gt(UserLoginOutDO::getLapseTime, now);
List<UserLoginOutDO> list = super.list(query);
if (CollectionUtils.isEmpty(list)) {
return new ArrayList<>();
}
return new ArrayList<>(list
.stream()
.collect(Collectors.toMap(UserLoginOutDO::getUserId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(UserLoginOutDO::getLoginTime))))
.values());
}
}

@ -2,8 +2,11 @@ package cn.soul2.jyjc.admin.service;
import cn.soul2.jyjc.admin.dto.UserLoginDTO;
import cn.soul2.jyjc.admin.dto.UserLogoutDTO;
import cn.soul2.jyjc.admin.entity.UserLoginOutDO;
import cn.soul2.jyjc.admin.vo.UserVO;
import java.util.List;
/**
* @author Soul2
* @date 2024-03-29 20:47
@ -43,4 +46,11 @@ public interface IUserService {
*/
Boolean remove(String userId);
/**
* 重建当前登录的用户
*
* @return {@link List}<{@link UserLoginOutDO}>
*/
List<UserLoginOutDO> getLoginedUser();
}

@ -3,6 +3,7 @@ package cn.soul2.jyjc.admin.service.impl;
import cn.soul2.jyjc.admin.dto.UserLoginDTO;
import cn.soul2.jyjc.admin.dto.UserLogoutDTO;
import cn.soul2.jyjc.admin.entity.UserDO;
import cn.soul2.jyjc.admin.entity.UserLoginOutDO;
import cn.soul2.jyjc.admin.repository.IUserLoginOutRepository;
import cn.soul2.jyjc.admin.repository.IUserRepository;
import cn.soul2.jyjc.admin.service.IUserService;
@ -12,6 +13,8 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Soul2
* @date 2024-03-29 20:47
@ -84,4 +87,9 @@ public class UserServiceImpl implements IUserService {
loginOutRepository.removeByUserId(userId);
return userRepository.removeById(userId);
}
@Override
public List<UserLoginOutDO> getLoginedUser() {
return loginOutRepository.getLoginedUser();
}
}

@ -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) {
}

@ -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
* <p>照抄自 <a href="https://blog.csdn.net/shaoduo/article/details/122322578">Shao duo</a></p>
*/
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<String,String>();
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'
};
}

@ -1,7 +1,7 @@
# 允许跨域的地址
cors:
allow-origin: http://localhost
allow-origin: http://localhost:6100, http://localhost:7620
---
spring:

@ -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

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!--<include resource="org/springframework/boot/logging/logback/base.xml" />-->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<!-- <property name="log.path" value=".logs/"/>-->
<springProperty scope="context" name="log.path" source="logging.path" defaultValue="./logs/"/>
<property name="log.pattern"
value="%cyan(%d{HH:mm:ss}) %green(%-5level) %blue(%logger{50}) - %boldMagenta(%msg) \t\t ---- %yellow([%thread] %d{yyyy-MM-dd HH:mm:ss.SSS}) %n"
/>
<property name="debug.path" value="${log.path}/%d{yyyyMM,aux}/%d{dd,aux}/debug-%d{yyyyMMdd}.%i.log.gz"/>
<property name="info.path" value="${log.path}/%d{yyyyMM,aux}/%d{dd,aux}/info-%d{yyyyMMdd}.%i.log.gz"/>
<property name="error.path" value="${log.path}/%d{yyyyMM,aux}/%d{dd,aux}/error-%d{yyyyMMdd}.%i.log.gz"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 控制台输出的日志级别是大于或等于此级别的日志信息 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<!--日志文件输出格式与字符集-->
<encoder>
<Pattern>${log.pattern}</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件-->
<!-- 时间滚动输出 level为 DEBUG 日志 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/debug.log</file>
<!--日志文件输出格式与字符集-->
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志归档 -->
<fileNamePattern>${debug.path}</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>7</maxHistory>
</rollingPolicy>
<!-- 此日志文件记录debug级别以上的 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${info.path}</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>60</maxHistory>
</rollingPolicy>
<!-- 此日志文件记录info级别以上的 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${error.path}</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>60</maxHistory>
</rollingPolicy>
<!-- 此日志文件记录error级别以上的 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--开发环境:打印控制台-->
<springProfile name="local,dev,test">
<logger level="DEBUG" additivity="false" name="cn.soul2">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!--生产环境:输出到文件-->
<springProfile name="pre,prod,prod-yun">
<root level="INFO">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
</configuration>
Loading…
Cancel
Save