创建认证中心 gmall-auth 微服务模块
Nacos注册中心
spring:
application:
name: gmall-auth
cloud:
nacos:
discovery:
server-addr: 192.168.139.10:8848
namespace: 36854647-e68c-409b-9233-708a2d41702c
Nacos配置中心
spring.application.name=gmall-auth
spring.cloud.nacos.config.server-addr=192.168.139.10:8848
spring.cloud.nacos.config.namespace=2ae4a6b9-995f-4e5d-9da0-75743a83ae86
spring.cloud.nacos.config.group=dev
192.168.139.10 auth.gmall.com
- id: gmall_auth_route
uri: lb://gmall-auth
predicates:
- Host=auth.gmall.com
登录页面资源
nginx/html/static/login/
注册页面资源
nginx/html/static/reg/
对于页面跳转,没有业务数据渲染,不需要创建handler方法来处理,推荐使用
ViewControllerRegistry 统一进行页面跳转处理。
WebConfig 配置类
package com.atguigu.gmall.auth.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置 {@link WebConfig}
*
* @author zhangwen
* @email: [email protected]
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
修改 index.html search.html item.html 三个页面的登录和注册链接地址
阿里云市场购买并开通短信服务
https://market.aliyun.com/products/57126001/cmapi00037415.html?spm=5176.730005.productlist.d_cmapi00037415.20e73524icZQR4&innerSource=search_%E7%9F%AD%E4%BF%A1#sku=yuncode3141500001
在 gmall-third-party 第三方服务中创建发送短信组件
SmsComponent
package com.atguigu.gmall.thirdparty.component;
import com.atguigu.gmall.thirdparty.util.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.springframework.boot.context.properties.Confĕ gurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 短信发送 {@link SmsComponent}
*
* @author zhangwen
* @email: [email protected]
*/
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
@Data
@Component
public class SmsComponent {
/**
* 短信接口API
*/
private String host;
private String path;
/**
* 签名id
*/
private String smsSignId;
/**
* 模板id
*/
private String templateId;
/**
* appcode
*/
private String appcode;
/**
* 验证码有效时间
*/
private String minute;
/**
* 发送短信验证码
* @param mobile
* @param code
*/
public void sendSmsCode (String mobile, String code) {
String method = "POST";
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", mobile);
querys.put("param", "**code**:"+ code + ", **minute**:" + minute);
querys.put("smsSignId", smsSignId);
querys.put("templateId", templateId);
Map<String, String> bodys = new HashMap<String, String>();
try {
HttpResponse response = HttpUtils.doPost(host, path, method, headers,querys, bodys);
} catch (Exception e) {
e.printStackTrace();
}
}
}
LoginController
/**
* 发送短信验证码
* @param mobile
* @return
*/
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode (@RequestParam("mobile") String mobile) {
try {
smsService.sendCode(mobile);
} catch (RRException e) {
return R.error(e.getCode(), e.getMsg());
}
return R.ok();
}
SmsServcieImpl
/**
* 发送短信验证码
* @param mobile 手机号码
*/
@Override
public void sendCode(String mobile) {
// 从缓存中获取验证码
String redisCode = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + mobile);
if (!StringUtils.isEmpty(redisCode)) {
long codeTime = Long.parseLong(redisCode.split("_")[1]);
long currTime = System.currentTimeMillis();
if (currTime - codeTime < 60000) {
// 60 秒内不能再次发送验证码
throw new AuthBizException(BizCode.SMS_CODE_EXCEPTION.getMessage(), BizCode.SMS_CODE_EXCEPTION.getCode());
}
}
String code = getCheckCode();
// 验证码校验,将验证码放入到 redis 中
redisTemplate.opsForValue().set(AuthConstant.SMS_CODE_CACHE_PREFIX + mobile,
code + "_" + System.currentTimeMillis(), 10, TimeUnit.MINUTES);
// 发送短信验证码
R r = thirdPartyFeignService.sendCode(mobile, code);
if (r.getCode() != 0) {
log.error("调用远程服务 gmall-third-party 发送短信验证码失败");
}
}
/**
* 生成随机六位数字验证码
* @return
*/
private String getCheckCode () {
char[] nonceChars = new char[6];
for (int i = 0; i < nonceChars.length; ++i) {
nonceChars[i] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
private static final String SYMBOLS = "0123456789";
private static final Random RANDOM = new SecureRandom();
/**
* 校验用户是否存在
* @param username
* @return
*/
@ResponseBody
@GetMapping("/check/user/{username}")
public R checkUser(@PathVariable("username") String username) {
R r = memberFeignService.checkUser(username);
return r;
}
/**
* 检查手机号是否存在
* @param mobile
* @return
*/
@ResponseBody
@GetMapping("/check/mobile/{mobile}")
public R checkMobile(@PathVariable("mobile") String mobile) {
R r = memberFeignService.checkMobile(mobile);
return r;
}
/**
* 校验验证码
* @param mobile 手机号码
* @param code 验证码
* @return
*/
@Override
public boolean checkCode(String mobile, String code) {
String redisCode = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + mobile);
if (StringUtils.isEmpty(redisCode)) {
return false;
} else {
if (code.equals(redisCode.split("_")[0])) {
//删除验证码, 令牌机制
redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + mobile);
return true;
} else {
return false;
}
}
}
LoginController
/**
* 用户注册
* @param registerVO
* @param result
* @param redirectAttributes
* @return
*/
@PostMapping("/reg")
public String register (@Valid UserRegisterVO registerVO, BindingResult result,RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
// 收集校验错误消息
Map<String, String> errors = result.getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors", errors);
// 校验出错,重定向到注册页面
// TODO 重定向携带数据, 利用Session原理. 到了目标页面从session中取出数据, 然后删除session中的数据.
// TODO 分布式 Session 问题
return "redirect:http://auth.gmall.com/reg.html";
}
// 校验验证码
boolean b = smsService.checkCode(registerVO.getMobile(), registerVO.getCode());
if (!b) {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gmall.com/reg.html";
}
// 调用远程会员注册
R r = memberFeignService.register(registerVO);
if (r.getCode() != 0) {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData("msg", new TypeReference<String>() {}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gmall.com/reg.html";
}
// 注册成功,重定向到登录页
return "redirect:http://auth.gmall.com/login.html";
}
MD5(Message Digest algorithm 5)信息摘要算法
压缩性:任意长度的数据,计算出的MD5值长度都是固定的
容易计算:从原数据计算出MD5值很容易
抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别
彩虹表:暴力破解,对各种字符串进行MD5计算,存入到表中,只要有相同的字符串就会立马匹配。所以不能用原始的MD5值,需要加盐处理
强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的
加盐:
网盘秒传实现:上传的文件计算MD5值,然后去数据库进行匹配,看是否有与之匹配的MD5值的文件,如果有则不用上传,对应一个文件链接即可。
/**
* 会员注册
* @param registerVO
*/
@Override
public void register(MemberRegisterVO registerVO) {
//检查用户名和手机号唯一
checkUsernameUnique(registerVO.getUsername());
checkMobileUnique(registerVO.getMobile());
//设置用户名和手机号
MemberEntity memberEntity = new MemberEntity();
memberEntity.setUsername(registerVO.getUsername());
memberEntity.setMobile(registerVO.getMobile());
//设置会员默认等级
MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(memberLevelEntity.getId());
//设置密码, 进行盐值加密处理
//BCryptPasswordEncoder.encode() 加密:先生成盐值,根据盐值以及明文密码生成密文
//BCryptPasswordEncoder.matches(登录密码,数据库存储密文) 密码匹配
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodePassword = passwordEncoder.encode(registerVO.getPassword());
memberEntity.setPassword(encodePassword);
//保存
save(memberEntity);
}