SpringSecurity許可權管理系統實戰—八、AOP 記錄使用者、異常日誌

codermy發表於2020-08-20

目錄

SpringSecurity許可權管理系統實戰—一、專案簡介和開發環境準備
SpringSecurity許可權管理系統實戰—二、日誌、介面文件等實現
SpringSecurity許可權管理系統實戰—三、主要頁面及介面實現
SpringSecurity許可權管理系統實戰—四、整合SpringSecurity(上)
SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)
SpringSecurity許可權管理系統實戰—六、SpringSecurity整合jwt
SpringSecurity許可權管理系統實戰—七、處理一些問題
SpringSecurity許可權管理系統實戰—八、AOP 記錄使用者日誌、異常日誌

前言

日誌功能在二的時候其實簡單實現了一下,但是有時我們需要對一些重要功能操作記錄日誌,或是在操作時發生異常,需要記錄異常日誌。但是之前每次發生異常要定位原因我們都要到伺服器去查詢日誌才能找到,或許是搭建一個日誌收集系統(但是本專案中暫不考慮)。那麼我們可以專門做個功能來記錄使用者操作日誌和異常日誌,在把日誌存入資料庫,方便查詢。

一、最終效果

在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述

二、新建my_log表

在這裡插入圖片描述

相應欄位都有註釋,很好理解,使用者日誌、異常日誌都存放在這一張表中,通過type來區分,當然也可以拆分成兩張表。

三、新增依賴

 		<!--aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--   UserAgentUtils,瀏覽器資訊工具類   -->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
        <!--ip2region,這是根據ip查地址的工具,有興趣自己可以瞭解-->
        <!-- <dependency>-->
        <!-- <groupId>org.lionsoul</groupId>-->
        <!-- <artifactId>ip2region</artifactId>-->
        <!-- <version>1.7.2</version>-->
        <!-- </dependency>-->
		<!--分頁工具-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.13</version>
        </dependency>
        <!--hutool工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.4</version>
        </dependency>

四、需要用到的工具類

SecurityUtils

/**
 * @author codermy
 * @createTime 2020/8/4
 */
public class SecurityUtils {

    /**
     * 獲取系統使用者名稱稱
     *
     * @return 系統使用者名稱稱
     */
    public static String getCurrentUsername() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            throw new MyException(ResultCode.UNAUTHORIZED, "當前登入狀態過期");
        }
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails.getUsername();
    }
    /**
     * 取得當前使用者登入IP, 如果當前使用者未登入則返回空字串.
     * 此方法無用
     */
    public static String getCurrentUserIp() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null) {
            throw new MyException(ResultCode.UNAUTHORIZED, "當前登入狀態過期");
        }
        Object details = authentication.getDetails();
        if (!(details instanceof WebAuthenticationDetails)) {
            return "";
        }
        WebAuthenticationDetails webDetails = (WebAuthenticationDetails) details;
        return webDetails.getRemoteAddress();
    }

}

LogUtils

/**
 * @author codermy
 * @createTime 2020/8/7
 */
public class LogUtils {
    private static final char SEPARATOR = '_';

    private static final String UNKNOWN = "unknown";
    /**
     * 獲取ip地址
     */
    public static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        String comma = ",";
        String localhost = "127.0.0.1";
        if (ip.contains(comma)) {
            ip = ip.split(",")[0];
        }
        if  (localhost.equals(ip))  {
            // 獲取本機真正的ip地址
            try {
                ip = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
        }
        return ip;
    }

    /**
     * 獲取瀏覽器資訊
     * @param request
     * @return
     */
    public static String getBrowser(HttpServletRequest request){
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        Browser browser = userAgent.getBrowser();
        return browser.getName();
    }

    /**
     * 獲取堆疊資訊
     */
    public static String getStackTrace(Throwable throwable){
        StringWriter sw = new StringWriter();
        try (PrintWriter pw = new PrintWriter(sw)) {
            throwable.printStackTrace(pw);
            return sw.toString();
        }
    }



}

RequestHolder

/**
 * @author codermy
 * @createTime 2020/8/4
 */
public class RequestHolder {
    /**
     * 獲取HttpServletRequest物件
     * @return
     */
    public static HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    }
}

五、相應實體類

這個部分省略,沒有什麼技術含量,根據資料庫來就行

六、自定義操作日誌的註解類

/**
 * @author codermy
 * @createTime 2020/8/4
 */
@Target(ElementType.METHOD)//註解放置的目標位置,METHOD是可註解在方法級別上
@Retention(RetentionPolicy.RUNTIME)//註解在哪個階段執行
public @interface MyLog {
    String value() default "";
}

關於java自定義註解可以看看這篇文章

七、新建切面類

這其實很好理解,就是我們學習spring時,aop的幾種通知。

/**
 * @author codermy
 * @createTime 2020/8/4
 */
@Component
@Aspect
@Slf4j
public class LogAspect {
    //注入logService用於將日誌存入資料庫
    @Autowired
    private MyLogService logService;

    ThreadLocal<Long> currentTime = new ThreadLocal<>();
    /**
     * 設定操作日誌切入點 記錄操作日誌 在註解的位置切入程式碼
     */
    @Pointcut("@annotation(com.codermy.myspringsecurityplus.log.aop.MyLog)")
    public void logPoinCut() {
    }

    /**
     * 配置環繞通知,使用在方法logPointcut()上註冊的切入點
     *
     * @param joinPoint join point for advice
     */
    @Around("logPoinCut()")
    public Object saveSysLog(ProceedingJoinPoint joinPoint)throws Throwable{
        Object result;
        currentTime.set(System.currentTimeMillis());//記錄方法的執行時間
        result = joinPoint.proceed();
        MyLog log = new MyLog("INFO",System.currentTimeMillis() - currentTime.get());//定義日誌型別
        currentTime.remove();
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        logService.save(SecurityUtils.getCurrentUsername(), LogUtils.getBrowser(request), LogUtils.getIp(request),joinPoint, log);
        return result;
    }

    /**
     * 配置異常通知
     *
     * @param joinPoint join point for advice
     * @param e exception
     */
    @AfterThrowing(pointcut = "logPoinCut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        MyLog log = new MyLog("ERROR",System.currentTimeMillis() - currentTime.get());
        currentTime.remove();
        log.setExceptionDetail(LogUtils.getStackTrace(e));
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        logService.save(SecurityUtils.getCurrentUsername(), LogUtils.getBrowser(request), LogUtils.getIp(request), (ProceedingJoinPoint)joinPoint, log);
    }
}

八、相應方法及介面

dao

/**
 * @author codermy
 * @createTime 2020/8/8
 */
@Mapper
public interface LogDao {

    /**
     * 儲存日誌
     * @param log
     */
    @Insert("insert into my_log(user_name,ip,description,params,type,exception_detail,browser,method,time,create_time)values(#{userName},#{ip},#{description},#{params},#{type},#{exceptionDetail},#{browser},#{method},#{time},now())")
    void save(MyLog log);

    /**
     * 分頁返回所有使用者日誌
     * @param logQuery 查詢條件
     * @return
     */
    List<LogDto> getFuzzyLogByPage( @Param("logQuery") LogQuery logQuery);


    /**
     * 分頁返回所有錯誤日誌
     * @param logQuery 查詢條件
     * @return
     */
    List<ErrorLogDto> getFuzzyErrorLogByPage(@Param("logQuery") LogQuery logQuery);


    /**
     * 刪除所有日誌
     * @param type 日誌型別
     */
    @Delete("delete from my_log where type = #{type}")
    void delAllByInfo(String type);
}

LogMapper.xml

<mapper namespace="com.codermy.myspringsecurityplus.log.dao.LogDao">
    <select id="getFuzzyLogByPage" resultType="com.codermy.myspringsecurityplus.log.dto.LogDto">
        SELECT t.user_name,t.ip,t.params,t.description,t.browser,t.time,t.method,t.create_time
        FROM my_log t
        <where>
            <if test="logQuery.logType != null and logQuery.logType  != ''">
                AND t.type = #{logQuery.logType}
            </if>
            <if test="logQuery.userName != null and logQuery.userName != ''">
                AND t.user_name like CONCAT('%', #{logQuery.userName}, '%')
            </if>
        </where>
        ORDER BY t.create_time desc
    </select>

    <select id="getFuzzyErrorLogByPage" resultType="com.codermy.myspringsecurityplus.log.dto.ErrorLogDto">
        SELECT t.user_name,t.ip,t.params,t.description,t.browser,t.exception_detail,t.method,t.create_time
        FROM my_log t
        <where>
            <if test="logQuery.logType != null and logQuery.logType  != ''">
                AND t.type = #{logQuery.logType}
            </if>
            <if test="logQuery.userName != null and logQuery.userName != ''">
                AND t.user_name like CONCAT('%', #{logQuery.userName}, '%')
            </if>
        </where>
        ORDER BY t.create_time desc
    </select>

</mapper>

MyLogServiceImpl

/**
 * @author codermy
 * @createTime 2020/8/4
 */
@Service
public class MyLogServiceImpl implements MyLogService {
    @Autowired
    private LogDao logDao;
	//返回使用者日誌
    @Override
    public Result<LogDto> getFuzzyInfoLogByPage(Integer offectPosition, Integer limit, LogQuery logQuery) {
        Page page = PageHelper.offsetPage(offectPosition,limit);
        List<LogDto> fuzzyLogByPage = logDao.getFuzzyLogByPage(logQuery);
        return Result.ok().count(page.getTotal()).data(fuzzyLogByPage).code(ResultCode.TABLE_SUCCESS);
    }
	//返回異常日誌
    @Override
    public Result<ErrorLogDto> getFuzzyErrorLogByPage(Integer offectPosition, Integer limit, LogQuery logQuery) {
        Page page = PageHelper.offsetPage(offectPosition,limit);
        List<ErrorLogDto> fuzzyErrorLogByPage = logDao.getFuzzyErrorLogByPage(logQuery);
        return Result.ok().count(page.getTotal()).data(fuzzyErrorLogByPage).code(ResultCode.TABLE_SUCCESS);
    }
	//儲存日誌到資料庫
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void save(String userName, String browser, String ip, ProceedingJoinPoint joinPoint, MyLog log) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        com.codermy.myspringsecurityplus.log.aop.MyLog myLog = method.getAnnotation(com.codermy.myspringsecurityplus.log.aop.MyLog.class);
        // 方法路徑
        String methodName = joinPoint.getTarget().getClass().getName()+"."+signature.getName()+"()";
        StringBuilder params = new StringBuilder("{");
        //引數值
        Object[] argValues = joinPoint.getArgs();
        //引數名稱
        String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
        if(argValues != null){
            for (int i = 0; i < argValues.length; i++) {
                params.append(" ").append(argNames[i]).append(": ").append(argValues[i]);
            }
        }
        // 描述
        if (log != null) {
            log.setDescription(myLog.value());
        }
        assert log != null;
        log.setIp(ip);
        String loginPath = "login";
        if(loginPath.equals(signature.getName())){
            try {
                assert argValues != null;
                userName = new JSONObject(argValues[0]).get("userName").toString();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        log.setMethod(methodName);
        log.setUserName(userName);
        log.setParams(params.toString() + " }");
        log.setBrowser(browser);
        logDao.save(log);
    }
	//刪除異常日誌
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delAllByError() {
        logDao.delAllByInfo("ERROR");
    }
	//刪除使用者日誌
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delAllByInfo() {
        logDao.delAllByInfo("INFO");
    }
}

LogController

/**
 * @author codermy
 * @createTime 2020/8/8
 */
@Controller
@RequestMapping("/api")
@Api(tags = "系統:日誌管理")
public class LogController {
    @Autowired
    private MyLogService logService;

    @GetMapping("/log/index")
    public String logIndex(){
        return "system/log/log";
    }

    @GetMapping("/log")
    @ResponseBody
    @ApiOperation(value = "日誌列表")
    @PreAuthorize("hasAnyAuthority('log:list')")
    public Result<LogDto> logList(PageTableRequest pageTableRequest, LogQuery logQuery){
        pageTableRequest.countOffset();
        logQuery.setLogType("INFO");
        return logService.getFuzzyInfoLogByPage(pageTableRequest.getOffset(),pageTableRequest.getLimit(),logQuery);
    }

    @DeleteMapping("/log")
    @MyLog("刪除所有INFO日誌")
    @ResponseBody
    @ApiOperation("刪除所有INFO日誌")
    @PreAuthorize("hasAnyAuthority('log:del')")
    public Result<Object> delAllByInfo(){
        logService.delAllByInfo();
        return Result.ok().message("刪除成功");
    }

    @GetMapping("/log/error/index")
    public String errorLogIndex(){
        return "system/log/errorLog";
    }

    @GetMapping("/error/log")
    @ResponseBody
    @ApiOperation(value = "錯誤日誌")
    @PreAuthorize("hasAnyAuthority('errorLog:list')")
    public Result<ErrorLogDto> errorLogList(PageTableRequest pageTableRequest, LogQuery logQuery){
        pageTableRequest.countOffset();
        logQuery.setLogType("ERROR");
        return logService.getFuzzyErrorLogByPage(pageTableRequest.getOffset(),pageTableRequest.getLimit(),logQuery);
  }
    @DeleteMapping("/error/log")
    @MyLog("刪除所有ERROR日誌")
    @ResponseBody
    @ApiOperation("刪除所有ERROR日誌")
    @PreAuthorize("hasAnyAuthority('errorLog:del')")
    public Result<Object> delAllByError(){
        logService.delAllByError();
        return Result.ok().message("刪除成功");
    }

}

相應的前端頁面就不貼出來了,有需要可以在我的giteegithub中獲取

九、使用

我們只需要在相應的介面上新增上@MyLog註解即可

在這裡插入圖片描述

我們可以自己先造一個異常來測試異常的收集

在這裡插入圖片描述

十、啟動測試

啟動專案,正常訪問測試即可,會自動收集日誌。

在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述
本系列giteegithub中同步更新

相關文章