SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:日志管理(四)集成Spring Security
目录
一、前言
二、后端开发及调整
1.日志管理开发
2.配置调整
3.日志入库(注解、切面)
三、前端调整
1.日志管理开发
四、附:源码
1.源码下载地址
五、结语
一、前言
此文章在上次调整的基础上开发后端管理系统的用户请求日志功能,并集成了Spring Security用来替代jwt认证和缓存用户信息,以便于日志能记录详细的用户操作信息。新增日志管理菜单可视化日志信息。
此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下:SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:权限管理(三)
(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)
二、后端开发及调整
1.日志管理开发
1.新建用户操作日志表user_log
CREATE TABLE `user_log` (`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',`type` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作类型',`method` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作接口',`status` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作状态(0成功,1失败)',`text` text COLLATE utf8mb4_general_ci COMMENT '响应结果',`ip_address` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'ip地址',`user_agent` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '客户端信息',`user_id` int DEFAULT NULL COMMENT '操作员ID(用户id)',`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间/操作时间',`create_by` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人/操作员',`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`update_by` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',`del_flag` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '删除标识0未删除,1已删除(逻辑删除)',`remark` varchar(120) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=253 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户操作日志表'
2.新增用户操作日志 实体类UserLogEntity.java
import lombok.Data;/*** 用户操作日志表* @TableName user_log*/
@Data
public class UserLogEntity extends BaseEntity{/*** 主键*/private Integer id;/*** 操作类型*/private String type;/*** 操作接口*/private String method;/*** 操作状态(0成功,1失败)*/private String status;/*** 响应结果*/private String text;/*** ip地址*/private String ipAddress;/*** 客户端信息*/private String userAgent;/*** 操作员ID(用户id)*/private Integer userId;/*** 备注*/private String remark;
}
3.新增用户操作日志控制器类LogController.java
import org.springframework.beans.factory.annotation.Autowired;
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 org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;
import org.wal.userdemo.service.UserLogService;
import org.wal.userdemo.utils.Result;import java.util.List;/*** 日志控制器类* 处理用户日志相关的API请求*/
@RestController
@RequestMapping("/api/log")
public class LogController {@Autowiredprivate UserLogService userLogService;/*** 获取用户日志列表* 根据查询条件获取用户日志数据,支持分页功能** @param queryUserLogReq 用户日志查询请求参数对象,包含查询条件和分页信息* @return Result 返回封装的用户日志列表结果,包含数据列表和总记录数*/@PostMapping("/getUserLogList")public Result<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq) {// 查询用户日志列表数据List<UserLogEntity> userLogList = userLogService.getUserLogList(queryUserLogReq);// 获取符合条件的用户日志总记录数int total = userLogService.getUserLogCount(queryUserLogReq);return Result.page(userLogList, total);}@GetMapping("/getUserLogById")public Result<UserLogEntity> getUserLogById(Integer id) {// 返回用户日志数据return Result.success(userLogService.getUserLogById(id));}
}
4.新增用户操作日志服务类UserLogService.java
import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;import java.util.List;public interface UserLogService {int insert(UserLogEntity record);List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq);int getUserLogCount(QueryUserLogReq queryUserLogReq);UserLogEntity getUserLogById(Integer id);
}
5.新增用户操作日志实现类UserLogServiceImpl.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;
import org.wal.userdemo.mapper.UserLogMapper;
import org.wal.userdemo.service.UserLogService;import java.util.Collections;
import java.util.List;@Service
public class UserLogServiceImpl implements UserLogService {@Autowiredprivate UserLogMapper userLogMapper;@Overridepublic int insert(UserLogEntity record) {return userLogMapper.insert( record);}@Overridepublic List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq) {return userLogMapper.getUserLogList(queryUserLogReq);}@Overridepublic int getUserLogCount(QueryUserLogReq queryUserLogReq) {return userLogMapper.getUserLogCount(queryUserLogReq);}@Overridepublic UserLogEntity getUserLogById(Integer id) {return userLogMapper.getUserLogById(id);}
}
6.新增用户操作日志Mapper接口类UserLogMapper.java
import org.apache.ibatis.annotations.Mapper;
import org.wal.userdemo.DTO.req.QueryUserLogReq;
import org.wal.userdemo.entity.UserLogEntity;import java.util.List;@Mapper
public interface UserLogMapper {int insert(UserLogEntity record);List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq);int getUserLogCount(QueryUserLogReq queryUserLogReq);UserLogEntity getUserLogById(Integer id);
}
6.新增用户操作日志Mapper.xml文件UserLogMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wal.userdemo.mapper.UserLogMapper"><resultMap id="BaseResultMap" type="org.wal.userdemo.entity.UserLogEntity"><id property="id" column="id" /><result property="type" column="type" /><result property="method" column="method" /><result property="status" column="status" /><result property="text" column="text" /><result property="ipAddress" column="ip_address" /><result property="userAgent" column="user_agent" /><result property="userId" column="user_id" /><result property="createTime" column="create_time" /><result property="createBy" column="create_by" /><result property="updateTime" column="update_time" /><result property="updateBy" column="update_by" /><result property="delFlag" column="del_flag" /></resultMap><sql id="Base_Column_List">id,type,method,status,text,ip_address,user_agent,user_id,create_time,create_by,update_time,update_by,del_flag</sql><insert id="insert" parameterType="org.wal.userdemo.entity.UserLogEntity" >insert into user_log<trim prefix="(" suffix=")" suffixOverrides=","><if test="type != null">type,</if><if test="method != null">method,</if><if test="status != null">status,</if><if test="text != null">text,</if><if test="ipAddress != null">ip_address,</if><if test="userAgent != null">user_agent,</if><if test="userId != null">user_id,</if><if test="createTime != null">create_time,</if><if test="createBy != null">create_by,</if><if test="updateTime != null">update_time,</if><if test="updateBy != null">update_by,</if><if test="delFlag != null">del_flag,</if></trim><trim prefix="values (" suffix=")" suffixOverrides=","><if test="type != null">#{type},</if><if test="method != null">#{method},</if><if test="status != null">#{status},</if><if test="text != null">#{text},</if><if test="ipAddress != null">#{ipAddress},</if><if test="userAgent != null">#{userAgent},</if><if test="userId != null">#{userId},</if><if test="createTime != null">#{createTime},</if><if test="createBy != null">#{createBy},</if><if test="updateTime != null">#{updateTime},</if><if test="updateBy != null">#{updateBy},</if><if test="delFlag != null">#{delFlag},</if></trim></insert><select id="getUserLogList" resultMap="BaseResultMap">select<include refid="Base_Column_List"/>from user_log<where><if test="type != null">and type = #{type}</if><if test="status != null">and status = #{status}</if><if test="createTime != null">and create_time like CONCAT('%',#{createTime},'%')</if></where></select><select id="getUserLogCount" resultType="java.lang.Integer">select count(1) from user_log<where><if test="type != null">and type = #{type}</if><if test="status != null">and status = #{status}</if><if test="createTime != null">and create_time like CONCAT('%',#{createTime},'%')</if></where></select><select id="getUserLogById" resultMap="BaseResultMap" parameterType="integer">select <include refid="Base_Column_List"/>from user_logwhere id = #{id}</select></mapper>
2.配置调整
1.新增pom.xml依赖,支持SpringSecurity
<!-- Spring Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--常用工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency>
2.移除JwtInterceptor.java拦截器,改用Spring Security进行认证、在上下文缓存登录用户信息。
3.移除WebConfig.java配置的请求拦截器,改用Spring Security的config进行过滤和拦截。
4.新增LoginUser.java类,实现Spring Security的UserDetails,封装用户认证和授权信息。
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.wal.userdemo.entity.UserEntity;import java.util.Collection;
import java.util.Collections;/*** 登录用户类,实现Spring Security的UserDetails接口* 用于封装用户认证和授权信息*/
public class LoginUser implements UserDetails {private Integer userId;private String username;private String password;/*** 构造函数,根据UserEntity初始化LoginUser* @param user 用户实体对象,包含用户的基本信息*/public LoginUser(UserEntity user) {this.userId = user.getId();this.username = user.getName();this.password = user.getPassword();}/*** 获取用户ID* @return 用户ID*/public Integer getUserId() {return userId;}/*** 获取用户权限集合* @return 包含用户权限的集合,默认返回"USER"权限*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return Collections.singletonList(new SimpleGrantedAuthority("USER"));}/*** 获取用户密码* @return 用户密码*/@Overridepublic String getPassword() {return password;}/*** 获取用户名* @return 用户名*/@Overridepublic String getUsername() {return username;}// 实现其他UserDetails方法.../*** 判断账户是否未过期* @return true表示账户未过期*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 判断账户是否未锁定* @return true表示账户未锁定*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 判断凭证是否未过期* @return true表示凭证未过期*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 判断账户是否启用* @return true表示账户已启用*/@Overridepublic boolean isEnabled() {return true;}
}
5. 新增JwtAuthenticationTokenFilter.java ,jwt认证过滤器,验证jwt令牌的有效性,有效则解析令牌信息中的用户信息并设置Spring Security的认证上下文。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;
import org.wal.userdemo.utils.JwtUtil;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** JWT认证过滤器,用于拦截请求并验证JWT令牌的有效性。* 如果令牌有效,则从令牌中解析用户信息,并设置Spring Security的认证上下文。*/
@Component
//@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate UserMapper userMapper;/*** 执行JWT认证的核心逻辑。* 该方法会在每个HTTP请求中执行一次,检查请求头中的Authorization字段是否包含有效的JWT令牌。** @param request HTTP请求对象* @param response HTTP响应对象* @param chain 过滤器链,用于继续执行后续过滤器* @throws ServletException 当Servlet处理出现异常时抛出* @throws IOException 当IO操作出现异常时抛出*/@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {// 获取请求头中的Authorization字段String authHeader = request.getHeader("Authorization");// 判断是否存在Bearer类型的JWT令牌if (authHeader != null && authHeader.startsWith("Bearer ")) {// 提取JWT令牌(去除Bearer前缀)String token = authHeader.substring(7);// 验证JWT令牌是否有效if (jwtUtil.validateToken(token)) {try {// 解析JWT令牌中的用户IDString userId = jwtUtil.parseUserId(token);// 根据用户ID查询用户信息UserEntity user = userMapper.getUserById(Integer.parseInt(userId));// 加载用户详细信息UserDetails userDetails = userDetailsService.loadUserByUsername(user.getName());// 创建认证对象UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 设置认证详情authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将认证信息存入安全上下文SecurityContextHolder.getContext().setAuthentication(authentication);} catch (Exception e) {// 记录JWT验证过程中的异常信息logger.error("JWT验证异常:", e);}}}// 继续执行过滤器链chain.doFilter(request, response);}
}
6.新增UserDetailsConfig.java类,实现Spring Security的UserDetailsService,用于加载用户详细信息进行认证
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;/*** 用户详情配置类,实现Spring Security的UserDetailsService接口* 用于加载用户详细信息进行认证*/
@Service
public class UserDetailsConfig implements UserDetailsService {@Autowiredprivate UserMapper userMapper;/*** 根据用户名加载用户详细信息* @param name 用户名* @return UserDetails 用户详细信息对象* @throws UsernameNotFoundException 当用户不存在时抛出此异常*/@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {// 通过用户名查询用户信息UserEntity user = userMapper.getUserByName(name);// 如果用户不存在,抛出用户名未找到异常if (user == null) {throw new UsernameNotFoundException("用户不存在");}// 将用户实体转换为登录用户对象并返回return new LoginUser(user);}
}
7.新增SecurityConfig.java安全配置类,配置用户认证、JWT过滤等相关安全组件。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.wal.userdemo.Filter.JwtAuthenticationTokenFilter;/*** Spring Security安全配置类* 配置用户认证、权限控制、JWT过滤器等安全相关组件*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** 创建密码编码器Bean* 使用BCrypt算法对密码进行加密处理** @return PasswordEncoder 密码编码器实例*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置认证管理器构建器* 设置自定义用户详情服务和密码编码器** @param auth AuthenticationManagerBuilder认证管理器构建器* @throws Exception 配置过程中可能抛出的异常*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** 暴露AuthenticationManager作为Bean* 用于在应用其他地方进行手动认证操作** @return AuthenticationManager 认证管理器实例* @throws Exception 获取认证管理器时可能抛出的异常*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 配置HTTP安全策略* 包括CSRF禁用、会话管理、URL权限控制和JWT过滤器添加** @param http HttpSecurity HTTP安全配置构建器* @throws Exception 配置过程中可能抛出的异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 禁用CSRF保护,配置无状态会话管理http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 配置URL访问权限.authorizeRequests()// 登录接口允许所有人访问.antMatchers("/api/auth/login").permitAll()// API接口需要认证后访问.antMatchers("/api/**").authenticated()// 其他所有请求都需要认证.anyRequest().authenticated();// 在用户名密码认证过滤器之前添加JWT认证过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}
}
8.重写LoginController.java的登录接口进行身份验证
/*** 用户登录接口* 通过用户名和密码进行身份验证,验证成功后生成JWT token返回** @param request 登录请求参数对象,包含用户名和密码* @return Result<?> 返回登录结果,成功时返回JWT token,失败时返回错误信息*/@UserLog("登录接口")@PostMapping("/login")public Result<?> login(@RequestBody LoginReq request) {try {// 使用Spring Security进行用户身份验证Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));// 将认证结果存储到安全上下文中SecurityContextHolder.getContext().setAuthentication(authentication);// 从认证结果中获取用户信息String username = authentication.getName();UserEntity user = userService.getUserByName(username);if (user != null) {// 生成JWT token并返回String token = jwtUtil.generateToken(user.getId());return Result.success(token);} else {return Result.error("用户不存在");}} catch (Exception e) {e.printStackTrace();return Result.error("用户名或密码错误2");}}
9.重写JwtUtil.java工具类,固定签名秘钥, 新增校验Jwt令牌方法。
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;import java.security.Key;
import java.util.Date;
/*** JwtUtil 是一个工具类,用于生成和解析 JWT(JSON Web Token)。* 主要功能包括:* - 生成带有用户名和过期时间的 JWT 令牌* - 从 JWT 令牌中解析出用户名** 注意事项:* - 密钥(SECRET_KEY)应通过配置文件管理,避免硬编码* - 过期时间(EXPIRATION)可按业务需求调整*/
@Component
public class JwtUtil {/*** JWT 签名所使用的密钥。* 在生产环境中建议使用更安全的方式存储,如配置中心或环境变量。*/private static final String SECRET = "myFixedSecretKey12345678901234567890";public static final Key SIGNING_KEY = Keys.hmacShaKeyFor(SECRET.getBytes());/*** JWT 令牌的有效期,单位为毫秒。* 当前设置为 24 小时(86,400,000 毫秒)。*/private static final long EXPIRATION = 86400000; // 24小时/*** 生成 JWT 令牌。** @param userId 用户id,作为 JWT 的 subject 字段* @return 返回生成的 JWT 字符串*/public static String generateToken(Integer userId) {return Jwts.builder()// 设置 JWT 的主题(通常为用户标识).setSubject(userId.toString())// 设置 JWT 的过期时间.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))// 使用 HS512 算法签名,并指定密钥.signWith(SIGNING_KEY)// 构建并返回紧凑格式的 JWT 字符串.compact();}/*** 从 JWT 令牌中解析出用户ID。** @param token 需要解析的 JWT 字符串* @return 解析出的用户ID(subject)* @throws JwtException 如果 token 无效或签名不匹配会抛出异常*/public static String parseUserId(String token) {return Jwts.parser()// 设置签名验证所使用的密钥.setSigningKey(SIGNING_KEY)// 解析并验证 JWT 令牌.parseClaimsJws(token)// 获取 JWT 中的负载(claims),并提取 subject(用户名).getBody().getSubject();}/*** 验证 JWT 令牌。** @param token 需要验证的 JWT 令牌* @return 如果令牌有效则返回 true,否则返回 false*/public boolean validateToken(String token) {try {Jwts.parserBuilder().setSigningKey(SIGNING_KEY).build().parseClaimsJws(token);return true;} catch (JwtException | IllegalArgumentException e) {return false;}}}
10. 新增PasswordUtil.java工具类,提供密码加密和验证功能。
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;/*** 密码工具类* 提供密码加密和验证功能*/
@Component
public class PasswordUtil {/*** 密码编码器实例* 使用BCrypt算法进行密码加密*/private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();/*** 加密密码* @param rawPassword 明文密码* @return 加密后的密码*/public static String encode(String rawPassword) {return passwordEncoder.encode(rawPassword);}/*** 验证密码* @param rawPassword 明文密码* @param encodedPassword 加密后的密码* @return 是否匹配*/public static boolean matches(String rawPassword, String encodedPassword) {return passwordEncoder.matches(rawPassword, encodedPassword);}
}
11.新增UserContextUtil.java用户上下文工具类,提供获取当前登录用户信息的功能。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.wal.userdemo.config.LoginUser;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;/*** 用户上下文工具类* 提供获取当前登录用户信息的便捷方法*/
@Component
public class UserContextUtil {@Autowiredprivate UserMapper userMapper;/*** 获取当前登录用户信息* 从Spring Security上下文中获取认证信息,解析出登录用户名称,* 然后通过用户Mapper查询完整的用户实体信息* @return 当前用户实体,如果未登录或发生异常则返回null*/public UserEntity getCurrentUser() {try {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {LoginUser loginUser = (LoginUser) authentication.getPrincipal();return userMapper.getUserByName(loginUser.getUsername());}} catch (Exception e) {// 记录日志e.printStackTrace();}return null;}/*** 获取当前登录用户名* 通过获取当前用户实体来提取用户名信息* @return 当前用户名,如果未登录则返回null*/public String getUsername() {UserEntity user = getCurrentUser();return user != null ? user.getName() : null;}/*** 获取当前登录用户ID* 通过获取当前用户实体来提取用户ID信息* @return 当前用户ID,如果未登录则返回null*/public Integer getUserId() {UserEntity user = getCurrentUser();return user != null ? user.getId() : null;}
}
12.可以在任意处调用当前登录用户信息,例如,新增用户、修改菜单等。
/*** 新增 用户** @param userEntity* @return Integer*/@Overridepublic Integer addUser(UserEntity userEntity) {userEntity.setCreateTime(DateUtils.getNowDate());userEntity.setCreateBy(userContextUtil.getUsername());// 添加用户前加密密码if (null != userEntity.getPassword() && !"".equals(userEntity.getPassword())) {userEntity.setPassword(PasswordUtil.encode(userEntity.getPassword()));}return userMapper.addUser(userEntity);}
3.日志入库(注解、切面)
1.新增UserLog.java自定义用户操作日志注解。
import java.lang.annotation.*;@Target(ElementType.METHOD) // 注解用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效
@Documented
public @interface UserLog {String value() default "";//操作接口描述
}
2.新增 UserLogAspect.java用户操作日志切面。
import com.fasterxml.jackson.databind.ObjectMapper;
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.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.wal.userdemo.annotation.UserLog;
import org.wal.userdemo.config.LoginUser;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.entity.UserLogEntity;
import org.wal.userdemo.mapper.UserMapper;
import org.wal.userdemo.service.UserLogService;
import org.wal.userdemo.utils.DateUtils;
import org.wal.userdemo.utils.JwtUtil;
import org.wal.userdemo.utils.Result;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;/*** 用户操作日志切面* 拦截带有 @UserLog 注解的方法,记录用户操作日志。* @author wal* @Date 2025年7月23日17:21:06*/
@Slf4j
@Aspect
@Component
public class UserLogAspect {@Autowiredprivate UserLogService userLogService; // 假设有一个服务类用于保存日志@Autowiredprivate UserMapper userMapper;/*** 定义切入点:匹配所有带有 @UserLog 注解的方法*/@Pointcut("@annotation(org.wal.userdemo.annotation.UserLog)")public void userLog() {}/*** 环绕通知:在目标方法执行前后进行日志记录* @param joinPoint 连接点对象,包含目标方法的信息* @return 目标方法的返回值* @throws Throwable 目标方法可能抛出的异常*/@Around("userLog()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {// 先执行目标方法Object result = null;Exception exception = null;try {result = joinPoint.proceed();} catch (Exception e) {exception = e;throw e;} finally {// 在 finally 块中记录日志,此时认证应该已完成try {recordLog(joinPoint, result, exception);} catch (Exception e) {log.error("记录操作日志失败", e);}}return result;}/*** 记录用户操作日志的核心逻辑* 提取方法信息、请求信息、用户信息,并构建 UserLogEntity 对象保存到数据库* @param joinPoint 连接点对象,包含目标方法的信息* @param result 目标方法的返回结果* @param exception 目标方法抛出的异常(如果有的话)*/private void recordLog(ProceedingJoinPoint joinPoint, Object result, Exception exception) {// 获取方法签名MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();// 获取 @UserLog 注解的值String remark = getUserLogRemark(method);String type = getHttpMethodFromMethod(method);// 获取请求信息ServletRequestAttributes attributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = null;if (attributes != null) {request = attributes.getRequest();}// 获取当前用户信息UserEntity currentUser = getCurrentUser();String methodUrl = null;String ipAddress = null;String userAgent = null;if (request != null) {methodUrl = request.getMethod() + " " + request.getRequestURI();ipAddress = request.getRemoteAddr();userAgent = request.getHeader("User-Agent");}String status = "0";String text = "";if (exception != null) {status = "1";text = "异常信息:" + exception.getMessage();} else if (result instanceof Result<?>) {Result<?> resultObj = (Result<?>) result;int code = resultObj.getCode();status = (code == 200) ? "0" : "1";try {text = new ObjectMapper().writeValueAsString(result);} catch (Exception e) {text = result.toString();}}// 构建并保存日志实体UserLogEntity userLog = new UserLogEntity();userLog.setType(type);userLog.setMethod(methodUrl);userLog.setStatus(status);userLog.setText(text);userLog.setIpAddress(ipAddress);userLog.setUserAgent(userAgent);if (currentUser != null) {userLog.setUserId(currentUser.getId());userLog.setCreateBy(currentUser.getName());userLog.setCreateTime(DateUtils.getNowDate());}userLog.setCreateTime(new Date());userLog.setDelFlag(0);userLog.setRemark(remark);userLogService.insert(userLog);}/*** 获取 @UserLog 注解中的 remark 值* @param method 目标方法* @return 注解中的值*/private String getUserLogRemark(Method method) {UserLog userLog = method.getAnnotation(UserLog.class);if (userLog != null) {return userLog.value(); // 获取注解中的值}return ""; // 如果没有注解,返回空字符串}/*** 获取当前登录用户信息* 支持从 Spring Security 上下文或 JWT Token 中解析用户信息* @return 当前用户实体,若未找到则返回 null*/private UserEntity getCurrentUser() {HttpServletRequest request = getCurrentRequest();// 方式1:尝试从安全上下文获取UserEntity user = getCurrentUserFromSecurityContext();if (user != null) {return user;}// 方式2:从请求头的 token 解析if (request != null) {user = getCurrentUserFromToken(request);if (user != null) {return user;}}return null;}/*** 获取当前 HTTP 请求对象* @return 当前请求对象,若不存在则返回 null*/private HttpServletRequest getCurrentRequest() {ServletRequestAttributes attributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();return attributes != null ? attributes.getRequest() : null;}/*** 从 Spring Security 上下文中获取当前用户信息* @return 当前用户实体,若未找到则返回 null*/private UserEntity getCurrentUserFromSecurityContext() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {LoginUser loginUser = (LoginUser) authentication.getPrincipal();return userMapper.getUserByName(loginUser.getUsername());}log.error("未找到当前用户信息");return null;}/*** 根据方法上的注解判断 HTTP 请求方法类型* @param method 目标方法* @return HTTP 方法类型字符串(如 GET、POST 等)*/public static String getHttpMethodFromMethod(Method method) {if (method.isAnnotationPresent(GetMapping.class)) {return "GET";} else if (method.isAnnotationPresent(PostMapping.class)) {return "POST";} else if (method.isAnnotationPresent(PutMapping.class)) {return "PUT";} else if (method.isAnnotationPresent(DeleteMapping.class)) {return "DELETE";} else if (method.isAnnotationPresent(PatchMapping.class)) {return "PATCH";} else if (method.isAnnotationPresent(RequestMapping.class)) {return "REQUEST";}return "UNKNOWN";}/*** 从请求头的 JWT Token 中解析当前用户信息* @param request 当前 HTTP 请求对象* @return 当前用户实体,若解析失败或未找到则返回 null*/private UserEntity getCurrentUserFromToken(HttpServletRequest request) {if (request == null) return null;String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {token = token.substring(7);try {String userId = JwtUtil.parseUserId(token);return userMapper.getUserById(Integer.parseInt(userId));} catch (Exception e) {log.warn("解析Token失败: {}", e.getMessage());return null;}}log.error("未找到Token");return null;}}
3.日志切面实现(以LoginController.java登录接口为例),在需要实现日志记录的接口上加上@UserLog(“接口描述”)即可实现访问该接口时记录日志。
@UserLog("登录接口")@PostMapping("/login")public Result<?> login(@RequestBody LoginReq request) {try {// 使用Spring Security进行用户身份验证Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));// 将认证结果存储到安全上下文中SecurityContextHolder.getContext().setAuthentication(authentication);// 从认证结果中获取用户信息String username = authentication.getName();UserEntity user = userService.getUserByName(username);if (user != null) {// 生成JWT token并返回String token = jwtUtil.generateToken(user.getId());return Result.success(token);} else {return Result.error("用户不存在");}} catch (Exception e) {e.printStackTrace();return Result.error("用户名或密码错误");}}
三、前端调整
1.日志管理开发
1.router路由,新增log.js日志管理=>操作日志
export default [{path: 'log',name: 'log',component: () => import('@/view/logmanage/log.vue'),meta: { title: '操作日志', requiresAuth: true }}]
2.请求封装,新增log.js封装日志请求
import request from '@/utils/request';export function getUserLogList(data) {return request({url: '/log/getUserLogList',method: 'post',data: data,});
}
export function getUserLogById(id){return request({url: '/log/getUserLogById',method: 'get',params: {id: id},});
}
3.在router/index.js中引入路由log.js到index路由下
//省略部分导入import log from './log.js'
Vue.use(Router)const router = new Router({mode: 'history',routes: [{path: '/',name: 'Index',component: Index,redirect: '/login', // 默认重定向到 /homechildren: [{path: '/home',name: 'home',component: () => import('@/view/home.vue'),meta: { title: '首页', requiresAuth: true }},// 其他子路由也可以放在这里...permission,...log]},
// 其他路由配置、、、、、、、
4.新增log.vue实现用户操作日志可视化
<template><div><!-- 查询条件 --><el-form :inline="true" label-position="right" label-width="80px" :model="queryForm"class="query-border-container"><el-row :gutter="20" justify="center"><!-- 操作类型 --><el-col :span="7"><el-form-item label="操作类型"><el-input v-model="queryForm.type" placeholder="请选择操作类型"></el-input></el-form-item></el-col><!-- 操作状态 --><el-col :span="7"><el-form-item label="操作状态"><el-input v-model="queryForm.status" placeholder="请选择操作状态"></el-input></el-form-item></el-col><!-- 操作时间 --><el-col :span="7"><el-form-item label="操作时间"><el-date-picker v-model="queryForm.createTime" type="date" placeholder="选择操作时间"style="width: 100%;"></el-date-picker></el-form-item></el-col><!-- 按钮组 --><el-col :span="7"><el-form-item><div style="display: flex; gap: 10px;"><el-button type="primary" @click="onQuery" size="small">查询</el-button><el-button @click="onReset" size="small">重置</el-button></div></el-form-item></el-col></el-row></el-form><!-- 用户列表 --><el-table :data="tableData" style="width: 100%;" class="table-border-container" max-height="480"v-loading="loading"><el-table-column type="index" label="序号" width="100" align="center"><template #default="scope">{{ (queryForm.page - 1) * queryForm.limit + scope.$index + 1 }}</template></el-table-column><el-table-column prop="type" label="操作类型" width="180" align="center"></el-table-column><el-table-column prop="method" label="操作接口" width="180" align="center"></el-table-column><el-table-column prop="status" label="操作状态" width="180" align="center"></el-table-column><el-table-column prop="text" label="响应结果" width="180" align="center"><template #default="scope"><el-tooltip :content="scope.row.text" placement="top":disabled="!scope.row.text || scope.row.text.length <= 20"><span class="ellipsis-text">{{ scope.row.text && scope.row.text.length > 20 ?scope.row.text.substring(0, 20) + '...' : scope.row.text }}</span></el-tooltip></template></el-table-column><el-table-column prop="userAgent" label="客户端信息" width="180" align="center"><template #default="scope"><el-tooltip :content="scope.row.userAgent" placement="top":disabled="!scope.row.userAgent || scope.row.userAgent.length <= 20"><span class="ellipsis-text">{{ scope.row.userAgent && scope.row.userAgent.length > 20 ?scope.row.userAgent.substring(0, 20) + '...' : scope.row.userAgent }}</span></el-tooltip></template></el-table-column><el-table-column prop="createTime" label="操作时间" width="180" align="center"></el-table-column><el-table-column prop="createBy" label="操作用户" width="180" align="center"></el-table-column><el-table-column label="操作" width="350" align="center"><template #default="scope"><el-button type="primary" size="small" @click="details(scope.$index, scope.row)">详情</el-button></template></el-table-column></el-table><!-- 分页 --><el-pagination background layout="total,sizes,prev, pager, next" :total="total" @size-change="handleSizeChange"@current-change="handleCurrentChange" :page-size.sync="queryForm.limit" :page-sizes="[10, 20, 50, 100]"class="page-border-container"></el-pagination></div></template><script>
import { getUserLogList, getUserLogById } from '@/api/logmanage/log';export default {name: 'logView',data() {return {tableData: [],queryForm: {page: 1,limit: 10,type: '',status: '',createTime: '',},total: 0,loading: false,form: {},};},created() {this.getUserLogList();},methods: {// 获取日志列表getUserLogList() {this.loading = true;getUserLogList(this.queryForm).then(res => {if (res.data.code == 200) {this.tableData = res.data.data;this.total = res.data.total;this.$message.success("获取日志列表成功!");} else {this.$message.error("获取日志列表失败!");}}).finally(() => {this.loading = false;});},// 查询onQuery() {this.getUserLogList();},// 重置表单并查询onReset() {this.queryForm = {page: 1,limit: 10,type: '',status: '',createTime: '',};this.getUserLogList();},details(index, row) {getUserLogById(row.id).then(res => {if (res.data.code == 200) {// this.form = res.data.data;// this.showUser = true;} else {this.$message.error("获取用户信息失败!");}});},//分页器改变handleSizeChange(val) {this.queryForm.limit = val;this.getUserLogList();},//改变页码handleCurrentChange(val) {this.queryForm.page = val;this.getUserLogList();},// 取消按钮:关闭弹窗closeRoleDialog() {this.userId = null;this.selectedRoles = [];this.showRoleDialog = false;},},
};
</script>
<style scoped></style>
四、附:源码
1.源码下载地址
https://gitee.com/wangaolin/user-demo.git
同学们有需要可以自行下载查看,此文章是dev-vue分支
五、结语
此次开发新引入了Spring Security用来认证和鉴权(没用到),近期工作较忙,近期不再发版,但是项目我还会继续维护,确确实实还有多少我觉得不合理、不够人性化的地方。
后续可能还会加的功能如下:
- 用户管理的头像、邮箱、电话、水印、信息加密等
- 菜单管理的支持拖拽、过滤等
- 日志管理细化、分类
- 完善首页的数据展示、优化等
有需要的同学可以关注我后续发布的文章和代码维护。
(注:接定制化开发前后端分离项目,私我)