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

sql 复制代码
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

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

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 {
    @Autowired
    private 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

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

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 {
    @Autowired
    private UserLogMapper userLogMapper;

    @Override
    public int insert(UserLogEntity record) {
        return userLogMapper.insert( record);
    }

    @Override
    public List<UserLogEntity> getUserLogList(QueryUserLogReq queryUserLogReq) {
        return userLogMapper.getUserLogList(queryUserLogReq);
    }

    @Override
    public int getUserLogCount(QueryUserLogReq queryUserLogReq) {
        return userLogMapper.getUserLogCount(queryUserLogReq);
    }

    @Override
    public UserLogEntity getUserLogById(Integer id) {
        return userLogMapper.getUserLogById(id);
    }
}

6.新增用户操作日志Mapper接口类UserLogMapper.java

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 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//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_log
        where id = #{id}
    </select>

</mapper>
2.配置调整

1.新增pom.xml依赖,支持SpringSecurity

XML 复制代码
      <!-- 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,封装用户认证和授权信息。

java 复制代码
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"权限
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singletonList(new SimpleGrantedAuthority("USER"));
    }

    /**
     * 获取用户密码
     * @return 用户密码
     */
    @Override
    public String getPassword() {
        return password;
    }

    /**
     * 获取用户名
     * @return 用户名
     */
    @Override
    public String getUsername() {
        return username;
    }

    // 实现其他UserDetails方法...

    /**
     * 判断账户是否未过期
     * @return true表示账户未过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 判断账户是否未锁定
     * @return true表示账户未锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 判断凭证是否未过期
     * @return true表示凭证未过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 判断账户是否启用
     * @return true表示账户已启用
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. 新增JwtAuthenticationTokenFilter.java ,jwt认证过滤器,验证jwt令牌的有效性,有效则解析令牌信息中的用户信息并设置Spring Security的认证上下文。
java 复制代码
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 {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserMapper userMapper;

    /**
     * 执行JWT认证的核心逻辑。
     * 该方法会在每个HTTP请求中执行一次,检查请求头中的Authorization字段是否包含有效的JWT令牌。
     *
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param chain    过滤器链,用于继续执行后续过滤器
     * @throws ServletException 当Servlet处理出现异常时抛出
     * @throws IOException      当IO操作出现异常时抛出
     */
    @Override
    protected 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令牌中的用户ID
                    String 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,用于加载用户详细信息进行认证

java 复制代码
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 {
    @Autowired
    private UserMapper userMapper;

    /**
     * 根据用户名加载用户详细信息
     * @param name 用户名
     * @return UserDetails 用户详细信息对象
     * @throws UsernameNotFoundException 当用户不存在时抛出此异常
     */
    @Override
    public 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过滤等相关安全组件。

java 复制代码
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 {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 创建密码编码器Bean
     * 使用BCrypt算法对密码进行加密处理
     *
     * @return PasswordEncoder 密码编码器实例
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置认证管理器构建器
     * 设置自定义用户详情服务和密码编码器
     *
     * @param auth AuthenticationManagerBuilder认证管理器构建器
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * 暴露AuthenticationManager作为Bean
     * 用于在应用其他地方进行手动认证操作
     *
     * @return AuthenticationManager 认证管理器实例
     * @throws Exception 获取认证管理器时可能抛出的异常
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置HTTP安全策略
     * 包括CSRF禁用、会话管理、URL权限控制和JWT过滤器添加
     *
     * @param http HttpSecurity HTTP安全配置构建器
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Override
    protected 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的登录接口进行身份验证

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令牌方法。

java 复制代码
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;
        }
    }

}
  1. 新增PasswordUtil.java工具类,提供密码加密和验证功能。
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用户上下文工具类,提供获取当前登录用户信息的功能。

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 {

    @Autowired
    private 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.可以在任意处调用当前登录用户信息,例如,新增用户、修改菜单等。

java 复制代码
    /**
     * 新增 用户
     *
     * @param userEntity
     * @return Integer
     */
    @Override
    public 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自定义用户操作日志注解。

java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD) // 注解用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效
@Documented
public @interface UserLog {

   String value() default "";//操作接口描述
}

2.新增 UserLogAspect.java用户操作日志切面。

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 {

    @Autowired
    private UserLogService userLogService; // 假设有一个服务类用于保存日志

    @Autowired
    private 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("接口描述")即可实现访问该接口时记录日志。

java 复制代码
@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日志管理=>操作日志

javascript 复制代码
export default [
    {
      path: 'log',
      name: 'log',
      component: () => import('@/view/logmanage/log.vue'),
      meta: { title: '操作日志', requiresAuth: true }
    }

  ]

2.请求封装,新增log.js封装日志请求

javascript 复制代码
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路由下

javascript 复制代码
//省略部分导入

import log from './log.js'
Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Index',
      component: Index,
      redirect: '/login', // 默认重定向到 /home
      children: [
        {
          path: '/home',
          name: 'home',
          component: () => import('@/view/home.vue'),
          meta: { title: '首页', requiresAuth: true }
        },
        // 其他子路由也可以放在这里
        ...permission,
        ...log
      ]
    },
// 其他路由配置、、、、、、、

4.新增log.vue实现用户操作日志可视化

html 复制代码
<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用来认证和鉴权(没用到),近期工作较忙,近期不再发版,但是项目我还会继续维护,确确实实还有多少我觉得不合理、不够人性化的地方。

后续可能还会加的功能如下:

  • 用户管理的头像、邮箱、电话、水印、信息加密等
  • 菜单管理的支持拖拽、过滤等
  • 日志管理细化、分类
  • 完善首页的数据展示、优化等

有需要的同学可以关注我后续发布的文章和代码维护。

(注:接定制化开发前后端分离项目,私我)

相关推荐
果冻kk43 分钟前
MySQL MVCC:并发神器背后的原理解析
数据库·mysql
我科绝伦(Huanhuan Zhou)1 小时前
【故障案例】Redis缓存三大难题:雪崩、击穿、穿透解析与实战解决方案
redis·缓存·mybatis
Resean02233 小时前
SpringMVC 6+源码分析(二)DispatcherServlet实例化流程 1
java·spring boot·spring·servlet·springmvc
Fireworkitte3 小时前
SQL 中 CASE WHEN 及 SELECT CASE WHEN 的用法
数据库·sql·mysql
AA-代码批发V哥5 小时前
MyBatisPlus之核心注解与配置
mybatis
波波玩转AI5 小时前
MyBatis核心
数据库·mybatis
叁沐6 小时前
MySQL 24 MySQL是怎么保证主备一致的?
mysql
hqxstudying7 小时前
SpringBoot相关注解
java·spring boot·后端
Cyber4K8 小时前
MySQL--组从复制的详解及功能演练
运维·数据库·mysql·云原生