对于多数B/C商业模式的项目来说,都具备多端登录的场景。举个例子,上个公司做的跑腿业务,就有这么几种:用户端手机号验证码登录、用户端用户名密码登录、用户端微信小程序登录、骑手端手机号验证码登录、商户端用户名密码登录、总后台用户名密码登录等等。如果让你去做一个记录多种用户类型加多端登录日志的功能,你会怎么设计,在接口方法的最后边手动添加吗?这明显不是一个好主意,我们可以结合自定义注解+AOP+策略模式的方式,去用切面记录一下!
(LoginConvert)
策略模式的基础接口,不同登录接口的入参对象可以实现这个接口,然后重载这个convert方法,将入参中的参数收集到LoginLogVo这个类中。
public interface LoginConvert {
LoginLogVo convert(PARAM param);
}
(LoginLogVo.class)
用来记录入参时的参数
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class LoginLogVo {
@ApiModelProperty("登录名")
private String userName;
@ApiModelProperty("验证码、小程序Code、密码、OpenId、、、")
private String codeOrPwdOrOpenId;
}
(LoginLogAnno)
注解中定义用户类型、操作类型和转换入参对象
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginLogAnno {
/**
* 用户类型
* [用户,小程序用户,骑手,管理员]
*/
String userType() default "";
/**
* 操作类型
* [1登录;2退出]
*/
int operationType() default 1;
/**
* 转换入参对象
*/
Class<? extends LoginConvert> convert();
}
接口的入参对象,实现了之前定义的基础接口,然后重载了convert方法,将对象中的入参提取出来放到LoginLogVo中,方便之后AOP切面中使用。
@Data
public class AppLoginRequest implements LoginConvert {
@ApiModelProperty(value = "登录手机号")
@NotBlank(message = "手机号必传")
private String phone;
@ApiModelProperty(value = "验证码")
@NotBlank(message = "验证码必传")
private String code;
@Override
public LoginLogVo convert(AppLoginRequest request) {
LoginLogVo loginLogVo = new LoginLogVo();
loginLogVo.setUserName(request.getPhone());
loginLogVo.setCodeOrPwdOrOpenId(request.getCode());
return loginLogVo;
}
}
@Data
public class WxMiniLoginRequest implements LoginConvert {
@NotBlank(message = "微信登录Code必传")
@ApiModelProperty("微信登录Code")
private String code;
@ApiModelProperty("openId")
private String openId;
@Override
public LoginLogVo convert(WxMiniLoginRequest wxMiniLoginRequest) {
LoginLogVo loginLogVo = new LoginLogVo();
loginLogVo.setUserName(wxMiniLoginRequest.getCode());
loginLogVo.setCodeOrPwdOrOpenId(wxMiniLoginRequest.getOpenId());
return loginLogVo;
}
}
@Data
public class WebLoginRequest implements LoginConvert {
@NotBlank(message = "用户名必传")
@ApiModelProperty(value = "用户名")
private String userName;
@NotBlank(message = "密码必传")
@ApiModelProperty(value = "密码")
private String password;
@Override
public LoginLogVo convert(WebLoginRequest webLoginRequest) {
LoginLogVo loginLogVo = new LoginLogVo();
loginLogVo.setUserName(webLoginRequest.getUserName());
loginLogVo.setCodeOrPwdOrOpenId(webLoginRequest.getPassword());
return loginLogVo;
}
}
Controller
@PostMapping("/user/phone/and/code/login")
@ApiOperation(value = "用户-手机号验证码登录")
public ResultEntity userPhoneAndCodeLogin(@Validated AppLoginRequest appLoginRequest) {
return ResultEntity.success(loginService.userPhoneAndCodeLoginService(appLoginRequest));
}
Service
/**
* 用户-手机号验证码登录
*
* @return 登录返回值
*/
@LoginLogAnno(userType = Constant.USER, operationType = 1, convert = AppLoginRequest.class)
public CommonLoginResponse userPhoneAndCodeLoginService(AppLoginRequest appLoginRequest) {
User user = userMapper.selectOne();
//判断用户状态抛出异常
if (ObjectUtils.isNull(user)) {
throw new BadRequestAlertException("账号不存在或已被删除或已被封禁");
}
//登录逻辑
//组装返回值
return response;
}
Controller
@PostMapping("/user/wx/check")
@ApiOperation(value = "微信小程序登录")
public ResultEntity wxMiniCheck(@RequestBody WxMiniLoginRequest request) throws JSONException {
return ResultEntity.success(loginService.wxMiniCheckService(request));
}
Service
/**
* 微信直接登录(如果已有账号且绑定微信)
*
* @return 返回参数
*/
@LoginLogAnno(userType = Constant.WX_USER, operationType = 1, convert = WxMiniLoginRequest.class)
public CommonLoginResponse wxMiniCheckService(WxMiniLoginRequest wxMiniLoginRequest) {
//登录逻辑
//组装返回值
return response;
}
Controller
@PostMapping("/admin/password/login")
@ApiOperation(value = "管理员-账号密码登录")
public ResultEntity adminPasswordLogin(@Validated WebLoginRequest webLoginRequest) {
return ResultEntity.success(loginService.adminPasswordLogin(webLoginRequest));
}
Service
/**
* 管理员登录
*
* @return 返回值
*/
@LoginLogAnno(userType = Constant.ADMIN, operationType = 1, convert = WebLoginRequest.class)
public CommonLoginResponse adminPasswordLogin(WebLoginRequest webLoginRequest) {
//登录逻辑
//组装返回值
return response;
}
(security_log)
CREATE TABLE `security_log` (
`id` bigint NOT NULL COMMENT '主键ID',
`log_type` tinyint DEFAULT NULL COMMENT '记录类型 (0登录账户;1登出账户;3异常情况)',
`user_type` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户类型 (普通用户=USER、微信用户=WX_USER、管理员=ADMIN)',
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
`user_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
`description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '详细描述',
`login_ip` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '登录IP',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='日志记录表';
主要的核心有这么几点:
@Slf4j
@Aspect
@Component
@AllArgsConstructor
public class LogService implements Ordered {
UserMapper userMapper;
AdminMapper adminMapper;
SecurityLogMapper securityLogMapper;
private final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
16, 200, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(Integer.MAX_VALUE)
);
/**
* 切点
*/
@Pointcut(value = "@annotation(com.mall.common.aop.annotation.LogRecordAnno)")
public void logPointcut() {}
/**
* 执行器
*
* @param joinPoint 切入点
*/
@Around("logPointcut()")
public Object saveLoginSecurity(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法属性
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
LogRecordAnno logRecordAnno = method.getAnnotation(LogRecordAnno.class);
int operationType = logRecordAnno.operationType();
String userType = logRecordAnno.userType();
//执行方法,获取返回值
UserLoginResponse userLoginResponse = new UserLoginResponse();
//用户退出
if (2 == operationType) {
joinPoint.proceed();
} else {
userLoginResponse = (UserLoginResponse) joinPoint.proceed();
}
//获取请求IP
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ip = HttpUtils.getRemoteHost(request);
//组装参数值
Long userId = 1L;
if (ObjectUtils.isNotEmpty(userLoginResponse)) {
userId = userLoginResponse.getId();
} else {
//退出功能,从Token信息中取UserId
Optional userModelOptional = SecurityUtils.getCurrentUserModel();
if (userModelOptional.isPresent()) {
userId = userModelOptional.get().getId();
}
}
//线程池去执行保存登录日志
Long finalUserId = userId;
threadPoolExecutor.execute(() -> {
String userName = "用户名称异常";
//用户名称、等级名称赋值
switch (userType) {
case Constant.USER:
User user = userMapper.selectById(finalUserId);
if (ObjectUtils.isNotEmpty(user)) {
if (StringUtils.isNotBlank(user.getUserName())) {
userName = user.getUserName();
} else {
userName = user.getNickName();
}
}
break;
case Constant.ADMIN:
Admin admin = adminMapper.selectById(finalUserId);
if (ObjectUtils.isNotEmpty(admin)) {
userName = admin.getUserName();
}
break;
}
//记录操作日志
SecurityLog securityLog = new SecurityLog();
securityLog.setUserType(userType);
securityLog.setUserId(finalUserId);
securityLog.setUserName(userName);
securityLog.setLoginIp(ip);
if (operationType == 1) {
securityLog.setLogType(0);
securityLog.setDescription("成功登录");
} else {
securityLog.setLogType(1);
securityLog.setDescription("登出账户");
}
securityLogMapper.insert(securityLog);
});
return userLoginResponse;
}
@AfterThrowing(pointcut = "logPointcut()", throwing = "badRequestAlertException")
public void afterThrowable(JoinPoint joinPoint, BadRequestAlertException badRequestAlertException) {
try {
//获取方法属性
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
LogRecordAnno logRecordAnno = method.getAnnotation(LogRecordAnno.class);
Class<? extends LoginConvert> convert = logRecordAnno.convert();
LoginConvert logLoginConvert = convert.newInstance();
LoginLogVo loginLogVo = logLoginConvert.convert(joinPoint.getArgs()[0]);
String userType = logRecordAnno.userType();
SecurityLog securityLog = new SecurityLog();
securityLog.setUserType(userType);
securityLog.setUserId(1L);
securityLog.setUserName(loginLogVo.getUserName() + ":" + loginLogVo.getPassword());
securityLog.setLogType(3);
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ip = HttpUtils.getRemoteHost(request);
securityLog.setLoginIp(ip);
securityLog.setDescription("登录或退出失败,失败原因为:" + (badRequestAlertException.getDetail()));
securityLogMapper.insert(securityLog);
} catch (Exception e) {
log.error("登录或退出抛异常,AOP切面记录记录失败!");
}
}
@Override
public int getOrder() {
return 1;
}
}
至此,使用自定义注解+AOP+策略模式记录多端登录日志的功能已经开发完成,核心技术点在于如何在不影响主线业务的同时,用切面去异步地保存登录的日志。并且还是一个多用户类型、多终端的登录方式。其实,在其他相同场景的业务中,也可以这么记录日志或做其他操作。比如,多端创建订单、多商户处理订单、多层级审批工单等业务场景,都可以使用这种方式,去做相同的处理。你在公司中也遇到过类似的场景吗,去试一下吧!
所有你想象的一切,皆是现实!!!
更新时间:2024-08-27
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号