目录
一、前言
此文章在上次调整的基础上开发后端管理系统的用户请求日志功能,并集成了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;
}
}
- 新增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;
}
}
}
- 新增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用来认证和鉴权(没用到),近期工作较忙,近期不再发版,但是项目我还会继续维护,确确实实还有多少我觉得不合理、不够人性化的地方。
后续可能还会加的功能如下:
- 用户管理的头像、邮箱、电话、水印、信息加密等
- 菜单管理的支持拖拽、过滤等
- 日志管理细化、分类
- 完善首页的数据展示、优化等
有需要的同学可以关注我后续发布的文章和代码维护。
(注:接定制化开发前后端分离项目,私我)