主页 > 人工智能  > 

谷粒商城—分布式高级②.md

谷粒商城—分布式高级②.md
认证服务 1. 环境搭建

创建gulimall-auth-server模块,导依赖,引入login.html和reg.html,并把静态资源放到nginx的static目录下

2. 注册功能 (1) 验证码倒计时

//点击发送验证码按钮触发下面函数 $("#sendCode").click(function () { //如果有disabled,说明最近已经点过,则什么都不做 if($(this).hasClass("disabled")){ }else { //调用函数使得当前的文本进行倒计时功能 timeOutChangeStyle(); //发送验证码 var phone=$("#phoneNum").val(); $.get("/sms/sendCode?phone="+phone,function (data){ if (data.code!=0){ alert(data.msg); } }) } }) let time = 60; function timeOutChangeStyle() { //开启倒计时后设置标志属性disable,使得该按钮不能再次被点击 $("#sendCode").attr("class", "disabled"); //当时间为0时,说明倒计时完成,则重置 if(time==0){ $("#sendCode").text("点击发送验证码"); time=60; $("#sendCode").attr("class", ""); }else { //每秒调用一次当前函数,使得time-- $("#sendCode").text(time+"s后再次发送"); time--; setTimeout("timeOutChangeStyle()", 1000); } } (2) 整合短信服务

在阿里云网页购买试用的短信服务

在gulimall-third-party中编写发送短信组件,其中host、path、appcode可以在配置文件中使用前缀spring.cloud.alicloud.sms进行配置

@Data @ConfigurationProperties(prefix = "spring.cloud.alicloud.sms") @Controller public class SmsComponent { private String host; private String path; private String appcode; public void sendCode(String phone,String code) { // String host = "http://dingxin.market.alicloudapi "; // String path = "/dx/sendSms"; 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); querys.put("tpl_id", "TP1711063"); Map<String, String> bodys = new HashMap<String, String>(); try { /** * 重要提示如下: * HttpUtils请从 * github /aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java * 下载 * * 相应的依赖请参照 * github /aliyun/api-gateway-demo-sign-java/blob/master/pom.xml */ 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(); } } }

编写controller,给别的服务提供远程调用发送验证码的接口

@Controller @RequestMapping(value = "/sms") public class SmsSendController { @Resource private SmsComponent smsComponent; /** * 提供给别的服务进行调用 * @param phone 电话号码 * @param code 验证码 * @return */ @ResponseBody @GetMapping(value = "/sendCode") public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) { //发送验证码 smsComponent.sendCode(phone,code); System.out.println(phone+code); return R.ok(); } } (3) 接口防刷

由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。

在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储 如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息 60s以后再次调用,需要删除之前存储的phone-code code存在一个过期时间,我们设置为10min,10min内验证该验证码有效 @GetMapping("/sms/sendCode") @ResponseBody public R sendCode(@RequestParam("phone")String phone) { //接口防刷,在redis中缓存phone-code ValueOperations<String, String> ops = redisTemplate.opsForValue(); String prePhone = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone; String v = ops.get(prePhone); if (!StringUtils.isEmpty(v)) { long pre = Long.parseLong(v.split("_")[1]); //如果存储的时间小于60s,说明60s内发送过验证码 if (System.currentTimeMillis() - pre < 60000) { return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg()); } } //如果存在的话,删除之前的验证码 redisTemplate.delete(prePhone); //获取到6位数字的验证码 String code = String.valueOf((int)((Math.random() + 1) * 100000)); //在redis中进行存储并设置过期时间 ops.set(prePhone,code+"_"+System.currentTimeMillis(),10, TimeUnit.MINUTES); thirdPartFeignService.sendCode(phone, code); return R.ok(); } (4) 注册接口编写

在gulimall-auth-server服务中编写注册的主体逻辑

若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面 若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员服务注册 会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面

注: RedirectAttributes可以通过session保存信息并在重定向的时候携带过去

@PostMapping("/register") public String register(@Valid UserRegisterVo registerVo, 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); }); //1.2 重定向到注册页 return "redirect:http://auth.gulimall /reg.html"; }else { //2.若JSR303校验通过 //判断验证码是否正确 String code = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + registerVo.getPhone()); //2.1 如果对应手机的验证码不为空且与提交上的相等-》验证码正确 if (!StringUtils.isEmpty(code) && registerVo.getCode().equals(code.split("_")[0])) { //2.1.1 使得验证后的验证码失效 redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + registerVo.getPhone()); //2.1.2 远程调用会员服务注册 R r = memberFeignService.register(registerVo); if (r.getCode() == 0) { //调用成功,重定向登录页 return "redirect:http://auth.gulimall /login.html"; }else { //调用失败,返回注册页并显示错误信息 String msg = (String) r.get("msg"); errors.put("msg", msg); attributes.addFlashAttribute("errors", errors); return "redirect:http://auth.gulimall /reg.html"; } }else { //2.2 验证码错误 errors.put("code", "验证码错误"); attributes.addFlashAttribute("errors", errors); return "redirect:http://auth.gulimall /reg.html"; } } }

通过gulimall-member会员服务注册逻辑

通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息 如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间 @RequestMapping("/register") public R register(@RequestBody MemberRegisterVo registerVo) { try { memberService.register(registerVo); //异常机制:通过捕获对应的自定义异常判断出现何种错误并封装错误信息 } catch (UserExistException userException) { return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(), BizCodeEnum.USER_EXIST_EXCEPTION.getMsg()); } catch (PhoneNumExistException phoneException) { return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg()); } return R.ok(); } public void register(MemberRegisterVo registerVo) { //1 检查电话号是否唯一 checkPhoneUnique(registerVo.getPhone()); //2 检查用户名是否唯一 checkUserNameUnique(registerVo.getUserName()); //3 该用户信息唯一,进行插入 MemberEntity entity = new MemberEntity(); //3.1 保存基本信息 entity.setUsername(registerVo.getUserName()); entity.setMobile(registerVo.getPhone()); entity.setCreateTime(new Date()); //3.2 使用加密保存密码 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodePassword = passwordEncoder.encode(registerVo.getPassword()); entity.setPassword(encodePassword); //3.3 设置会员默认等级 //3.3.1 找到会员默认登记 MemberLevelEntity defaultLevel = memberLevelService.getOne(new QueryWrapper<MemberLevelEntity>().eq("default_status", 1)); //3.3.2 设置会员等级为默认 entity.setLevelId(defaultLevel.getId()); // 4 保存用户信息 this.save(entity); } private void checkUserNameUnique(String userName) { Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName)); if (count > 0) { throw new UserExistException(); } } private void checkPhoneUnique(String phone) { Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone)); if (count > 0) { throw new PhoneNumExistException(); } } 3. 用户名密码登录

在gulimall-auth-server模块中的主体逻辑

通过会员服务远程调用登录接口 如果调用成功,重定向至首页 如果调用失败,则封装错误信息并携带错误信息重定向至登录页 @RequestMapping("/login") public String login(UserLoginVo vo,RedirectAttributes attributes){ R r = memberFeignService.login(vo); if (r.getCode() == 0) { return "redirect:http://gulimall /"; }else { String msg = (String) r.get("msg"); Map<String, String> errors = new HashMap<>(); errors.put("msg", msg); attributes.addFlashAttribute("errors", errors); return "redirect:http://auth.gulimall /login.html"; } }

在gulimall-member模块中完成登录

当数据库中含有以当前登录名为用户名或电话号且密码匹配时,验证通过,返回查询到的实体 否则返回null,并在controller返回用户名或密码错误 @RequestMapping("/login") public R login(@RequestBody MemberLoginVo loginVo) { MemberEntity entity=memberService.login(loginVo); if (entity!=null){ return R.ok(); }else { return R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg()); } } @Override public MemberEntity login(MemberLoginVo loginVo) { String loginAccount = loginVo.getLoginAccount(); //以用户名或电话号登录的进行查询 MemberEntity entity = this.getOne(new QueryWrapper<MemberEntity>().eq("username", loginAccount).or().eq("mobile", loginAccount)); if (entity!=null){ BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); boolean matches = bCryptPasswordEncoder.matches(loginVo.getPassword(), entity.getPassword()); if (matches){ entity.setPassword(""); return entity; } } return null; } 4. 社交登录 (1) oauth2.0

(2) 在微博开放平台创建应用

(3) 在登录页引导用户至授权页 GET api.weibo /oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI client_id: 创建网站应用时的app key YOUR_REGISTERED_REDIRECT_URI: 认证完成后的跳转链接(需要和平台高级设置一致)

如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE

code是我们用来换取令牌的参数

(4) 换取token POST api.weibo /oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE client_id: 创建网站应用时的app key client_secret: 创建网站应用时的app secret YOUR_REGISTERED_REDIRECT_URI: 认证完成后的跳转链接(需要和平台高级设置一致) code:换取令牌的认证码

返回数据如下

(5) 获取用户信息

open.weibo /wiki/2/users/show

结果返回json

(6) 代码编写

认证接口

通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录 若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页 @Controller public class OauthController { @Autowired private MemberFeignService memberFeignService; @RequestMapping("/oauth2.0/weibo/success") public String authorize(String code, RedirectAttributes attributes) throws Exception { //1. 使用code换取token,换取成功则继续2,否则重定向至登录页 Map<String, String> query = new HashMap<>(); query.put("client_id", "2144***074"); query.put("client_secret", "ff63a0d8d5*****29a19492817316ab"); query.put("grant_type", "authorization_code"); query.put("redirect_uri", "http://auth.gulimall /oauth2.0/weibo/success"); query.put("code", code); //发送post请求换取token HttpResponse response = HttpUtils.doPost(" api.weibo ", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>()); Map<String, String> errors = new HashMap<>(); if (response.getStatusLine().getStatusCode() == 200) { //2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页 String json = EntityUtils.toString(response.getEntity()); SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() { }); R login = memberFeignService.login(socialUser); //2.1 远程调用成功,返回首页并携带用户信息 if (login.getCode() == 0) { String jsonString = JSON.toJSONString(login.get("memberEntity")); MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() { }); attributes.addFlashAttribute("user", memberResponseVo); return "redirect:http://gulimall "; }else { //2.2 否则返回登录页 errors.put("msg", "登录失败,请重试"); attributes.addFlashAttribute("errors", errors); return "redirect:http://auth.gulimall /login.html"; } }else { errors.put("msg", "获得第三方授权失败,请重试"); attributes.addFlashAttribute("errors", errors); return "redirect:http://auth.gulimall /login.html"; } }

登录接口

登录包含两种流程,实际上包括了注册和登录 如果之前未使用该社交账号登录,则使用token调用开放api获取社交账号相关信息,注册并将结果返回 如果之前已经使用该社交账号登录,则更新token并将结果返回 @RequestMapping("/oauth2/login") public R login(@RequestBody SocialUser socialUser) { MemberEntity entity=memberService.login(socialUser); if (entity!=null){ return R.ok().put("memberEntity",entity); }else { return R.error(); } } @Override public MemberEntity login(SocialUser socialUser){ MemberEntity uid = this.getOne(new QueryWrapper<MemberEntity>().eq("uid", socialUser.getUid())); //1 如果之前未登陆过,则查询其社交信息进行注册 if (uid == null) { Map<String, String> query = new HashMap<>(); query.put("access_token",socialUser.getAccess_token()); query.put("uid", socialUser.getUid()); //调用微博api接口获取用户信息 String json = null; try { HttpResponse response = HttpUtils.doGet(" api.weibo ", "/2/users/show.json", "get", new HashMap<>(), query); json = EntityUtils.toString(response.getEntity()); } catch (Exception e) { e.printStackTrace(); } JSONObject jsonObject = JSON.parseObject(json); //获得昵称,性别,头像 String name = jsonObject.getString("name"); String gender = jsonObject.getString("gender"); String profile_image_url = jsonObject.getString("profile_image_url"); //封装用户信息并保存 uid = new MemberEntity(); MemberLevelEntity defaultLevel = memberLevelService.getOne(new QueryWrapper<MemberLevelEntity>().eq("default_status", 1)); uid.setLevelId(defaultLevel.getId()); uid.setNickname(name); uid.setGender("m".equals(gender)?0:1); uid.setHeader(profile_image_url); uid.setAccessToken(socialUser.getAccess_token()); uid.setUid(socialUser.getUid()); uid.setExpiresIn(socialUser.getExpires_in()); this.save(uid); }else { //2 否则更新令牌等信息并返回 uid.setAccessToken(socialUser.getAccess_token()); uid.setUid(socialUser.getUid()); uid.setExpiresIn(socialUser.getExpires_in()); this.updateById(uid); } return uid; } 5. SpringSession (1) session 原理

jsessionid相当于银行卡,存在服务器的session相当于存储的现金,每次通过jsessionid取出保存的数据

问题:但是正常情况下session不可跨域,它有自己的作用范围

(2) 分布式下session共享问题

(3) 解决方案 1) session复制

2) 客户端存储

3) hash一致性

4) 统一存储

(4) SpringSession整合redis

通过SpringSession修改session的作用域

1) 环境搭建

导入依赖

<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

修改配置

spring: redis: host: 192.168.56.102 session: store-type: redis

添加注解

@EnableRedisHttpSession public class GulimallAuthServerApplication { 2) 自定义配置

由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化

并且通过修改CookieSerializer扩大session的作用域至**.gulimall

@Configuration public class GulimallSessionConfig { @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("GULISESSIONID"); serializer.setDomainName("gulimall "); return serializer; } } (5) SpringSession核心原理 - 装饰者模式 原生的获取session时是通过HttpServletRequest获取的 这里对request进行包装,并且重写了包装request的getSession()方法 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); //对原生的request、response进行包装 SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper( wrappedRequest, response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { wrappedRequest mitSession(); } } 购物车 1. 数据模型分析 (1) 数据存储

购物车是一个读多写多的场景,因此放入数据库并不合适,但购物车又是需要持久化,因此这里我们选用redis存储购物车数据。

(2) 数据结构

一个购物车是由各个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用hash进行存储

(3) VO编写

购物项vo

public class CartItemVo { private Long skuId; //是否选中 private Boolean check = true; //标题 private String title; //图片 private String image; //商品套餐属性 private List<String> skuAttrValues; //价格 private BigDecimal price; //数量 private Integer count; //总价 private BigDecimal totalPrice; /** * 当前购物车项总价等于单价x数量 * @return */ public BigDecimal getTotalPrice() { return price.multiply(new BigDecimal(count)); } public void setTotalPrice(BigDecimal totalPrice) { this.totalPrice = totalPrice; }

购物车vo

public class CartVo { /** * 购物车子项信息 */ List
标签:

谷粒商城—分布式高级②.md由讯客互联人工智能栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“谷粒商城—分布式高级②.md