fix: 日志入库

This commit is contained in:
wangyu 2021-01-17 09:42:43 +08:00
parent 925cc2945f
commit 950f337994
20 changed files with 392 additions and 86 deletions

View File

@ -96,6 +96,11 @@ public class User extends AuditDomain implements IUser {
*/
private Integer errorCount = 0;
/**
* 上次登录日期
*/
private Date lastTime;
@Override
public User toUser() {
return this;

View File

@ -1,6 +1,5 @@
package com.flyfish.framework.logging.config;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import java.util.HashMap;
@ -27,6 +26,8 @@ public class LoggingTextRegistry implements InitializingBean {
this.mapping.put("UPDATE_ALL", "更新全部");
this.mapping.put("DELETE", "删除");
this.mapping.put("SYNC", "同步");
this.mapping.put("LOGIN", "系统登录");
this.mapping.put("LOGOUT", "退出登录");
}
public String text(String code) {

View File

@ -15,6 +15,9 @@ import java.util.Date;
@EqualsAndHashCode(callSuper = true)
public class Log extends Domain {
// 日志类型
private LogType type;
// 业务
private String business;

View File

@ -0,0 +1,14 @@
package com.flyfish.framework.logging.domain;
import com.flyfish.framework.enums.NamedEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum LogType implements NamedEnum {
OPERATION("操作"), AUTHENTICATION("登录");
private final String name;
}

View File

@ -3,6 +3,7 @@ package com.flyfish.framework.logging.service;
import com.alibaba.fastjson.JSON;
import com.flyfish.framework.logging.config.LoggingTextRegistry;
import com.flyfish.framework.logging.domain.Log;
import com.flyfish.framework.logging.domain.LogType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
@ -38,6 +39,7 @@ public class LogManager implements DisposableBean {
executorService.execute(() -> {
// 构建日志
Log log = new Log();
log.setType(LogType.OPERATION);
log.setSuccess(context.isSuccess());
log.setBody(bodyString(context.getArgs()));
log.setModule(context.getModule());

View File

@ -0,0 +1,117 @@
package com.flyfish.framework.logging.service;
import com.flyfish.framework.domain.base.Domain;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.logging.domain.Log;
import com.flyfish.framework.logging.domain.LogType;
import com.flyfish.framework.service.AuthenticationLogger;
import com.flyfish.framework.utils.AuthenticationMessages;
import com.flyfish.framework.utils.MapBuilder;
import com.flyfish.framework.utils.UserUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
/**
* 简单的认证日志
*
* @author wangyu
*/
@Service
public class SimpleAuthenticationLogger implements AuthenticationLogger {
private final Map<String, String> uris = MapBuilder.<String, String>builder()
.put("login", "登录")
.put("logout", "注销")
.build();
@Resource
private LogService logService;
/**
* 记录成功
*
* @param user 用户
*/
@Override
public void success(UserDetails user) {
saveSuccess("LOGIN", "登录成功", user);
}
/**
* 记录失败
*
* @param form 表单
* @param exception 错误异常
*/
@Override
public void failure(MultiValueMap<String, String> form, AuthenticationException exception) {
saveError("LOGIN", "登录失败", form.getFirst("username"), AuthenticationMessages.message(exception));
}
/**
* 记录退出
*
* @param user 用户
*/
@Override
public void logout(UserDetails user) {
saveSuccess("LOGOUT", "退出成功", user);
}
/**
* 操作成功记录
*
* @param business 业务
* @param message 信息
* @param user 用户
*/
private void saveSuccess(String business, String message, UserDetails user) {
User raw = UserUtils.toUser(user);
Log log = new Log();
log.setType(LogType.AUTHENTICATION);
log.setSignature(uris.get(business));
log.setSuccess(true);
log.setBody("为了安全,本内容隐藏");
log.setModule("登录模块");
log.setBusiness(business);
log.setResponse(message);
log.setOperator(Optional.ofNullable(raw).map(Domain::getName).orElse(""));
log.setPeriod(0L);
log.setStartTime(new Date());
// 写入日志
logService.create(log);
}
/**
* 保存失败日志
*
* @param business 业务
* @param message 消息
* @param username 用户名
* @param error 错误
*/
private void saveError(String business, String message, String username, String error) {
Log log = new Log();
log.setType(LogType.AUTHENTICATION);
log.setSignature(uris.get(business));
log.setSuccess(false);
log.setBody("某人尝试使用用户名'" + username + "',密码:******,进行登录");
log.setModule("登录模块");
log.setBusiness(business);
log.setError(error);
log.setResponse(message);
log.setOperator("无用户");
log.setPeriod(0L);
log.setStartTime(new Date());
// 写入日志
logService.create(log);
}
}

View File

@ -0,0 +1,82 @@
package com.flyfish.framework.config;
import com.flyfish.framework.domain.AdminUserDetails;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.enums.UserStatus;
import com.flyfish.framework.service.AuthenticationAuditor;
import com.flyfish.framework.service.AuthenticationLogger;
import com.flyfish.framework.service.ReactiveUserService;
import com.flyfish.framework.transform.ResultDataTransformer;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.WebFilterExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.Date;
/**
* 提供对象写入登录登出审计
*
* @author wangyu
*/
public class AuthenticationAuditorImpl extends ResultDataTransformer implements AuthenticationAuditor {
@Resource
private ReactiveUserService reactiveUserService;
@Resource
private AuthenticationLogger authenticationLogger;
/**
* 检查用户异常无异常清空错误次数设置上次登录
*
* @param user 用户
*/
@Override
public Mono<Void> success(UserDetails user) {
AdminUserDetails details = (AdminUserDetails) user;
User updating = new User();
updating.setId(details.getId());
updating.setErrorCount(0);
updating.setLastTime(new Date());
return reactiveUserService.updateSelectiveById(updating)
.then(Mono.fromRunnable(() -> authenticationLogger.success(user)));
}
/**
* 登录失败的回调
* 当输入错误的账号密码记录错误次数否则只记录日志
*
* @param webFilterExchange 请求信息
* @param exception 异常
*/
@Override
public Mono<Void> error(WebFilterExchange webFilterExchange, AuthenticationException exception) {
return webFilterExchange.getExchange()
.getFormData()
.flatMap(data -> {
// 当且仅当为凭据错误尝试
if (exception instanceof BadCredentialsException) {
return reactiveUserService.findByUsername(data.getFirst("username"))
.flatMap(user -> {
User updating = new User();
updating.setId(user.getId());
updating.setErrorCount(user.getErrorCount() + 1);
if (updating.getErrorCount() >= 5) {
updating.setStatus(UserStatus.LOCKED);
}
return reactiveUserService.updateSelectiveById(updating);
})
.map(user -> data);
}
return Mono.just(data);
})
.flatMap(data -> Mono.fromRunnable(() -> authenticationLogger.failure(data, exception)));
}
@Override
public Mono<Void> logout(UserDetails userDetails) {
return Mono.fromRunnable(() -> authenticationLogger.logout(userDetails));
}
}

View File

@ -12,8 +12,8 @@ import com.flyfish.framework.handler.JsonAuthenticationFailureHandler;
import com.flyfish.framework.handler.JsonAuthenticationSuccessHandler;
import com.flyfish.framework.handler.JsonLogoutSuccessHandler;
import com.flyfish.framework.initializer.UserInitializer;
import com.flyfish.framework.service.AuthenticationAuditor;
import com.flyfish.framework.service.UserService;
import com.flyfish.framework.transform.ResultDataTransformer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -58,6 +58,11 @@ public class WebSecurityConfig {
return delegatingPasswordEncoder;
}
@Bean
public AuthenticationAuditor loginAuditor() {
return new AuthenticationAuditorImpl();
}
@ConditionalOnProperty(value = "jwt.enable", havingValue = "true")
@Bean("contextRepository")
public JwtSecurityContextRepository jwtSecurityContextRepository() {
@ -75,11 +80,6 @@ public class WebSecurityConfig {
return new TokenProvider(properties);
}
@Bean
public ResultDataTransformer resultDataTransformer() {
return new ResultDataTransformer();
}
/**
* spring安全拦截规则配置
*
@ -90,10 +90,10 @@ public class WebSecurityConfig {
@Bean
@SuppressWarnings("all")
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ResultDataTransformer dataTransformer,
TokenProvider tokenProvider,
SecurityProperties properties,
ReactiveUserDetailsService userDetailsService) {
ReactiveUserDetailsService userDetailsService,
AuthenticationAuditor authenticationAuditor) {
http
.securityContextRepository(contextRepository())
.authorizeExchange()
@ -105,13 +105,13 @@ public class WebSecurityConfig {
.formLogin() // 配置登录节点
.authenticationManager(authenticationManager(properties, userDetailsService))
.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
.authenticationFailureHandler(new JsonAuthenticationFailureHandler(dataTransformer))
.authenticationSuccessHandler(new JsonAuthenticationSuccessHandler(dataTransformer))
.authenticationSuccessHandler(new JsonAuthenticationSuccessHandler(authenticationAuditor))
.authenticationFailureHandler(new JsonAuthenticationFailureHandler(authenticationAuditor))
.requiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login", "/api/login"))
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(new JsonLogoutSuccessHandler(dataTransformer, tokenProvider))
.logoutSuccessHandler(new JsonLogoutSuccessHandler(authenticationAuditor, tokenProvider))
.and()
.csrf().disable();
return http.build();

View File

@ -138,7 +138,7 @@ public class AdminUserDetails implements UserDetails, IUser {
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return isAccountNonExpired();
return null == validDate || validDate.after(new Date());
}
@JsonIgnore

View File

@ -17,7 +17,6 @@ import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
@ -149,24 +148,4 @@ public class MongoUserDetailsServiceImpl implements MongoUserDetailsService {
.flatMap(context -> contextRepository.save(exchange, context));
}
/**
* 发生错误时的处理
*
* @param exchange 交换
* @return 结果
*/
@Override
public Mono<User> error(ServerWebExchange exchange) {
return exchange.getFormData().flatMap(data -> userService.findByUsername(data.getFirst("username")))
.flatMap(user -> {
User updating = new User();
updating.setId(user.getId());
updating.setErrorCount(user.getErrorCount() + 1);
if (updating.getErrorCount() >= 5) {
updating.setStatus(UserStatus.LOCKED);
}
return userService.updateSelectiveById(updating);
});
}
}

View File

@ -1,20 +1,15 @@
package com.flyfish.framework.handler;
import com.flyfish.framework.bean.Result;
import com.flyfish.framework.service.MongoUserDetailsService;
import com.flyfish.framework.transform.DataBufferTransformer;
import com.flyfish.framework.service.AuthenticationAuditor;
import com.flyfish.framework.utils.AuthenticationMessages;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* 基于json的登录失败包装详见Spring Security
*
@ -22,16 +17,11 @@ import java.util.Optional;
*/
public class JsonAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
private final String defaultMessage = "用户登录异常!";
// 数据块工厂
private final DataBufferTransformer<Result<?>> dataBufferTransformer;
private final AuthenticationAuditor authenticationAuditor;
private final Map<Class<? extends AuthenticationException>, String> descriptionMap = new HashMap<>();
public JsonAuthenticationFailureHandler(DataBufferTransformer<Result<?>> dataBufferTransformer) {
this.dataBufferTransformer = dataBufferTransformer;
descriptionMap.put(BadCredentialsException.class, "用户名密码不正确!");
// todo 剩下的都在userDetailsService
public JsonAuthenticationFailureHandler(AuthenticationAuditor authenticationAuditor) {
this.authenticationAuditor = authenticationAuditor;
}
/**
@ -45,15 +35,8 @@ public class JsonAuthenticationFailureHandler implements ServerAuthenticationFai
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return Mono.justOrEmpty(Optional.ofNullable(webFilterExchange.getExchange().getApplicationContext()))
.flatMap(applicationContext -> {
MongoUserDetailsService userDetailsService = applicationContext.getBean(MongoUserDetailsService.class);
if (exception instanceof BadCredentialsException) {
return userDetailsService.error(webFilterExchange.getExchange())
.flatMap(user -> write(response, exception));
}
return write(response, exception);
});
// 记录错误次数错误日志等最后写入
return authenticationAuditor.error(webFilterExchange, exception).then(write(response, exception));
}
/**
@ -64,9 +47,6 @@ public class JsonAuthenticationFailureHandler implements ServerAuthenticationFai
* @return 结果
*/
private Mono<Void> write(ServerHttpResponse response, AuthenticationException exception) {
return response.writeWith(Mono.fromCallable(() ->
dataBufferTransformer.transform(
Result.error(descriptionMap.getOrDefault(exception.getClass(), exception.getMessage()))
)));
return response.writeWith(authenticationAuditor.transform(Result.error(AuthenticationMessages.message(exception))));
}
}

View File

@ -1,13 +1,13 @@
package com.flyfish.framework.handler;
import com.flyfish.framework.bean.Result;
import com.flyfish.framework.transform.DataBufferTransformer;
import com.flyfish.framework.service.AuthenticationAuditor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import reactor.core.publisher.Mono;
@ -21,7 +21,7 @@ import reactor.core.publisher.Mono;
public class JsonAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
// 数据块工厂
private final DataBufferTransformer<Result<?>> dataBufferTransformer;
private final AuthenticationAuditor authenticationAuditor;
/**
* 登录成功后要返回用户的基本信息节省带宽
@ -35,9 +35,9 @@ public class JsonAuthenticationSuccessHandler implements ServerAuthenticationSuc
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
HttpHeaders headers = response.getHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 查询
return response.writeWith(ReactiveSecurityContextHolder.getContext().map(securityContext ->
dataBufferTransformer.transform(Result.accept(securityContext.getAuthentication().getPrincipal()))
));
Object principal = authentication.getPrincipal();
// 如果登录成功清空错误次数更新上次登录时间
return authenticationAuditor.success((UserDetails) principal)
.then(response.writeWith(authenticationAuditor.transform(Result.accept(principal))));
}
}

View File

@ -2,11 +2,12 @@ package com.flyfish.framework.handler;
import com.flyfish.framework.bean.Result;
import com.flyfish.framework.configuration.jwt.TokenProvider;
import com.flyfish.framework.transform.DataBufferTransformer;
import com.flyfish.framework.service.AuthenticationAuditor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import reactor.core.publisher.Mono;
@ -20,8 +21,9 @@ import reactor.core.publisher.Mono;
public class JsonLogoutSuccessHandler implements ServerLogoutSuccessHandler {
// 数据块工厂
private final DataBufferTransformer<Result<?>> dataBufferTransformer;
private final AuthenticationAuditor authenticationAuditor;
// token提供者
private final TokenProvider tokenProvider;
@Override
@ -29,7 +31,8 @@ public class JsonLogoutSuccessHandler implements ServerLogoutSuccessHandler {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
tokenProvider.removeToken(webFilterExchange.getExchange());
// 查询
return response.writeWith(Mono.just(dataBufferTransformer.transform(Result.ok())));
// 记录日志
return authenticationAuditor.logout((UserDetails) authentication.getPrincipal())
.then(response.writeWith(authenticationAuditor.transform(Result.ok())));
}
}

View File

@ -0,0 +1,38 @@
package com.flyfish.framework.service;
import com.flyfish.framework.bean.Result;
import com.flyfish.framework.transform.DataBufferTransformer;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.WebFilterExchange;
import reactor.core.publisher.Mono;
/**
* 登录审计
*
* @author wangyu
*/
public interface AuthenticationAuditor extends DataBufferTransformer<Result<?>> {
/**
* 登录成功的回调
*
* @param userDetails 用户信息
*/
Mono<Void> success(UserDetails userDetails);
/**
* 登录失败的回调
*
* @param webFilterExchange 请求信息
* @param exception 异常
*/
Mono<Void> error(WebFilterExchange webFilterExchange, AuthenticationException exception);
/**
* 退出登录的回调
*
* @param userDetails 用户详情
*/
Mono<Void> logout(UserDetails userDetails);
}

View File

@ -0,0 +1,36 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.po.User;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.MultiValueMap;
/**
* 认证日志者
*
* @author wangyu
*/
public interface AuthenticationLogger {
/**
* 记录成功
*
* @param user 用户
*/
void success(UserDetails user);
/**
* 记录失败
*
* @param form 表单
* @param exception 错误异常
*/
void failure(MultiValueMap<String, String> form, AuthenticationException exception);
/**
* 记录退出
*
* @param user 用户
*/
void logout(UserDetails user);
}

View File

@ -1,6 +1,5 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.po.User;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
@ -42,12 +41,4 @@ public interface MongoUserDetailsService extends ReactiveUserDetailsService, Rea
* @return 结果
*/
Mono<Void> logout(ServerWebExchange exchange);
/**
* 发生错误时的处理
*
* @param exchange 交换
* @return 结果
*/
Mono<User> error(ServerWebExchange exchange);
}

View File

@ -1,13 +1,20 @@
package com.flyfish.framework.transform;
import org.springframework.core.io.buffer.DataBuffer;
import reactor.core.publisher.Mono;
/**
* 用于转换DataBuffer的工具
*
* @auther wangyu
* @author wangyu
*/
public interface DataBufferTransformer<T> {
DataBuffer transform(T source);
/**
* 写入二进制数据返回数据
*
* @param source 数据源
* @return 结果
*/
Mono<DataBuffer> transform(T source);
}

View File

@ -4,6 +4,7 @@ import com.flyfish.framework.bean.Result;
import com.flyfish.framework.utils.JacksonUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
@ -21,10 +22,11 @@ public class ResultDataTransformer implements DataBufferTransformer<Result<?>> {
}
@Override
public DataBuffer transform(Result<?> source) {
public Mono<DataBuffer> transform(Result<?> source) {
return JacksonUtil.toJson(source)
.map(result -> result.getBytes(Charset.defaultCharset()))
.map(factory::wrap)
.orElse(null);
.map(Mono::just)
.orElse(Mono.empty());
}
}

View File

@ -0,0 +1,29 @@
package com.flyfish.framework.utils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import java.util.Map;
/**
* 认证消息
* @author wangyu
*/
public interface AuthenticationMessages {
String DEFAULT_MESSAGE = "用户登录异常!";
// todo 剩下的都在userDetailsService
Map<Class<? extends AuthenticationException>, String> DESCRIPTION_MAP = MapBuilder.<Class<? extends AuthenticationException>, String>builder()
.put(BadCredentialsException.class, "用户名密码不正确!")
.build();
/**
* 消息构建通过异常
* @param exception 异常
* @return 结果
*/
static String message(AuthenticationException exception) {
return DESCRIPTION_MAP.getOrDefault(exception.getClass(), exception.getMessage());
}
}

View File

@ -3,6 +3,7 @@ package com.flyfish.framework.utils;
import com.flyfish.framework.domain.base.IUser;
import com.flyfish.framework.domain.po.User;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 用户工具
@ -20,4 +21,20 @@ public class UserUtils {
public static User extractUser(SecurityContext securityContext) {
return ((IUser) securityContext.getAuthentication().getPrincipal()).toUser();
}
/**
* 转换用户
*
* @param userDetails 用户详情
* @return 结果
*/
public static User toUser(UserDetails userDetails) {
if (null == userDetails) {
return null;
}
if (userDetails instanceof IUser) {
return ((IUser) userDetails).toUser();
}
return null;
}
}