环境搭建
选择模块thymeleaf,web,openFeign,lombok,spring-boot-devTools
加入common模块(有注册中心之类的很多依赖)排除jdbc依赖
- <dependency>
- <groupId>com.wuyimin.gulimall</groupId>
- <artifactId>gulimall-common</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <exclusions>
- <exclusion>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
配置yml文件
- spring: #配置nacos
- cloud:
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
-
- application:
- name: gulimall-auth-server
- server:
- port: 20000
放入登录页面和注册页面,并且把静态资源转给nginx,login后改名为index
网关配置
添加域名
登录准备
以前我们需要写一个空方法转发请求到登录注册页面
- @GetMapping("/login.html")
- public String loginPage(){
- return "login";
- }
- @GetMapping("/reg.html")
- public String regPage(){
- return "reg";
- }
现在使用映射配置类
- @Configuration
- public class MyWebConfig implements WebMvcConfigurer {
- //视图映射
- @Override
- public void addViewControllers(ViewControllerRegistry registry) {
- registry.addViewController("/login.html").setViewName("login");
- registry.addViewController("/reg.html").setViewName("reg");
- }
- }
整合短信验证码
https://market.aliyun.com/products/?keywords=短信验证码
去买一个免费的,复制一下测试代码,然后把appid换成自己的
正常来说是要用@ConfigurationProperties配置到配置文件里的,这里直接抽取不做配置
- @Component
- @Data
- public class SmsComponent {
- private String host;
- private String path;
- private String templateId="908e94ccf08b4476ba6c876d13f084ad";
- private String smsSignId="2e65b1bb3d054466b82f0c9d125465e2";
- private String appCode="78442b1006ae490da40cedda6826c7b5";
- public void sendSmsCode(String phone,String code){
- String host = "https://gyytz.market.alicloudapi.com";
- String path = "/sms/smsSend";
- String method = "POST";
- String appcode = appCode;
- Map<String, String> headers = new HashMap<String, String>();
- //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
- headers.put("Authorization", "APPCODE " + appcode);
- Map<String, String> querys = new HashMap<String, String>();
- querys.put("mobile", phone);
- querys.put("param", "**code**:"+code+"**minute**:5");
- querys.put("smsSignId", smsSignId);
- querys.put("templateId", templateId);
- Map<String, String> bodys = new HashMap<String, String>();
- try {
- /**
- * HttpUtils请从
- * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
- * 下载
- */
- HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
- System.out.println(response.toString());
- //获取response的body
- //System.out.println(EntityUtils.toString(response.getEntity()));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
第三方模块远程被调用的类
- @RestController
- @RequestMapping("/sms")
- public class SmsSendController {
- @Autowired
- SmsComponent smsComponent;
- //提供给别的服务进行调用
- @GetMapping("/sendcode")
- public R sendCode(@RequestParam("phone") String phone, @RequestParam("code")String code){
- smsComponent.sendSmsCode(phone,code);
- return R.ok();
- }
- }
auth模块远程调用接口:
- @FeignClient("gulimall-third-party")
- public interface ThirdPartyFeignService {
- @GetMapping("/sms/sendcode")
- public R sendCode(@RequestParam("phone") String phone, @RequestParam("code")String code);
- }
细化验证码:
1.接口防刷(每次只要刷新后就可以重发验证码)
2.验证码校验--存入redis中
接口防刷
redis依赖导入
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
配置redis的地址以及具体实现逻辑
- spring:
- redis:
- host: 192.168.116.128
- @Slf4j
- @Controller
- public class LoginController {
- @Autowired
- ThirdPartyFeignService thirdPartyFeignService;
- @Autowired
- StringRedisTemplate redisTemplate;
- @ResponseBody
- @GetMapping("/sms/sendcode")
- public R sendCode(@RequestParam("phone") String phone){
- //TODO 接口防刷
- String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + phone);
- if(!StringUtils.isEmpty(redisCode)){
- long l=Long.parseLong(redisCode.split("_")[1]);//拿到时间
- if(System.currentTimeMillis()-l<60000){
- //60秒内不能再发
- return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
- }
- }
- String code = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();//加上系统时间
- //验证码的再次校验,存入redis key-手机号 value-code
- redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CAHE_PREFIX+phone,code,10, TimeUnit.MINUTES);
- try {
- thirdPartyFeignService.sendCode(phone,code);//第三方服务
- } catch (Exception e) {
- log.warn("远程调用不知名错误 [无需解决]");
- }
- return R.ok();
- }
- }
注册页环境
踩坑
校验注解不生效是因为少了这个依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-validation</artifactId>
- </dependency>
如果在controller类上添加了@Validated注解,错误会以500的响应模式出来
校验实体类
- @Data
- public class UserRegistVo {
- @NotEmpty(message = "用户名必须提交")
- @Length(min = 6,max = 18,message = "长度必须在6-18")
- private String userName;
- @NotEmpty(message = "密码必须提交")
- @Length(min = 6,max = 18,message = "长度必须在6-18")
- private String password;
- //第一个数组必须是1,第二个数字在3-9剩下9个数字在0-9,一共11位
- @NotEmpty(message = "手机号必须填写")
- @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
- private String phone;
- @NotEmpty(message = "验证码必须填写")
- private String code;
- }
- //TODO 重定向携带数据,利用session原理,将数据放在session中,只要跳到下一个页面,取出这个数据以后session里的数据就会删掉
- //TODO 分布式下的session问题
- //post请求不支持--我们配置的路径映射默认都是get方式才能访问,所以不能直接return“forward:/reg.html”,这样会直接把post请求发给页面
- @PostMapping("/regist")
- public String register(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes attributes){//第三个参数是专门用来重定向携带数据的
- //注册成功会到登录页
- //1.判断校验是否通过
- Map<String, String> errors = new HashMap<>();
- if (result.hasErrors()){
- //1.1 如果校验不通过,则封装校验结果
- result.getFieldErrors().forEach(item->{
- // 获取错误的属性名和错误信息
- errors.put(item.getField(), item.getDefaultMessage());
- //1.2 将错误信息封装到session中
- attributes.addFlashAttribute("errors", errors);
- });
- //校验出错,重定向到注册页
- return "redirect:http://auth.gulimall.com/reg.html";//防止刷新的时候表单重复提交,采用重定向
- }else{
- return "redirect:http://auth.gulimall.com/login.html";
- }
- }
- }
异常机制
远程的Member服务
- @PostMapping("/regist")
- public R regist(@RequestBody MemberRegisterVo vo){//远程服务必须要获得json对象
- try{
- memberService.regist(vo);
- }catch (Exception e){
- //对于不同的异常有不同的处理方式
- }
- return R.ok();
- }
查出默认等级的方法
- <select id="getDefaultLevel" resultType="com.wuyimin.gulimall.member.entity.MemberLevelEntity">
- select * from ums_member_level where default_status=1
- </select>
自定义异常,用于判断手机号与用户名是否已经存在
- public class PhoneExistException extends RuntimeException {
- public PhoneExistException() {
- super("手机号已经存在");
- }
- }
检查手机和用户名的函数
- @Override
- public void checkPhone(String phone) throws PhoneExistException {
- Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
- if (count > 0) {
- throw new PhoneExistException();
- }
- }
-
- @Override
- public void checkUserName(String userName) throws UsernameExistException {
- Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
- if (count > 0) {
- throw new UsernameExistException();
- }
- }
现阶段的regist方法
- @Override
- public void regist(MemberRegisterVo vo) {
- MemberEntity memberEntity = new MemberEntity();
- MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
- memberEntity.setLevelId(memberLevelEntity.getId());//默认等级为1普通会员
- //设置其他的默认信息
- //检查用户名和手机号的唯一性 为了让controller能感知异常,我们使用异常机制
- checkPhone(vo.getPhone());
- checkUserName(vo.getUserName());
- //设置用户名和手机
- memberEntity.setMobile(vo.getPhone());
- memberEntity.setUsername(vo.getUserName());
- //设置密码(密码需要进行加密存储)
- baseMapper.insert(memberEntity);
- }
MD5,盐值和BCrypt
MD5:信息摘要算法,是不可逆的算法--》但是利用其抗修改性,使用彩虹表可以暴力破解,所以不能直接存储
加盐:通过生产随机数和MD5字符串进行组合,数据库同时存储MD5值和盐值,验证正确的时候使用salt进行MD5即可
- @Override
- public void regist(MemberRegisterVo vo) {
- MemberEntity memberEntity = new MemberEntity();
- MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
- memberEntity.setLevelId(memberLevelEntity.getId());
- checkPhone(vo.getPhone());
- checkUserName(vo.getUserName());
- memberEntity.setMobile(vo.getPhone());
- memberEntity.setUsername(vo.getUserName());
- //设置密码(密码需要进行加密存储)
- BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
- String encode = passwordEncoder.encode(vo.getPassword());
- memberEntity.setPassword(encode);
- //其他的默认信息。。
- //保存
- baseMapper.insert(memberEntity);
- }
远程注册功能完善
- @PostMapping("/regist")
- public R regist(@RequestBody MemberRegisterVo vo){//远程服务必须要获得json对象
- try{
- memberService.regist(vo);
- }catch (PhoneExistException e){
- return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
- }catch (UsernameExistException e){
- return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(),BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
- }
- return R.ok();
- }
auth模块远程调用member模块的接口
- @FeignClient("gulimall-member")
- public interface MemberFeignService {
- @PostMapping("/member/member/regist")
- R regist(@RequestBody MemberRegisterVo vo);
- }
- //TODO 重定向携带数据,利用session原理,将数据放在session中,只要跳到下一个页面,取出这个数据以后session里的数据就会删掉
- //TODO 分布式下的session问题
- //post请求不支持--我们配置的路径映射默认都是get方式才能访问,所以不能直接return“forward:/reg.html”,这样会直接把post请求发给页面
- @PostMapping("/regist")
- public String register(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes attributes){//第三个参数是专门用来重定向携带数据的
- //注册成功会到登录页
- //1.判断校验是否通过
- Map<String, String> errors = new HashMap<>();
- if (result.hasErrors()){
- //1.1 如果校验不通过,则封装校验结果
- result.getFieldErrors().forEach(item->{
- // 获取错误的属性名和错误信息
- errors.put(item.getField(), item.getDefaultMessage());
- //1.2 将错误信息封装到session中
- attributes.addFlashAttribute("errors", errors);
- });
- //校验出错,重定向到注册页
- return "redirect:http://auth.gulimall.com/reg.html";//防止刷新的时候表单重复提交,采用重定向
- }else{
- //真正的注册
- //1.校验验证码
- String code=vo.getCode();
- String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + vo.getPhone());
- if(!StringUtils.isEmpty(redisCode)){
- if(code.equals(redisCode.split("_")[0])){
- //删除验证码
- redisTemplate.delete(AuthServerConstant.SMS_CODE_CAHE_PREFIX + vo.getPhone());
- //验证码通过,调用远程接口进行服务注册
- R r = memberFeignService.regist(vo);
- if(r.getCode()==0){
- //成功
- return "redirect:http://auth.gulimall.com/login.html";
- }else{
- //调用失败,返回注册页并显示错误信息
- String msg = (String) r.get("msg");
- errors.put("msg", msg);
- attributes.addFlashAttribute("errors", errors);
- log.error("远程调用会员服务失败");
- return "redirect:http://auth.gulimall.com/reg.html";
- }
- }else{
- //验证码没有匹配
- errors.put("code","验证码错误");
- attributes.addFlashAttribute("errors",errors);
- return "redirect:http://auth.gulimall.com/reg.html";
- }
- }else{
- //没有验证码
- errors.put("code","验证码错误");
- attributes.addFlashAttribute("errors",errors);
- return "redirect:http://auth.gulimall.com/reg.html";
- }
- }
- }
修改验证码的bug,存进redis的是带uuid的但是传递进service服务的参数不该带uuid
- @ResponseBody
- @GetMapping("/sms/sendcode")
- public R sendCode(@RequestParam("phone") String phone){
- //TODO 接口防刷
- String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + phone);
- if(!StringUtils.isEmpty(redisCode)){
- long l=Long.parseLong(redisCode.split("_")[1]);//拿到时间
- if(System.currentTimeMillis()-l<60000){
- //60秒内不能再发
- return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
- }
- }
- String code = UUID.randomUUID().toString().substring(0, 5);
- String saveInRedis = code + "_" + System.currentTimeMillis();//加上系统时间
- //验证码的再次校验,存入redis key-手机号 value-code
- redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CAHE_PREFIX+phone,saveInRedis,10, TimeUnit.MINUTES);
- try {
- thirdPartyFeignService.sendCode(phone,code);//第三方服务
- } catch (Exception e) {
- log.warn("远程调用不知名错误 [无需解决]");
- }
- return R.ok();
- }
至此所有注册功能已经完成