先上效果图
第一步、导入需要用到的包
eu.bitwalker
UserAgentUtils
1.21
第二步、自定义注解Log
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 编辑的表主键
* @return
*/
String editTableId() default "id";
/**
* 编辑的表名称
* @return
*/
String editTableName() default "未知";
}
第三步、切面类
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sifan.erp.domain.LogDO;
import com.sifan.erp.mapper.SysLogMapper;
import com.sifan.erp.utils.CommonUtil;
import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class LogAspect {
@Resource
private SysLogMapper logMapper;
@Value("${spring.application.name}")
private String appNname;
//定义切入点(有注解@Log的方法为切入点)
@Pointcut("@annotation(com.sifan.erp.annotation.Log)")
public void logPoint() {}
@Around("logPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Object result = null;
LogDO logDO = new LogDO();
try {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
//浏览器对象
Browser browser = userAgent.getBrowser();
//操作系统对象
OperatingSystem operatingSystem = userAgent.getOperatingSystem();
logDO.setBrowserName(browser.getName());
ApiOperation aon = methodSignature.getMethod().getAnnotation(ApiOperation.class);
if (aon != null) {
logDO.setModuleName(aon.value());
}
logDO.setOsName(operatingSystem.getName());
logDO.setIpAddr(CommonUtil.getIpAddr(request));
logDO.setAppName(appNname);
logDO.setClassName(joinPoint.getTarget().getClass().getName());
logDO.setMethodName(methodSignature.getMethod().getName());
logDO.setRequestUrl(request.getRequestURI());
logDO.setRequestMethod(request.getMethod());
//获取请求参数
CommonUtil.getRequestParam(joinPoint, methodSignature, logDO);
logDO.setResultText(new ObjectMapper().writeValueAsString(result));
logDO.setStatus(0);
logDO.setCreateTime(CommonUtil.getCurrentDate());
logDO.setCreateUserId(CommonUtil.getCurrentUserId());
logDO.setCreatePhoneNumber(CommonUtil.getCurrentPhoneNumber());
logDO.setCreateUserName(CommonUtil.getCurrentUserName());
long startTime = System.currentTimeMillis();
//方法执行
result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
logDO.setTakeUpTime(String.format("耗时:%s 毫秒", endTime - startTime));
if(result != null){
logDO.setResultText(result.toString());
}
} catch (Exception e) {
log.error("记录用户日志信息出现异常 {}",e);
logDO.setStatus(1);
logDO.setErrorText(e.toString());
log.info("Controller出现异常移除ThreadLocal中的token:{}", UserUtil.getLoginUser());
UserUtil.removeUser();
result = e.toString();
} finally {
logMapper.insert(logDO);
log.info("用户 {} 日志插入成功",logDO.getCreateUserId());
}
return result;
}
}
第四步、建表,表可以根据自己需求增减字段
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`module_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '模块名称',
`browser_name` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '浏览器名称',
`os_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作系统名称',
`ip_addr` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求ip',
`app_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '服务名称',
`class_name` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类名',
`method_name` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '方法',
`request_url` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求url',
`request_method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求方式,POST、GET',
`request_param` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求参数',
`result_text` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '响应参数',
`status` tinyint(1) NULL DEFAULT NULL COMMENT '接口状态(0成功 1失败)',
`error_text` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '错误信息',
`take_up_time` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '耗时',
`edit_table_id` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '编辑的表主键,只有修改时才有值',
`edit_table_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '编辑的表名称,只有修改时才有值',
`create_time` datetime NULL DEFAULT NULL COMMENT '操作时间',
`create_user_id` int(11) NULL DEFAULT NULL COMMENT '创建人id',
`create_phone_number` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人手机号',
`create_user_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人姓名',
`seller_account_id` int(11) NULL DEFAULT NULL COMMENT '店铺id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系统操作日志' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
第五步、实体类
idea使用mybatisx插件自动生成实体类
config name 这里只留下mapperInterface,点击Finish就搞定了。
第六步、工具类UserUtil
这个工具类的作用就是保存用户信息。
package com.sifan.erp.utils;
import com.sifan.erp.domain.User;
/**
* 存储/获取当前线程的用户信息工具类
*/
public abstract class UserUtil {
//线程变量,存放user实体类信息,即使是静态的与其他线程也是隔离的
private static ThreadLocal userThreadLocal = new ThreadLocal();
//从当前线程变量中获取用户信息
public static User getLoginUser() {
User user = userThreadLocal.get();
return user;
}
/**
* 获取当前登录用户的ID
* 未登录返回null
*
* @return
*/
public static Integer getLoginUserId() {
User user = userThreadLocal.get();
if (user != null && user.getId() != null) {
return user.getId();
}
return null;
}
//为当前的线程变量赋值上用户信息
public static void setLoginUser(User user) {
userThreadLocal.set(user);
}
//清除线程变量
public static void removeUser() {
userThreadLocal.remove();
}
}
第七步、工具类CommonUtil
package com.sifan.erp.utils;
import com.sifan.erp.annotation.Log;
import com.sifan.erp.domain.LogDO;
import com.sifan.erp.domain.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
/**
* 公共工具类
*/
public class CommonUtil {
private static final Logger log = LoggerFactory.getLogger(CommonUtil.class);
/**
* 获取ip
*
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.info("ip获取失败");
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
/**
* 获取当前时间戳
*
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis();
}
/**
* 获取当前日期
*
* @return
*/
public static Date getCurrentDate() {
return new Date();
}
/**
* 获取当前操作用户的主键id
*
* @return
*/
public static Integer getCurrentUserId() {
User loginUser = getLoginUser();
if (loginUser == null) {
return null;
}
return loginUser.getId();
}
private static User getLoginUser() {
return UserUtil.getLoginUser();
}
/**
* 获取当前操作用户的手机号
*
* @return
*/
public static String getCurrentPhoneNumber() {
User loginUser = getLoginUser();
if (loginUser == null) {
return null;
}
return loginUser.getPhoneNumber();
}
/**
* 获取当前操作用户的名称
*
* @return
*/
public static String getCurrentUserName() {
User loginUser = getLoginUser();
if (loginUser == null) {
return null;
}
return loginUser.getUsername();
}
/**
* 判断当前用户是否管理员
*/
public static boolean isAdmin() {
return getLoginUser().getUserRole() == 1;
}
/**
* 获取请求参数
*
* @param joinPoint 切入点
* @param signature 方法签名
* @param logDO 日志对象
*/
public static void getRequestParam(ProceedingJoinPoint joinPoint, MethodSignature signature, LogDO logDO) {
// 参数值
Object[] args = joinPoint.getArgs();
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
Method method = signature.getMethod();
String[] parameterNames = pnd.getParameterNames(method);
Map paramMap = new HashMap<>(32);
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
//获取类的字节码对象,通过字节码对象获取方法信息
Class> targetCls=joinPoint.getTarget().getClass();
try {
//获取目标方法上的注解指定的操作名称
Method targetMethod=
targetCls.getDeclaredMethod(signature.getName(), signature.getParameterTypes());
//获取方法上的@Log注解
Log requiredLog=
targetMethod.getAnnotation(Log.class);
//获取注解中的值
String editTableId=requiredLog.editTableId();
String editTableName = requiredLog.editTableName();
logDO.setEditTableId(editTableId);
logDO.setEditTableName(editTableName);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
logDO.setRequestParam(paramMap.toString());
}
}
RedisUtil
package com.sifan.erp.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Resource
private RedisTemplate redisTemplate;
/**
* 给一个指定的 key 值附加过期时间
*
* @param key
* @param time
* @return
*/
public boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
/**
* 根据key 获取过期时间
*
* @param key
* @return
*/
public long getTime(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 根据key 获取过期时间
*
* @param key
* @return
*/
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 移除指定key 的过期时间
*
* @param key
* @return
*/
public boolean persist(String key) {
return redisTemplate.boundValueOps(key).persist();
}
//- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - -
/**
* 根据key获取值
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 将值放入缓存
*
* @param key 键
* @param value 值
* @return true成功 false 失败
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 将对象放入到redis中
* @param key 键
* @param value 值
* @param time 时间(秒) -1为无期限
*/
public void setObject(String key, Object value,long time) {
redisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS);
}
/**
* 将值放入缓存并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) -1为无期限
* @return true成功 false 失败
*/
public void set(String key, String value, long time) {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, value);
}
}
/**
* 批量添加 key (重复的键会覆盖)
*
* @param keyAndValue
*/
public void batchSet(Map keyAndValue) {
redisTemplate.opsForValue().multiSet(keyAndValue);
}
/**
* 批量添加 key-value 只有在键不存在时,才添加
* map 中只要有一个key存在,则全部不添加
*
* @param keyAndValue
*/
public void batchSetIfAbsent(Map keyAndValue) {
redisTemplate.opsForValue().multiSetIfAbsent(keyAndValue);
}
/**
* 对一个 key-value 的值进行加减操作,
* 如果该 key 不存在 将创建一个key 并赋值该 number
* 如果 key 存在,但 value 不是长整型 ,将报错
*
* @param key
* @param number
*/
public Long increment(String key, long number) {
return redisTemplate.opsForValue().increment(key, number);
}
/**
* 对一个 key-value 的值进行加减操作,
* 如果该 key 不存在 将创建一个key 并赋值该 number
* 如果 key 存在,但 value 不是 纯数字 ,将报错
*
* @param key
* @param number
*/
public Double increment(String key, double number) {
return redisTemplate.opsForValue().increment(key, number);
}
//- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - -
/**
* 将数据放入set缓存
*
* @param key 键
* @return
*/
public void sSet(String key, String value) {
redisTemplate.opsForSet().add(key, value);
}
/**
* 获取变量中的值
*
* @param key 键
* @return
*/
public Set
第八步、过滤器
现在UserUtil中的ThreadLocal是没有用户信息的,需要一个过滤器拿到token,获取到用户信息设置到ThreadLocal,我这个只能从请求头和请求参数中获取token,如果需要从请求体获取token的话需要自己实现。
package com.sifan.erp.filter;
import com.sifan.erp.domain.User;
import com.sifan.erp.utils.RedisUtil;
import com.sifan.erp.utils.UserUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* * 将请求头或请求参数中的token对应的用户对象存到ThreadLocal中
*/
@Component
@WebFilter(filterName = "UserFilter",urlPatterns = {"/*"})
public class TokenFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(TokenFilter.class);
@Resource
private RedisUtil redisUtil;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = null;
//获取请求头中的token
String headerToken = request.getHeader("token");
//获取请求参数中的token
String paramToken = request.getParameter("token");
if(headerToken!= null){
token = headerToken;
}else if(paramToken!=null){
token = paramToken;
}
Object obj = redisUtil.get(token);
User user = null;
if(obj != null){
user = (User)obj;
}
log.info("过滤器中得到的用户的用户:{}",user);
UserUtil.setLoginUser(user);
filterChain.doFilter(servletRequest, servletResponse);
}
}
第九步、拦截器UserLoginInterceptor
这个拦截器的作用一是检查过滤器是否获取到用户信息,如果没有,则是未登录,不让访问接口。
作用二是接口执行完毕移除用户信息,避免内存泄漏
package com.sifan.erp.interceptor;
import com.sifan.erp.domain.User;
import com.sifan.erp.utils.UserUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserLoginInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(UserLoginInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//这里可以直接在拦截器中再次获取用户信息
User user = UserUtil.getLoginUser();
if (user == null || user.getId() == null) {
response.setStatus(401);
log.info("用户没有登录");
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//controller方法执行完毕
UserUtil.removeUser(); //其中调用的就是 userThreadLocal.remove();
}
}
注意:Controller执行过程中发生异常可能导致afterCompletion方法没执行,所以需要写一个全局异常处理器来移除ThreadLocal中的用户。这是没加Log注解的Controller方法会走地方,加了Log注解的在切面方法会catch到异常,哪个地方也需要移除用户。
import com.sifan.erp.utils.UserUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public String handler(RuntimeException exception) {
log.error(exception.getMessage());
log.info("Controller出现异常移除ThreadLocal中的token");
UserUtil.removeUser();
return "500";
}
}
第十步、配置拦截器
未登录就能访问的接口也需要自己移除。
package com.sifan.erp.config;
import com.sifan.erp.interceptor.UserLoginInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
/**
* 配置拦截器*
*
* @param interceptorRegistry
*/
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
InterceptorRegistration registry = interceptorRegistry.addInterceptor(new UserLoginInterceptor());
//拦截所有请求,根据自己的需求配置
registry.addPathPatterns("/**")
.excludePathPatterns("/user/login", "/favicon.ico", "/error", "/", "/index", "/login", "/user/register",
"/views/static/css/**", "/views/static/font/**", "/views/static/images/**", "/views/static/js/**");
}
/**
* 配置静态资源路径,这里是静态资源位置,不需要的删掉即可*
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/views/static/**").addResourceLocations("classpath:/views/static/");
}
}
第十一步、测试
只需要在使用的方法上面加注解@Log
@ApiOperation(value = "登录接口")这里写接口的名称即可
package com.sifan.erp.controller;
import com.sifan.erp.annotation.Log;
import com.sifan.erp.domain.User;
import com.sifan.erp.service.UserService;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Resource
private UserService userService;
@Log(editTableId = "id",editTableName = "user")
@ApiOperation(value = "登录接口")
@PostMapping("/login")
public String login(User loginUser){
log.info("登录的用户信息:{}",loginUser);
String token = userService.login(loginUser);
return token;
}
@Log(editTableId = "id",editTableName = "user")
@ApiOperation(value = "登出接口")
@GetMapping("/logout")
public void logout(String token){
userService.logout(token);
log.info("登出成功");
}
}