文章目录
一、启动后台服务
- 启动paascloud-eureka
- 启动paascloud-discovery
- 启动paascloud-provider-uac
- 启动paascloud-provider-tpc
- 启动paascloud-provider-mdc
- 启动paascloud-gateway

二、启动前台系统
1.启动paascloud-login-web系统


2.修复验证码不显示
正常启动后台系统之后(相关的域名改为localhost),前端请求网关地址7979能正常路由到/uac下,就可正常显示验证码

三、注册逻辑
1、获取验证码
路径:paascloud-login-web\src\views\auth\user-register\index.vue
1.页面请求
created () {
this.getImage();
},
mounted () {
},
methods: {
getImage () {
this.deviceId = new Date().getTime();
let that = this;
that.$http({
method: 'POST',
url: '/uac/auth/code/image',
headers: {
'deviceId': that.deviceId
}
}).then((res) => {
that.imageCode = 'data:image/jpg;base64,' + res.result;
});
},
......
}
在create()中调用getImage(),请求获取验证码的方法(上一篇已经介绍了)。
2.页面后台处理

调用paascloud-security-core模块下ValidateCodeController中
@PostMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{type}")
public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
validateCodeProcessorHolder.findValidateCodeProcessor(type).create(new ServletWebRequest(request, response));
}
//String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/auth/code";
findValidateCodeProcessor()
ValidateCodeProcessor findValidateCodeProcessor(String type) {
//1)获取name值
String name = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
//2)validateCodeProcessors处理器集合
ValidateCodeProcessor processor = validateCodeProcessors.get(name);
if (processor == null) {
throw new ValidateCodeException("验证码处理器" + name + "不存在");
}
return processor;
}
1)获取name值
ValidateCodeProcessor.class.getSimpleName();

在Class.java类中的getName()方法中,因为是ValidateCodeProcessor.class调用。所以this.name返回ValidateCodeProcessor字符串
public String getName() {
String name = this.name;
if (name == null)
this.name = name = getName0();
return name;
}
由此name = imageValidateCodeProcessor
2)validateCodeProcessors处理器集合
在ValidateCodeProcessorHolder.java中
private final Map<String, ValidateCodeProcessor> validateCodeProcessors;
@Autowired
public ValidateCodeProcessorHolder(Map<String, ValidateCodeProcessor> validateCodeProcessors) {
this.validateCodeProcessors = validateCodeProcessors;
}
可见validateCodeProcessors 处理器集合是成员变量,且被注入。@Autowired放在构造器上,是使用构造器的方式进行注入。


以EmailCodeProcessor为例,继承了AbstractValidateCodeProcessor类,AbstractValidateCodeProcessor实现了ValidateCodeProcessor。由此
在注入Map<String, ValidateCodeProcessor> 时,会将ValidateCodeProcessor接口下所有被spring管理的子类进行注入,且放入map中。
@Component("emailValidateCodeProcessor")
public class EmailCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {}
@Component("imageValidateCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {}
@Component("smsValidateCodeProcessor")
@Slf4j
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {}
Map<String, ValidateCodeProcessor> validateCodeProcessors 中,Map集合中的String表示Component注册spring的名称,接口表示具体类。
a)ValidateCodeGenerator处理器集合
以ValidateCodeProcessor子类中的ImageCodeProcessor为例,构造函数:
public ImageCodeProcessor(Map<String, ValidateCodeGenerator> validateCodeGenerators, ValidateCodeRepository validateCodeRepository) {
super(validateCodeGenerators, validateCodeRepository);
}
super()方法调用父类AbstractValidateCodeProcessor中的构造方法
/**
* 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
*/
private final Map<String, ValidateCodeGenerator> validateCodeGenerators;
private final ValidateCodeRepository validateCodeRepository;
@Autowired
public AbstractValidateCodeProcessor(Map<String, ValidateCodeGenerator> validateCodeGenerators,
ValidateCodeRepository validateCodeRepository) {
this.validateCodeGenerators = validateCodeGenerators;
this.validateCodeRepository = validateCodeRepository;
}
和ValidateCodeProcessors一样,Map<String, ValidateCodeGenerator> validateCodeGenerators注入的也是ValidateCodeGenerator接口的被spring管理的所有子类。

@Component("emailValidateCodeGenerator")
public class EmailCodeGenerator implements ValidateCodeGenerator {}
public class ImageCodeGenerator implements ValidateCodeGenerator {}
@Component("smsValidateCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {}
Map<String, ValidateCodeGenerator> validateCodeGenerators 中key就是各实现类中Component的name,value就是各个子实现类。
注意:这里的ImageCodeGenerator 类没有用@Component注解修饰,类中成员变量SecurityProperties 和Producer 也没使用@Autowired修饰,表示隐式注入。
public class ImageCodeGenerator implements ValidateCodeGenerator {
private SecurityProperties securityProperties;
private Producer captchaProducer;
}
等价于:
@Component("imageValidateCodeGenerator")
public class ImageCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private Producer captchaProducer;
}
还是以ImageCodeGenerator 为例
public class ImageCodeGenerator implements ValidateCodeGenerator {
private SecurityProperties securityProperties;
private Producer captchaProducer;
/**
* 生成图片验证码.
*
* @param request the request
*
* @return the image code
*/
@Override
public ImageCode generate(ServletWebRequest request) {
String kaptchaCode = captchaProducer.createText();
BufferedImage image = captchaProducer.createImage(kaptchaCode);
return new ImageCode(image, kaptchaCode, securityProperties.getCode().getImage().getExpireIn());
}
/**
* Sets security properties.
*
* @param securityProperties the security properties
*/
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
/**
* Sets captcha producer.
*
* @param captchaProducer the captcha producer
*/
public void setCaptchaProducer(Producer captchaProducer) {
this.captchaProducer = captchaProducer;
}
主要是对成员变量securityProperties和captchaProducer进行赋值。
b)SecurityProperties 安全配置
@ConfigurationProperties(prefix = "paascloud.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
private SocialProperties social = new SocialProperties();
private OAuth2Properties oauth2 = new OAuth2Properties();
......
}
paascloud:
security:
code:
image:
expireIn: 180
url: /auth/register,/auth/submitResetPwdEmail,/auth/code/sms
sms:
expireIn: 600
url: /uac/auth/submitResetPwdPhone
mobileMaxSendCount: 1
ipMaxSendCount: 2
totalMaxSendCount: 3
SecurityProperties 主要对成员类 ValidateCodeProperties进行赋值,在 @ConfigurationProperties修饰下,相对应的配置在yml中paascloud.security下。将paascloud.security.code中的值赋值到ValidateCodeProperties 中
c)ValidateCodeProperties配置
@Data
public class ValidateCodeProperties {
/**
* 图片验证码配置
*/
private ImageCodeProperties image = new ImageCodeProperties();
/**
* 短信验证码配置
*/
private SmsCodeProperties sms = new SmsCodeProperties();
/**
* 邮箱验证码配置
*/
private EmailCodeProperties email = new EmailCodeProperties();
}
ValidateCodeProperties 有三个子成员类,其中ImageCodeProperties继承SmsCodeProperties
@Data
public class SmsCodeProperties {
/**
* 验证码长度
*/
private int length = 6;
/**
* 过期时间
*/
private int expireIn = 60;
/**
* 要拦截的url,多个url用逗号隔开,ant pattern
*/
private String url;
/**
* 每天每个手机号最大送送短信数量
*/
private int mobileMaxSendCount;
/**
* 每天每个IP最大送送短信数量
*/
private int ipMaxSendCount;
/**
* 每天最大送送短信数量
*/
private int totalMaxSendCount;
}
所以paascloud.security.code.sms封装到SmsCodeProperties中,paascloud.security.code.image封装到ImageCodeProperties 中,由于没有配置paascloud.security.code.email 所以EmailCodeProperties 对应默认参数。
综上,validateCodeProcessors表示三个加载器的集合,三个加载器分别为
- emailValidateCodeProcessor
- imageValidateCodeProcessor
- smsValidateCodeProcessor


validateCodeProcessorHolder.findValidateCodeProcessor(type)方法返回的就是ImageCodeProcessor对象,但是是ValidateCodeProcessor的引用
create()
实际是调用AbstractValidateCodeProcessor的create方法。
@Override
public void create(ServletWebRequest request) throws Exception {
C validateCode = generate(request);
save(request, validateCode);
send(request, validateCode);
}
1)generate()
private C generate(ServletWebRequest request) {
String type = getValidateCodeType().toString().toLowerCase();
String generatorName = type + ValidateCodeGenerator.class.getSimpleName();
ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
if (validateCodeGenerator == null) {
throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");
}
return (C) validateCodeGenerator.generate(request);
}
通过类型调用不同的ValidateCodeGenerator 验证码生成器。这次是ImageCodeGenerator。
然后调用ImageCodeGenerator类中的generate方法,返回一张带有验证码的图片对象。
@Override
public ImageCode generate(ServletWebRequest request) {
String kaptchaCode = captchaProducer.createText();
BufferedImage image = captchaProducer.createImage(kaptchaCode);
return new ImageCode(image, kaptchaCode, securityProperties.getCode().getImage().getExpireIn());
}
2)save()
也是调用AbstractValidateCodeProcessor中的save()方法
private final ValidateCodeRepository validateCodeRepository;
private void save(ServletWebRequest request, C validateCode) {
ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());
validateCodeRepository.save(request, code, getValidateCodeType());
}
使用validateCodeRepository进行保存
@Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public RedisValidateCodeRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType type) {
String key = buildKey(request, type);
redisTemplate.opsForValue().set(key, code, 3, TimeUnit.MINUTES);
}
//生成redis中的key
private String buildKey(ServletWebRequest request, ValidateCodeType type) {
String deviceId = request.getHeader("deviceId");
if (StringUtils.isBlank(deviceId)) {
throw new ValidateCodeException("请在请求头中携带deviceId参数");
}
return "code:" + type.toString().toLowerCase() + ":" + deviceId;
}
}
注入的ValidateCodeRepository 实际上是RedisValidateCodeRepository 这个实现类。此时调用的就是RedisValidateCodeRepository 中的save方法,将验证码进行保存。


3)send()
调用ImageCodeProcessor类中的send方法将验证码写回
@Override
protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(imageCode.getImage(), "JPEG", bos);
SecurityResult result = SecurityResult.ok(bos.toByteArray());
String json = objectMapper.writeValueAsString(result);
HttpServletResponse response = request.getResponse();
response.setCharacterEncoding("UTF-8");
response.getWriter().write(json);
}
2、注册
1.页面请求
<button class="register-btn" @click="submit">注册</button>
submit () {
let validateResult = this.formValidate();
// 验证成功
if (validateResult.status) {
this.register(() => {
this.$store.dispatch('new_notice', {
autoClose: true,
content: '注册成功, 请登录邮箱激活用户'
});
this.loadPage('Login');
});
} else {
this.$store.dispatch('new_notice', {
autoClose: true,
content: validateResult.msg
});
}
},
register (resolve) {
this.$http({
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'deviceId': this.deviceId
},
url: '/uac/auth/register',
params: this.registerForm
}).then((res) => {
this.getImage();
if (res) {
resolve(res);
}
}).catch((err) => {
console.log(err);
});
},
2.页面后台处理
Controller处理类所在位置
//AuthRestController.java
@PostMapping(value = "/register")
@ApiOperation(httpMethod = "POST", value = "注册用户")
public Wrapper registerUser(UserRegisterDto user) {
uacUserService.register(user);
return WrapMapper.ok();
}
UacUserServiceImpl.java/register()
@Override
public void register(UserRegisterDto registerDto) {
// 1.校验前端页面封装的注册信息
validateRegisterInfo(registerDto);
String mobileNo = registerDto.getMobileNo();
String email = registerDto.getEmail();
Date row = new Date();
//2.雪花算法生成分布式唯一ID
String salt = String.valueOf(generateId());
long id = generateId();
UacUser uacUser = new UacUser();
uacUser.setLoginName(registerDto.getLoginName());
uacUser.setSalt(salt);
uacUser.setLoginPwd(Md5Util.encrypt(registerDto.getLoginPwd()));
uacUser.setMobileNo(mobileNo);
//设置状态为不可用 DISABLE
uacUser.setStatus(UacUserStatusEnum.DISABLE.getKey());
//设置状态为不可用 DISABLE
uacUser.setUserSource(UacUserSourceEnum.REGISTER.getKey());
uacUser.setCreatedTime(row);
uacUser.setUpdateTime(row);
uacUser.setEmail(email);
uacUser.setId(id);
uacUser.setCreatorId(id);
uacUser.setCreator(registerDto.getLoginName());
uacUser.setLastOperatorId(id);
uacUser.setUserName(registerDto.getLoginName());
uacUser.setLastOperator(registerDto.getLoginName());
//3.生成activeToken并保存redis
String activeToken = PubUtils.uuid() + super.generateId();
redisService.setKey(RedisKeyUtil.getActiveUserKey(activeToken), email, 1, TimeUnit.DAYS);
Map<String, Object> param = Maps.newHashMap();
param.put("loginName", registerDto.getLoginName());
param.put("email", registerDto.getEmail());
param.put("activeUserUrl", activeUserUrl + activeToken);
param.put("dateTime", DateUtil.formatDateTime(new Date()));
Set<String> to = Sets.newHashSet();
to.add(registerDto.getEmail());
//4.发送激活邮件
MqMessageData mqMessageData = emailProducer.sendEmailMq(to, UacEmailTemplateEnum.ACTIVE_USER, AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER, param);
userManager.register(mqMessageData, uacUser);
}
1.校验前端页面封装的注册信息(validateRegisterInfo)
// 1.校验前端页面封装的注册信息
validateRegisterInfo(registerDto);
validateRegisterInfo方法主要校验前端传入的参数,并根据用户名、手机号、Email查询是否已经存在。
UacUser uacUser = new UacUser();
uacUser.setLoginName(registerDto.getLoginName());
int count = uacUserMapper.selectCount(uacUser);
if (count > 0) {
throw new UacBizException(ErrorCodeEnum.UAC10011012);
}
uacUser = new UacUser();
uacUser.setMobileNo(registerDto.getMobileNo());
count = uacUserMapper.selectCount(uacUser);
if (count > 0) {
throw new UacBizException(ErrorCodeEnum.UAC10011013);
}
uacUser = new UacUser();
uacUser.setEmail(registerDto.getEmail());
count = uacUserMapper.selectCount(uacUser);
if (count > 0) {
throw new UacBizException(ErrorCodeEnum.UAC10011019);
}
2.雪花算法生成分布式唯一ID(generateId)
//2.雪花算法生成分布式唯一ID
String salt = String.valueOf(generateId());
long id = generateId();
generateId()调用抽象类BaseService.java中的
protected long generateId() {
return UniqueIdGenerator.getInstance(IncrementIdGenerator.getServiceId()).nextId();
}
1)IncrementIdGenerator.getServiceId()
IncrementIdGenerator.java 所属paascloud-common-zk模块下
private static Long serviceId = null;
public static Long getServiceId() {
return serviceId;
}

在paascloud-common-core模块下的ZookeeperInitRunner.java中
@Component
@Order
@Slf4j
public class ZookeeperInitRunner implements CommandLineRunner {
@Resource
private PaascloudProperties paascloudProperties;
@Value("${spring.application.name}")
private String applicationName;
@Override
public void run(String... args) throws Exception {
String hostAddress = InetAddress.getLocalHost().getHostAddress();
log.info("###ZookeeperInitRunner,init. HostAddress={}, applicationName={}", hostAddress, applicationName);
RegistryCenterFactory.startup(paascloudProperties, hostAddress, applicationName);
log.info("###ZookeeperInitRunner,finish<<<<<<<<<<<<<");
}
}
a)ZookeeperInitRunner.run()
因为ZookeeperInitRunner 实现CommandLineRunner 接口,并且实现了run方法。paascloud-provider-uac的父pom是 paascloud-provide 。paascloud-provid的pom文件中依赖paascloud-common-core。所以随着paascloud-provider-uac模块启动,spring容器加载ZookeeperInitRunner 并运行run方法。

在run方法中调用RegistryCenterFactory.startup();
b)RegistryCenterFactory.startup();
//paascloudProperties:yml文件中GlobalConstant.ROOT_PREFIX(paascloud)路径下的配置
//host:此时就是10.10.110.120
//app:yml文件中spring.application.name的配置
public static void startup(PaascloudProperties paascloudProperties, String host, String app) {
CoordinatorRegistryCenter coordinatorRegistryCenter = createCoordinatorRegistryCenter(paascloudProperties.getZk());
RegisterDto dto = new RegisterDto(app, host, coordinatorRegistryCenter);
Long serviceId = new IncrementIdGenerator(dto).nextId();
IncrementIdGenerator.setServiceId(serviceId);
registerMq(paascloudProperties, host, app);
}
paascloudProperties.getZk() 在获取的是paascloud路径下的配置。
在application-dev.yml文件中配置。返回ZookeeperProperties对象。

c)createCoordinatorRegistryCenter()
public static CoordinatorRegistryCenter createCoordinatorRegistryCenter(ZookeeperProperties zookeeperProperties) {
//将zk的连接地址进行hash处理
Hasher hasher = Hashing.md5().newHasher().putString(zookeeperProperties.getZkAddressList(), Charsets.UTF_8);
HashCode hashCode = hasher.hash();
//在注册中心中查找已经hash的连接地址
//ConcurrentHashMap<HashCode, CoordinatorRegistryCenter> REG_CENTER_REGISTRY其实就是一个Map
CoordinatorRegistryCenter result = REG_CENTER_REGISTRY.get(hashCode);
//如果没有就新建一个ZookeeperRegistryCenter
if (null != result) {
return result;
}
result = new ZookeeperRegistryCenter(zookeeperProperties);
//调用init()方法注册zk
result.init();
//加入map
REG_CENTER_REGISTRY.put(hashCode, result);
return result;
}
d)new IncrementIdGenerator(dto).nextId()
new IncrementIdGenerator(dto)其实就是通过构造方法,给registerDto赋值,在nextId()中使用。
public Long nextId() {
//获取构造函数赋值之后的内容
String app = this.registerDto.getApp();//paascloud-provider-uac
String host = this.registerDto.getHost();//10.10.110.120
CoordinatorRegistryCenter regCenter = this.registerDto.getCoordinatorRegistryCenter();
//ZK_REGISTRY_ID_ROOT_PATH = "/paascloud/registry/id"
//SLASH = "/"
//path = "/paascloud/registry/id/paascloud-provider-uac/10.11.320.120"
String path = GlobalConstant.ZK_REGISTRY_ID_ROOT_PATH + GlobalConstant.Symbol.SLASH + app + GlobalConstant.Symbol.SLASH + host;
if (regCenter.isExisted(path)) {
// 如果已经有该节点,表示已经为当前的host上部署的该app分配的编号(应对某个服务重启之后编号不变的问题),直接获取该id,而无需生成
return Long.valueOf(regCenter.getDirectly(GlobalConstant.ZK_REGISTRY_ID_ROOT_PATH + GlobalConstant.Symbol.SLASH + app + GlobalConstant.Symbol.SLASH + host));
} else {
// 节点不存在,那么需要生成id,利用zk节点的版本号每写一次就自增的机制来实现
//GlobalConstant.ZK_REGISTRY_SEQ = "/paascloud/seq"
//创建zk自增节点
regCenter.increment(GlobalConstant.ZK_REGISTRY_SEQ, new RetryNTimes(2000, 3));
// 创建节点id
Integer id = regCenter.getAtomicValue(GlobalConstant.ZK_REGISTRY_SEQ, new RetryNTimes(2000, 3)).postValue();
// 将数据写入节点
regCenter.persist(path);
regCenter.persist(path, String.valueOf(id));
return Long.valueOf(id);
}
}
nextId()就是通过在zk上注册节点,生成服务注册的id,以便通过zk给不同服务不同节点生成分布式唯一id
最后IncrementIdGenerator.setServiceId(serviceId);将生成的 id赋值给IncrementIdGenerator类中的serviceId。
由此,uac模块在启动的时候就连接好了zk,并复制了serviceId,最后IncrementIdGenerator.getServiceId()就能获取了。

2)UniqueIdGenerator.getInstance().nextId()
a)UniqueIdGenerator.getInstance()
对UniqueIdGenerator进行加锁操作,防止出现相同id的情况,此时appHostId=1,调用new UniqueIdGenerator(appHostId)对成员变量进行赋值。
public static UniqueIdGenerator getInstance(long appHostId) {
if (idGen == null) {
synchronized (UniqueIdGenerator.class) {
if (idGen == null) {
idGen = new UniqueIdGenerator(appHostId);
}
}
}
return idGen;
}
private UniqueIdGenerator(long appHostId) {
if (appHostId > MAX_APP_HOST_ID) {
// zk分配的serviceId过大(基本小规模的公司不会出现这样的问题)
throw new IllegalArgumentException(String.format("app host Id wrong: %d ", appHostId));
}
this.appHostId = appHostId;
}
b)nextId()
没有具体研究,应该可以简单理解为时间戳。
public Long nextId() {
return this.genUniqueId();
}
/**
* 生成唯一id的具体实现
*/
private synchronized long genUniqueId() {
long current = System.currentTimeMillis();
if (current < lastTimestamp) {
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,出现问题返回-1
return -1;
}
if (current == lastTimestamp) {
// 如果当前生成id的时间还是上次的时间,那么对sequence序列号进行+1
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == MAX_SEQUENCE) {
// 当前毫秒生成的序列数已经大于最大值,那么阻塞到下一个毫秒再获取新的时间戳
current = this.nextMs(lastTimestamp);
}
} else {
// 当前的时间戳已经是下一个毫秒
sequence = 0L;
}
// 更新上次生成id的时间戳
lastTimestamp = current;
// 进行移位操作生成int64的唯一ID
return ((current - START_TIME) << TIMESTAMP_LEFT_SHIFT)
| (this.appHostId << APP_HOST_ID_SHIFT)
| sequence;
}
3.生成activeToken并保存redis
//3.生成activeToken并保存redis
String activeToken = PubUtils.uuid() + super.generateId();
redisService.setKey(RedisKeyUtil.getActiveUserKey(activeToken), email, 1, TimeUnit.DAYS);
PubUtils.uuid()就是生成随机的randomUUID。super.generateId()就是雪花算法生成的分布式id。
1)getActiveUserKey()
getActiveUserKey() 返回的activeToken值为paascloud:activeUser:xxxx
public static String getActiveUserKey(String activeToken) {
Preconditions.checkArgument(StringUtils.isNotEmpty(activeToken), "参数不能为空");
//ACTIVE_USER = "paascloud:activeUser"
return ACTIVE_USER + ":" + activeToken;
}
2)setKey()
@Override
public void setKey(String key, String value, long timeout, TimeUnit unit) {
Preconditions.checkArgument(StringUtils.isNotEmpty(key), "Redis key is not null");
Preconditions.checkArgument(unit != null, "TimeUnit is not null");
ValueOperations<String, String> ops = rt.opsForValue();
ops.set(key, value);
rt.expire(key, timeout, unit);
log.info("setKey. [OK] key={}, value={}, timeout={}, unit={}", key, value, timeout, unit);
}
setKey()中key就是生成的activeToken,value就是注册的email。过期时间为1天。
4.发送激活邮件
发送邮件是通过rocketMq消息队列去完成的
//4.发送激活邮件
//to 发送邮件的邮箱地址数组
//UacEmailTemplateEnum.ACTIVE_USER("ACTIVE_USER", "注册用户-等待激活用户", "email/activeUserTemplate.ftl"),
//AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER("ACTIVE_USER", MqTopicEnum.SEND_EMAIL_TOPIC.getTopic(), "激活用户"),
//param就是发送邮件的其他参数信息
MqMessageData mqMessageData = emailProducer.sendEmailMq(to, UacEmailTemplateEnum.ACTIVE_USER, AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER, param);
userManager.register(mqMessageData, uacUser);
1)sendEmailMq()
public MqMessageData sendEmailMq(Set<String> emailSet, UacEmailTemplateEnum emailTemplateEnum, AliyunMqTopicConstants.MqTagEnum tagEnum, Map<String, Object> param) {
log.info("pcSendEmailRequest - 发送邮件MQ. emailSet={}, param={}", emailSet, param);
String msgBody;
try {
Preconditions.checkArgument(emailTemplateEnum != null, "邮箱模板信息不存在");
//构建请求
PcSendEmailRequest request = new PcSendEmailRequest();
//通过传入的参数获取邮件文本内容
//emailTemplateEnum:("ACTIVE_USER", "注册用户-等待激活用户", "email/activeUserTemplate.ftl")
String templateLocation = emailTemplateEnum.getLocation();//email/activeUserTemplate.ftl
String text = uacFreeMarkerService.getTemplate(param, templateLocation);
request.setText(text);
request.setTo(emailSet);
request.setSubject(emailTemplateEnum.getSubject());//"注册用户-等待激活用户"
//将请求转化成json
msgBody = JSON.toJSONString(request);
} catch (Exception e) {
log.error("发送邮件验证码 smsMessage转换为json字符串失败", e);
throw new UacBizException(ErrorCodeEnum.UAC10011021);
}
//tagEnum:ACTIVE_USER("ACTIVE_USER", MqTopicEnum.SEND_EMAIL_TOPIC.getTopic(), "激活用户"),
//SEND_EMAIL_TOPIC("SEND_EMAIL_TOPIC", "发送邮件"),
String topic = tagEnum.getTopic();//"SEND_EMAIL_TOPIC"
String tag = tagEnum.getTag();//"ACTIVE_USER"
String key = RedisKeyUtil.createMqKey(topic, tag, emailSet.toString(), msgBody);
return new MqMessageData(msgBody, topic, tag, key);
}
a)createMqKey()
//topic:"SEND_EMAIL_TOPIC"
//tag:"ACTIVE_USER"
//refNo:发送的目的地邮箱地址
//body:邮件内容json
public static String createMqKey(String topic, String tag, String refNo, String body) {
String temp = refNo;
//...省略检查
if (refNo.length() > REF_NO_MAX_LENGTH) {//REF_NO_MAX_LENGTH = 100
//如果发送的邮件地址大于100个 才进入
temp = StringUtils.substring(refNo, 0, REF_NO_MAX_LENGTH) + "...";
}
return topic + "_" + tag + "_" + temp + "-" + body.hashCode();
}
createMqKey()就是组合传入参数进行组装,返回一个存入redis中的key。
b)new MqMessageData()
就是将sendEmailMq()方法中的内容封装成一个MqMessageData对象。
2)register() 调用切面方法
在UserManager.java中register方法
@MqProducerStore
public void register(final MqMessageData mqMessageData, final UacUser uacUser) {
log.info("注册用户. mqMessageData={}, user={}", mqMessageData, uacUser);
//@MqProducerStore切面完成调用joinPoint.proceed()后,执行最后保存用户信息到pc_uac_user表中
uacUserMapper.insertSelective(uacUser);
}
因为用@MqProducerStore修饰,在进入register()方法之前,首先进入切面MqProducerStoreAspect.java中的processMqProducerStoreJoinPoint方法
//MqProducerStoreAspect.java
@Pointcut("@annotation(com.paascloud.provider.annotation.MqProducerStore)")
public void mqProducerStoreAnnotationPointcut() {
}
@Value("${paascloud.aliyun.rocketMq.producerGroup}")
private String producerGroup; //在uac-dev.yml中为PID_UAC
@Around(value = "mqProducerStoreAnnotationPointcut()")
public Object processMqProducerStoreJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("processMqProducerStoreJoinPoint - 线程id={}", Thread.currentThread().getId());
Object result;
//args表示传入切面修饰方法的参数 在register()方法中[mqMessageData uacUser]
Object[] args = joinPoint.getArgs();
//通过getAnnotation()方法获取注解对象
MqProducerStore annotation = getAnnotation(joinPoint);
//annotation.sendType()返回默认 MqSendTypeEnum.WAIT_CONFIRM
MqSendTypeEnum type = annotation.sendType();
//annotation.orderType()返回MqOrderTypeEnum.ORDER //MqOrderTypeEnum.orderType()返回默认的ORDER(1),表示有序
int orderType = annotation.orderType().orderType();
//annotation.delayLevel()返回DelayLevelEnum.ZERO ZERO(0, "不延时"),
DelayLevelEnum delayLevelEnum = annotation.delayLevel();
if (args.length == 0) {
throw new TpcBizException(ErrorCodeEnum.TPC10050005);
}
//获取传入register()方法中的MqMessageData 参数
MqMessageData domain = null;
for (Object object : args) {
if (object instanceof MqMessageData) {
domain = (MqMessageData) object;
break;
}
}
if (domain == null) {
throw new TpcBizException(ErrorCodeEnum.TPC10050005);
}
domain.setOrderType(orderType);//orderType = 1
domain.setProducerGroup(producerGroup);//producerGroup = PID_UAC
//此时type = MqSendTypeEnum.WAIT_CONFIRM
if (type == MqSendTypeEnum.WAIT_CONFIRM) {
//此时delayLevelEnum = DelayLevelEnum.ZERO
if (delayLevelEnum != DelayLevelEnum.ZERO) {
domain.setDelayLevel(delayLevelEnum.delayLevel());
}
//(1)执行saveWaitConfirmMessage
mqMessageService.saveWaitConfirmMessage(domain);
}
//(2)joinPoint.proceed() 返回注解方法,执行业务
result = joinPoint.proceed();
if (type == MqSendTypeEnum.SAVE_AND_SEND) {
mqMessageService.saveAndSendMessage(domain);
} else if (type == MqSendTypeEnum.DIRECT_SEND) {
mqMessageService.directSendMessage(domain);
} else {
//(3) type = WAIT_CONFIRM 执行confirmAndSendMessage
final MqMessageData finalDomain = domain;
taskExecutor.execute(() -> mqMessageService.confirmAndSendMessage(finalDomain.getMessageKey()));
}
return result;
}
a)执行saveWaitConfirmMessage()
在MqMessageServiceImpl.java中
@Override
@Transactional(rollbackFor = Exception.class)
public void saveWaitConfirmMessage(final MqMessageData mqMessageData) {
//保存消息到pc_mq_message_data表中
this.saveMqProducerMessage(mqMessageData);
// 发送预发送状态的消息给消息中心
TpcMqMessageDto tpcMqMessageDto = mqMessageData.getTpcMqMessageDto();
//远程调用,保存消息到pc_tpc_mq_message表中
tpcMqMessageFeignApi.saveMessageWaitingConfirm(tpcMqMessageDto);
log.info("<== saveWaitConfirmMessage - 存储预发送消息成功. messageKey={}", mqMessageData.getMessageKey());
}
saveMqProducerMessage方法通过mqMessageDataMapper将消息内容保存到pc_mq_message_data表中。
@Override
public void saveMqProducerMessage(MqMessageData mqMessageData) {
// 校验消息数据
this.checkMessage(mqMessageData);
// 先保存消息
mqMessageData.setMessageType(MqMessageTypeEnum.PRODUCER_MESSAGE.messageType());//PRODUCER_MESSAGE(10, "生产者"), type = 10
mqMessageData.setId(UniqueIdGenerator.generateId());
mqMessageDataMapper.insertSelective(mqMessageData);
}
远程调用tpcMqMessageFeignApi.saveMessageWaitingConfirm();
//调用paascloud-provider-tpc服务
@FeignClient(value = "paascloud-provider-tpc", configuration = OAuth2FeignAutoConfiguration.class, fallback = TpcMqMessageFeignApiHystrix.class)
public interface TpcMqMessageFeignApi {
@PostMapping(value = "/api/tpc/saveMessageWaitingConfirm")
Wrapper saveMessageWaitingConfirm(@RequestBody TpcMqMessageDto mqMessageDto);
}

//paascloud-provider-tpc模块下TpcMqMessageFeignClient.java
@Override
@ApiOperation(httpMethod = "POST", value = "预存储消息")
public Wrapper saveMessageWaitingConfirm(@RequestBody TpcMqMessageDto mqMessageDto) {
logger.info("预存储消息. mqMessageDto={}", mqMessageDto);
//保存到pc_tpc_mq_message表中
tpcMqMessageService.saveMessageWaitingConfirm(mqMessageDto);
return WrapMapper.ok();
}
将消息内容存储到pc_tpc_mq_message表中
综上,saveWaitConfirmMessage()方法将消息内容保存到了
pc_mq_message_data和pc_tpc_mq_message表中
且用@Transactional修饰,表示两次保存是同一个事务。
b)joinPoint.proceed() 返回注解方法,执行业务
调用joinPoint.proceed()就执行@MqProducerStore修饰方法的方法体。
@MqProducerStore
public void register(final MqMessageData mqMessageData, final UacUser uacUser) {
log.info("注册用户. mqMessageData={}, user={}", mqMessageData, uacUser);
//@MqProducerStore切面完成调用joinPoint.proceed()后,执行最后保存用户信息到pc_uac_user表中
uacUserMapper.insertSelective(uacUser);
}
将UacUser 中封装的注册人信息通过uacUserMapper保存到pc_uac_user表中
执行完register()方法后,依然返回切面方法中。
c)type = WAIT_CONFIRM 执行confirmAndSendMessage
final MqMessageData finalDomain = domain;
//finalDomain.getMessageKey()其实就是MqMessageData 的messageKey。也就是
//createMqKey()生成的 topic + "_" + tag + "_" + temp + "-" + body.hashCode();
taskExecutor.execute(() -> mqMessageService.confirmAndSendMessage(finalDomain.getMessageKey()));
//MqMessageServiceImpl.java中
@Override
public void confirmAndSendMessage(String messageKey) {
// 发送确认消息给消息中心
try {
Wrapper wrapper = tpcMqMessageFeignApi.confirmAndSendMessage(messageKey);
//.....
}
远程调用confirmAndSendMessage方法。
@FeignClient(value = "paascloud-provider-tpc", configuration = OAuth2FeignAutoConfiguration.class, fallback = TpcMqMessageFeignApiHystrix.class)
public interface TpcMqMessageFeignApi {
@PostMapping(value = "/api/tpc/confirmAndSendMessage")
Wrapper confirmAndSendMessage(@RequestParam("messageKey") String messageKey);
}
TpcMqMessageFeignClient.java
@Override
@ApiOperation(httpMethod = "POST", value = "确认并发送消息")
public Wrapper confirmAndSendMessage(@RequestParam("messageKey") String messageKey) {
logger.info("确认并发送消息. messageKey={}", messageKey);
tpcMqMessageService.confirmAndSendMessage(messageKey);
return WrapMapper.ok();
}
调用confirmAndSendMessage()
@Override
public void confirmAndSendMessage(String messageKey) {
//通过messageKey查询pc_tpc_mq_message表中的数据
final TpcMqMessage message = tpcMqMessageMapper.getByMessageKey(messageKey);
if (message == null) {
throw new TpcBizException(ErrorCodeEnum.TPC10050002);
}
TpcMqMessage update = new TpcMqMessage();
//设置发送状态为Sending
update.setMessageStatus(MqSendStatusEnum.SENDING.sendStatus());
update.setId(message.getId());
update.setUpdateTime(new Date());
//第一步:
//主要是修改pc_tpc_mq_message表中的MessageStatus为Sending
tpcMqMessageMapper.updateByPrimaryKeySelective(update);
//第二步:
// 创建消费待确认列表
this.createMqConfirmListByTopic(message.getMessageTopic(), message.getId(), message.getMessageKey());
// 直接发送消息
this.directSendMessage(message.getMessageBody(), message.getMessageTopic(),
message.getMessageTag(), message.getMessageKey(),
message.getProducerGroup(), message.getDelayLevel());
}
第一步:使用tpcMqMessageMapper将pc_tpc_mq_message表中已存入的数据进行修改,其中将MessageStatus设置Sending。
第二步:调用this.createMqConfirmListByTopic()创建消费待确认列表。将消息等待对象保存到pc_tpc_mq_confirm表中
public void createMqConfirmListByTopic(final String topic, final Long messageId, final String messageKey) {
// topic :"SEND_EMAIL_TOPIC"
//messageKey:reateMqKey()生成的 topic + "_" + tag + "_" + temp + "-" + body.hashCode();
List<TpcMqConfirm> list = Lists.newArrayList();
TpcMqConfirm tpcMqConfirm;
//SELECT consumer_code from pc_tpc_mq_subscribe WHERE topic_code = #{topic}
//通过topic查询pc_tpc_mq_subscribe表,查到所有订阅该topic的消费者组
List<String> consumerGroupList = tpcMqConsumerService.listConsumerGroupByTopic(topic);
if (PublicUtil.isEmpty(consumerGroupList)) {
throw new TpcBizException(ErrorCodeEnum.TPC100500010, topic);
}
for (final String cid : consumerGroupList) {
//遍历消费者组,得到所有的消费者,封装到TpcMqConfirm对象中
tpcMqConfirm = new TpcMqConfirm(UniqueIdGenerator.generateId(), messageId, messageKey, cid);
list.add(tpcMqConfirm);
}
//将所有的TpcMqConfirm对象批量保存到pc_tpc_mq_confirm表中
tpcMqConfirmMapper.batchCreateMqConfirm(list);
}
第三步:使用this.directSendMessage()发送消息
@Override
public void directSendMessage(String body, String topic, String tag, String key, String pid, Integer delayLevel) {
RocketMqProducer.sendSimpleMessage(body, topic, tag, key, pid, delayLevel);
}
RocketMqProducer.java
private static final int PRODUCER_RETRY_TIMES = 3;
public static SendResult sendSimpleMessage(String body, String topic, String tag, String key, String pid, Integer delayLevel) {
//...
return retrySendMessage(pid, message);
}
private static SendResult retrySendMessage(String pid, Message msg) {
int iniCount = 1;
SendResult result;
while (true) {
try {
result = MqProducerBeanFactory.getBean(pid).send(msg);
break;
} catch (Exception e) {
log.error("发送消息失败:", e);
if (iniCount++ >= PRODUCER_RETRY_TIMES) {
throw new TpcBizException(ErrorCodeEnum.TPC100500014, msg.getTopic(), msg.getKeys());
}
}
}
log.info("<== 发送MQ SendResult={}", result);
return result;
}
实际上就是通过MqProducerBeanFactory工厂getBean()获取消息生产类,最后send()发送
getBean()
private static final ConcurrentHashMap<String, DefaultMQProducer> DEFAULT_MQ_PRODUCER_MAP = new ConcurrentHashMap<>();
public static DefaultMQProducer getBean(String pid) {
//省略检查
return DEFAULT_MQ_PRODUCER_MAP.get(pid);
}
DEFAULT_MQ_PRODUCER_MAP集合在buildProducerBean()中赋值。
buildProducerBean()在MqBeanInitRunner.java中调用。同样MqBeanInitRunner实现了CommandLineRunner,在tpc模块启动时,spring将其加入容器。不再赘述。
DEFAULT_MQ_PRODUCER_MAP集合主要取自zk中这里节点的内容。

调用完之后,调用updateOnLineStatusByPid方法,通过mdcMqProducerMapper把pc_tpc_mq_producer表中的状态更新为在线20.
tpcMqProducerService.updateOnLineStatusByPid(producerDto.getProducerGroup());
public void updateOffLineStatusByPid(final String producerGroup) {
logger.info("更新生产者pid={}状态为离线", producerGroup);
this.updateStatus(producerGroup, 20);
}
private void updateStatus(final String producerGroup, final int status) {
TpcMqProducer tpcMqProducer = mdcMqProducerMapper.getByPid(producerGroup);
if (tpcMqProducer.getStatus() != null && tpcMqProducer.getStatus() != status) {
TpcMqProducer update = new TpcMqProducer();
update.setStatus(status);
update.setId(tpcMqProducer.getId());
mdcMqProducerMapper.updateByPrimaryKeySelective(update);
}
最后调用send()将消息发送。
至此,切面中的confirmAndSendMessage()调用完成
taskExecutor.execute(() -> mqMessageService.confirmAndSendMessage(finalDomain.getMessageKey()));
AuthRestController中/register 完成。
本文详细解析了注册过程中的验证码生成、分布式唯一ID生成(雪花算法)、注册逻辑以及邮件发送。首先,后台服务启动涉及多个模块如Eureka、Discovery、Provider等。接着,验证码的获取通过ValidateCodeProcessor生成并存储到Redis。注册时,雪花算法生成的ID用于用户ID,同时生成activeToken保存在Redis。最后,通过RocketMQ发送邮件消息到消息队列,等待后续处理。整个流程涵盖了前端交互、后端逻辑、数据库操作及消息队列的使用。

769

被折叠的 条评论
为什么被折叠?



