環境搭建
選擇模塊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(); }
至此所有注冊功能已經完成