目錄
一、前言
二、后端開發及調整
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用來認證和鑒權(沒用到),近期工作較忙,近期不再發版,但是項目我還會繼續維護,確確實實還有多少我覺得不合理、不夠人性化的地方。
后續可能還會加的功能如下:
- 用戶管理的頭像、郵箱、電話、水印、信息加密等
- 菜單管理的支持拖拽、過濾等
- 日志管理細化、分類
- 完善首頁的數據展示、優化等
有需要的同學可以關注我后續發布的文章和代碼維護。?
(注:接定制化開發前后端分離項目,私我)?