先看一下优化之前的登录代码:UserServiceImpl.java
package cn.edu.sgu.www.mhxysy.service.system.impl;
import cn.edu.sgu.www.mhxysy.consts.RedisKeyPrefixConst;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.entity.system.UserLoginLog;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.property.EmailProperties;
import cn.edu.sgu.www.mhxysy.property.SystemSettingsProperties;
import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import cn.edu.sgu.www.mhxysy.redis.StringRedisUtils;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import cn.edu.sgu.www.mhxysy.service.system.UserService;
import cn.edu.sgu.www.mhxysy.util.IpUtils;
import cn.edu.sgu.www.mhxysy.util.StringUtils;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* @author heyunlin
* @version 1.0
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
private final FeignService feignService;
private final JavaMailSender javaMailSender;
private final EmailProperties emailProperties;
private final RedisRepository redisRepository;
private final StringRedisUtils stringRedisUtils;
private final SystemSettingsProperties systemSettingsProperties;
@Autowired
public UserServiceImpl(
FeignService feignService,
JavaMailSender javaMailSender,
EmailProperties emailProperties,
RedisRepository redisRepository,
StringRedisUtils stringRedisUtils,
SystemSettingsProperties systemSettingsProperties) {
this.feignService = feignService;
this.javaMailSender = javaMailSender;
this.emailProperties = emailProperties;
this.redisRepository = redisRepository;
this.stringRedisUtils = stringRedisUtils;
this.systemSettingsProperties = systemSettingsProperties;
}
@Override
public void login(UserLoginDTO loginDTO) {
// 一、验证码判断
// 得到用户输入的验证码
String code = loginDTO.getCode();
// 获取正确的验证码
String uuid = loginDTO.getUuid();
String key = RedisKeyPrefixConst.PREFIX_CAPTCHA + uuid;
String realCode = stringRedisUtils.get(key);
// 得到的验证码为空,则获取验证码到登录之间的时间已经过了3分钟,验证码过期已经被删除
if (realCode == null) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码已失效,请刷新页面重新获取~");
}
// 验证码校验
if (!code.equalsIgnoreCase(realCode)) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码错误~");
}
// 二、登录流程
// 得到用户名
String username = loginDTO.getUsername();
log.debug("用户{}正在登录...", username);
// 查询用户信息,如果用户被锁定,提前退出
User user = feignService.selectByUsername(username);
if (user != null) {
if (user.getEnable()) {
// 1、shiro登录认证
UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
Subject subject = UserUtils.getSubject();
subject.login(token);
// 设置session失效时间:永不超时
subject.getSession().setTimeout(-1001);
// 2、修改管理员上一次登录时间
User usr = new User();
usr.setId(user.getId());
usr.setLastLoginTime(LocalDateTime.now());
feignService.updateById(usr);
// 3、邮件通知
if (emailProperties.isEnable()) {
new Thread(() -> {
// 定义日期格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
try {
String text = "您的账号" + username + "在广州登录了。" +
"[" + LocalDateTime.now(ZoneId.of(systemSettingsProperties.getZoneId())).format(formatter) + "]";
helper.setFrom(emailProperties.getFrom());
helper.setTo(emailProperties.getTo());
helper.setText(text);
javaMailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}).start();
}
// 4、如果开启了系统日志,添加管理员登录历史
if (systemSettingsProperties.isLoginLog()) {
UserLoginLog loginLog = new UserLoginLog();
loginLog.setId(StringUtils.uuid());
loginLog.setUserId(user.getId());
loginLog.setLoginIp(IpUtils.getIp());
loginLog.setLoginTime(LocalDateTime.now());
loginLog.setLoginHostName(IpUtils.getLocalHostName());
feignService.saveLoginLog(loginLog);
}
// 5、从redis中删除用户权限
redisRepository.delete(username);
// 6、查询用户的权限信息,并保存到redis
redisRepository.save(username);
} else {
throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
}
} else {
throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
}
}
}
因为刚好刷视频刷到了责任链模式,想着在项目上能不能应用上,刚好登录接口完美符合了使用场景。没有开启事务。
于是就开始着手设计处理链,首先创建一个接口,表示登录的处理器,里面定义了处理方法、设置参数和设置下一个处理器的方法。
起初,handle()方法上是有一个Object params参数的,后来通过整体需求,调整为将参数设置成成员变量,通过setParams()方法手动设置。然后在UserLoginHandler接口的具体实现类的handle()方法上调用下一个处理器的handler()方法(如果下一个处理器不为null)。
package cn.edu.sgu.www.mhxysy.chain.login;
/**
* 登录处理器
* @author heyunlin
* @version 1.0
*/
public interface UserLoginHandler {
void handle();
void setNext(UserLoginHandler next);
void setParams(Object params);
}
验证码判断的处理器CaptchaHandler.java
package cn.edu.sgu.www.mhxysy.chain.login;
import cn.edu.sgu.www.mhxysy.consts.RedisKeyPrefixConst;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.redis.StringRedisUtils;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class CaptchaHandler implements UserLoginHandler {
private Object params;
private UserLoginHandler next;
private final StringRedisUtils stringRedisUtils;
@Autowired
public CaptchaHandler(StringRedisUtils stringRedisUtils) {
this.stringRedisUtils = stringRedisUtils;
}
@Override
public void handle() {
UserLoginDTO loginDTO = (UserLoginDTO) params;
// 得到用户输入的验证码
String code = loginDTO.getCode();
// 获取正确的验证码
String uuid = loginDTO.getUuid();
String key = RedisKeyPrefixConst.PREFIX_CAPTCHA + uuid;
String realCode = stringRedisUtils.get(key);
// 得到的验证码为空,则获取验证码到登录之间的时间已经过了3分钟,验证码过期已经被删除
if (realCode == null) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码已失效,请刷新页面重新获取~");
}
// 验证码校验
if (!code.equalsIgnoreCase(realCode)) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码错误~");
}
if (next != null) {
next.handle();
}
}
@Override
public void setNext(UserLoginHandler next) {
this.next = next;
}
@Override
public void setParams(Object params) {
this.params = params;
}
}
负责shiro登录认证的处理器LoginHandler.java
package cn.edu.sgu.www.mhxysy.chain.login;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class LoginHandler implements UserLoginHandler {
private Object params;
private UserLoginHandler next;
@Override
public void handle() {
UserLoginDTO loginDTO = (UserLoginDTO) params;
// shiro登录认证
UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
Subject subject = UserUtils.getSubject();
subject.login(token);
// 设置session失效时间:永不超时
subject.getSession().setTimeout(-1001);
if (next != null) {
next.handle();
}
}
@Override
public void setNext(UserLoginHandler next) {
this.next = next;
}
@Override
public void setParams(Object params) {
this.params = params;
}
}
负责修改用户最后一次登录时间的处理器UserUpdateHandler.java
package cn.edu.sgu.www.mhxysy.chain.login;
import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class UserUpdateHandler implements UserLoginHandler {
private Object params;
private UserLoginHandler next;
private final FeignService feignService;
@Autowired
public UserUpdateHandler(FeignService feignService) {
this.feignService = feignService;
}
@Override
public void handle() {
String userId = (String) params;
User usr = new User();
usr.setId(userId);
usr.setLastLoginTime(LocalDateTime.now());
feignService.updateById(usr);
if (next != null) {
next.handle();
}
}
@Override
public void setNext(UserLoginHandler next) {
this.next = next;
}
@Override
public void setParams(Object params) {
this.params = params;
}
}
发送邮件通知的处理器EmailSendHandler.java
package cn.edu.sgu.www.mhxysy.chain.login;
import cn.edu.sgu.www.mhxysy.property.EmailProperties;
import cn.edu.sgu.www.mhxysy.property.SystemSettingsProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class EmailSendHandler implements UserLoginHandler {
private Object params;
private UserLoginHandler next;
private final JavaMailSender javaMailSender;
private final EmailProperties emailProperties;
private final SystemSettingsProperties systemSettingsProperties;
@Autowired
public EmailSendHandler(
JavaMailSender javaMailSender,
EmailProperties emailProperties,
SystemSettingsProperties systemSettingsProperties) {
this.javaMailSender = javaMailSender;
this.emailProperties = emailProperties;
this.systemSettingsProperties = systemSettingsProperties;
}
@Override
public void handle() {
if (emailProperties.isEnable()) {
new Thread(() -> {
// 定义日期格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
try {
String username = (String) params;
String text = "您的账号" + username + "在广州登录了。" +
"[" + LocalDateTime.now(ZoneId.of(systemSettingsProperties.getZoneId())).format(formatter) + "]";
helper.setFrom(emailProperties.getFrom());
helper.setTo(emailProperties.getTo());
helper.setText(text);
javaMailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}).start();
}
if (next != null) {
next.handle();
}
}
@Override
public void setNext(UserLoginHandler next) {
this.next = next;
}
@Override
public void setParams(Object params) {
this.params = params;
}
}
负责保存用户登录日志的处理器LoginLogHandler.java
package cn.edu.sgu.www.mhxysy.chain.login;
import cn.edu.sgu.www.mhxysy.entity.system.UserLoginLog;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.property.SystemSettingsProperties;
import cn.edu.sgu.www.mhxysy.util.IpUtils;
import cn.edu.sgu.www.mhxysy.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class LoginLogHandler implements UserLoginHandler {
private Object params;
private UserLoginHandler next;
private final FeignService feignService;
private final SystemSettingsProperties systemSettingsProperties;
@Autowired
public LoginLogHandler(
FeignService feignService, SystemSettingsProperties systemSettingsProperties) {
this.feignService = feignService;
this.systemSettingsProperties = systemSettingsProperties;
}
@Override
public void handle() {
if (systemSettingsProperties.isLoginLog()) {
String userId = (String) params;
UserLoginLog loginLog = new UserLoginLog();
loginLog.setId(StringUtils.uuid());
loginLog.setUserId(userId);
loginLog.setLoginIp(IpUtils.getIp());
loginLog.setLoginTime(LocalDateTime.now());
loginLog.setLoginHostName(IpUtils.getLocalHostName());
feignService.saveLoginLog(loginLog);
}
if (next != null) {
next.handle();
}
}
@Override
public void setNext(UserLoginHandler next) {
this.next = next;
}
@Override
public void setParams(Object params) {
this.params = params;
}
}
负责更新redis缓存的处理器
package cn.edu.sgu.www.mhxysy.chain.login;
import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class RedisCacheHandler implements UserLoginHandler {
private Object params;
private UserLoginHandler next;
private final RedisRepository redisRepository;
@Autowired
public RedisCacheHandler(RedisRepository redisRepository) {
this.redisRepository = redisRepository;
}
@Override
public void handle() {
String username = (String) params;
// 从redis中删除用户权限
redisRepository.delete(username);
// 查询用户的权限信息,并保存到redis
redisRepository.save(username);
if (next != null) {
next.handle();
}
}
@Override
public void setNext(UserLoginHandler next) {
this.next = next;
}
@Override
public void setParams(Object params) {
this.params = params;
}
}
最后,重构后的代码
package cn.edu.sgu.www.mhxysy.service.system.impl;
import cn.edu.sgu.www.mhxysy.chain.login.UserLoginHandler;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.dto.system.UserUpdateDTO;
import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import cn.edu.sgu.www.mhxysy.service.system.UserService;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author heyunlin
* @version 1.0
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
private final UserLoginHandler captchaHandler;
private final UserLoginHandler userUpdateHandler;
private final UserLoginHandler emailSendHandler;
private final UserLoginHandler loginLogHandler;
private final UserLoginHandler loginHandler;
private final UserLoginHandler redisCacheHandler;
private final FeignService feignService;
private final RedisRepository redisRepository;
@Autowired
public UserServiceImpl(
UserLoginHandler captchaHandler,
UserLoginHandler userUpdateHandler,
UserLoginHandler emailSendHandler,
UserLoginHandler loginLogHandler,
UserLoginHandler loginHandler,
UserLoginHandler redisCacheHandler,
FeignService feignService,
RedisRepository redisRepository) {
this.captchaHandler = captchaHandler;
this.userUpdateHandler = userUpdateHandler;
this.emailSendHandler = emailSendHandler;
this.loginLogHandler = loginLogHandler;
this.loginHandler = loginHandler;
this.redisCacheHandler = redisCacheHandler;
this.feignService = feignService;
this.redisRepository = redisRepository;
}
@Override
public void logout() {
// 删除角色的权限
redisRepository.delete(UserUtils.getLoginUsername());
// 注销
UserUtils.getSubject().logout();
}
@Override
public void login(UserLoginDTO loginDTO) {
// 一、验证码判断
captchaHandler.setParams(loginDTO);
captchaHandler.handle();
// 二、登录流程
// 得到用户名
String username = loginDTO.getUsername();
log.debug("用户{}正在登录...", username);
// 查询用户信息
User user = feignService.selectByUsername(username);
if (user != null) {
String userId = user.getId();
if (user.getEnable()) {
// 1、shiro登录认证
loginHandler.setNext(userUpdateHandler);
loginHandler.setParams(loginDTO);
// 2、修改管理员上一次登录时间
userUpdateHandler.setNext(emailSendHandler);
userUpdateHandler.setParams(userId);
// 3、邮件通知
emailSendHandler.setNext(loginLogHandler);
emailSendHandler.setParams(username);
// 4、如果开启了系统日志,添加管理员登录日志
loginLogHandler.setNext(redisCacheHandler);
loginLogHandler.setParams(userId);
// 5、从redis中删除并查询用户的权限保存到redis
redisCacheHandler.setParams(username); // 这是最后一个处理流程
// 开始执行处理链
loginHandler.handle();
} else {
// 用户被锁定,抛出异常
throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
}
} else {
throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
}
}
@Override
public void updatePass(UserUpdateDTO userUpdateDTO) {
feignService.updatePass(userUpdateDTO);
}
}
这样设计的好处有: