03谷粒商城-高级篇三

前言

可以间接性堕落,但总不能一直清醒的堕落吧

9.商城业务-认证服务

9.1环境搭建

主要步骤:

  • 创建gulimall-auth-serviceapplication.yml配置nacos

  • 配置gulimall-auth-servicepom.xml,此服务暂不需要mybatis-plus

  • 配置hosts文件

  • 上传登录和注册的静态资源到nginx

  • 配置nginx

  • 配置gulimall-gateway网关服务

  • gulimall-auth-service添加登录页和注册页,登录页改为index.html方便测试

  • 修改登录页和注册页的静态资源访问地址

  • 测试访问http://auth.gulimall.com/

创建gulimall-auth-serviceapplication.yml配置nacos地址

server:
  port: 8209
spring:
  application:
    name: gulimall-auth-service
  main:
    allow-circular-references: true
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.188.180:8848 # nacos地址

image-20240730013915882

gulimall-auth-service 配置pom.xml,此服务暂不需要mybatis-plus,需要从继承的父类中排除

需要检查父类继承的其他包有没有使用mybatis-plus,需要一并排除

    <parent>
        <groupId>com.peng</groupId>
        <artifactId>service</artifactId>
        <version>1.0</version>
        <relativePath />
    </parent>
    
      <dependencies>
        <dependency>
            <groupId>com.peng</groupId>
            <artifactId>service</artifactId>
            <version>1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.peng</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

image-20240730014215548

管理员启动SwitchHosts,配置hosts文件

192.168.188.180     auth.gulimall.com

image-20240730014236327

上传登录和注册的静态资源到nginx/root/mall/nginx/html/static/目录下

image-20240730014416886

配置nginx,因为 auth.gulimall.com匹配*.gulimall.com,这里不需要多加配置,留意一下即可

image-20240730014507160

配置gulimall-gateway网关服务,添加gulimall-auth-service服务的转发

- id: gulimall_auth_route
  uri: lb://gulimall-auth-service
  predicates:
    - Host=auth.gulimall.com

image-20240730014620567

gulimall-auth-service添加登录页和注册页,登录页就为index.html方便测试

image-20240730014813096

修改登录页和注册页的静态资源访问地址

登录页

# 静态资源路径
href="
href="/static/login/
# 图片路径
src="
src="/static/login/

image-20240730015046821

注册页

# 静态资源路径
href="
href="/static/reg/
# 图片路径
src="
src="/static/reg/

image-20240730033211029

测试访问http://auth.gulimall.com/

image-20240730015341932

9.2验证码倒计时

主要步骤:

  • 创建创建LoginController,登录页、注册页跳转

  • 首页、登录页、注册页跳转

  • 发送短信倒计时

    • 全局声明var num = 60倒计时
    • 当前标签添加类disabled,防止重复点击开启定时器
    • num = 0时结束倒计时,重置num = 60,清除类disabled
    • num > 0时,启动定时器计时

创建LoginController,登录页、注册页跳转

@Controller
public class LoginController {
    @GetMapping("/login.html")
    public String loginPage(){
        return  "login";
    }

    @GetMapping("/reg.html")
    public String regPage(){
        return  "reg";
    }
}

image-20240730033839859

登录页

<!--顶部logo-->
		<header>
			<a href="http://gulimall.com/"><img src="/static/login/JD_img/logo.jpg" /></a>
			<p>欢迎登录</p>
			<div class="top-1">
				<img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_06.png" /><span>登录页面,调查问卷</span>
			</div>
		</header>

image-20240730031219442

登录页:立即注册

<h5 class="rig">
    <img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_25.png" />
    <span><a href="http://auth.gulimall.com/reg.html">立即注册</a></span>
</h5>

image-20240730033554701

商品服务index.html

<li>
  <a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>
  <a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/login.html">你好,请登录</a>
</li>
<li>
  <a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/reg.html" class="li_2">免费注册</a>
</li>

image-20240730031505345

注册页:请登录

<div class="dfg">
    <span>已有账号?</span>
    <a href="http://auth.gulimall.com/login.html">请登录</a>
</div>

image-20240730033726322

发送短信倒计时

$(function () {
    $("#sendCode").click(function () {
       //2、倒计时
       if($(this).hasClass("disabled")) {
          //正在倒计时中
       } else {
          timeoutChangeStyle();
       }
    });
});
var num = 60;
function timeoutChangeStyle() {
    $("#sendCode").attr("class","disabled");
    if(num == 0) {
       $("#sendCode").text("发送验证码");
       num = 60;
       $("#sendCode").attr("class","");
    } else {
       var str = num + "s 后再次发送";
       $("#sendCode").text(str);
       setTimeout("timeoutChangeStyle()",1000);
    }
    num --;
}

自定义导航

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    /**·
     * 视图映射:发送一个请求,直接跳转到一个页面
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

image-20240731162614596

然后可以删掉LoginController的导航

image-20240731162637915

9.3整合短信验证码

主要步骤:

  • 1.申请阿里云短信验证码服务
  • 2.整合并测试短信验证码服务

申请阿里云短信验证码服务

地址:https://www.aliyun.com/benefit/waitou/V2?utm_content=se_1018076021

打开云市场

image-20240731145226055

点击搜索框,找到短信

image-20240731151845001

随便选择一个服务商

image-20240731151933685

选择免费试用

image-20240731152005898

记住自己的AppCode,然后进入调试服务

image-20240731152456954

这里有事例

image-20240731152717023

整合并测试短信验证码服务

将事例中的链接代码拷贝到项目

image-20240731153420511

找到HttpUtils

https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java

image-20240731153450518

拷贝到项目

image-20240731153531233

封装SmsComponent

@ConfigurationProperties(prefix = "alibaba.cloud.sms")
@Data
@Component
public class SmsComponent {

    // 服务地址
    private String host;
    // 路径
    private String path;
    // 请求方式
    private String method;
    // appcode
    private String appcode;
    // 短信前缀
    private String smsSignId;
    // 短信模板
    private String templateId;
    // 有效时长
    private String minute;


    public void sendCode(String phone,String code) {
        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**:"+minute);

        //smsSignId(短信前缀)和templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html

        querys.put("smsSignId", smsSignId);
        querys.put("templateId", templateId);
        Map<String, String> bodys = new HashMap<String, String>();
        //JDK 1.8示例代码请在这里下载:  http://code.fegine.com/Tools.zip
        try {
            /**
             * 重要提示如下:
             * HttpUtils请从\r\n\t    \t* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java\r\n\t    \t* 下载
             *
             * 相应的依赖请参照
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

image-20240731160519694

测试SmsComponent

@Autowired
SmsComponent component;

@Test
public void testSms1(){
    component.sendCode("15727328076","12345");
}

image-20240731160609668

9.4验证码防刷校验

主要步骤:

  • gulimall-third-party封装发送短信验证码接口
  • gulimall-auth-service封装远程接口调用gulimall-third-party发送短信验证码
  • gulimall-auth-service发送验证码接口
    • 接口防刷,60s内不能重复调用,获取验证后将验证码和当前时间都存入redis,前端调用时根据key当前手机号获取存入的验证码和时间,如果当前时间-存入的时间小于60s直接返回
  • 前端调用gulimall-auth-service

gulimall-third-party封装发送短信验证码接口

/**
 * 提供给别的服务进行调用
 * @param phone
 * @param code
 * @return
 */
@GetMapping(value = "/sendCode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) {
    //发送验证码
    smsComponent.sendCode(phone,code);
    return R.ok();
}

image-20240801022223033

gulimall-auth-service封装远程接口调用gulimall-third-party发送短信验证码

@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {

    @GetMapping(value = "/sms/sendCode")
    R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);

}

image-20240801022301602

接口防刷,60s内不能重复调用,获取验证后将验证码和当前时间都存入redis,前端调用时根据key当前手机号获取存入的验证码和时间,如果当前时间-存入的时间小于60s直接返回

@Autowired
private ThirdPartFeignService thirdPartFeignService;

@Autowired
private StringRedisTemplate stringRedisTemplate;

@ResponseBody
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone) {

    //1、接口防刷
    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
    if (!StringUtils.isEmpty(redisCode)) {
        //活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
        long currentTime = Long.parseLong(redisCode.split("_")[1]);
        if (System.currentTimeMillis() - currentTime < 60000) {
            //60s内不能再发
            return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
        }
    }

    //2、验证码的再次效验 redis.存key-phone,value-code
    String code = UUID.randomUUID().toString().substring(0,5)+"_"+System.currentTimeMillis();
    stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone, code,10, TimeUnit.MINUTES);
    thirdPartFeignService.sendCode(phone, codeNum);

    return R.ok();
}

image-20240801022421520

9.5注册页环境

主要步骤:

  • 封装注册功能Vo实体 UserRegisterVo,使用hibernate-validator特性校验
  • 封装注册接口
  • 修改前端页面注册提交

封装注册功能Vo实体 UserRegisterVo,使用hibernate-validator特性校验

/**
 * 注册使用的vo,使用JSR303校验
 */
@Data
public class UserRegisterVo {

    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6, max = 19, message="用户名长度必须是6-18字符")
    private String userName;

    @NotEmpty(message = "密码必须填写")
    @Length(min = 6,max = 18,message = "密码长度必须是6—18位字符")
    private String password;

    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号格式不正确")
    private String phone;

    @NotEmpty(message = "验证码必须填写")
    private String code;

}

image-20240801025255349

封装注册接口

@PostMapping(value = "/register")
public String register(@Valid UserRegisterVo vos, BindingResult result,
                       RedirectAttributes attributes) {

    //如果有错误回到注册页面
    if (result.hasErrors()) {
        Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        attributes.addFlashAttribute("errors",errors);
        //效验出错回到注册页面
        return "redirect:http://auth.gulimall.com/reg.html";
    }
    return "redirect:/login.html";
}

image-20240801025714808

修改前端页面注册提交

image-20240801025421698

image-20240801025517245

9.6异常机制

主要步骤:

  • 如果界面参数验证通过,获取Redis的验证码code
    • 如果Redis存在code ,和传入的code 一样调用注册接口保存用户信息
    • 如果Redis存在code ,和传入的code 不一样,跳转到注册
    • 如果Redis不存在code ,跳转到注册页面
  • gulimall-member添加注册接口
    • 设置默认等级
    • 验证手机号唯一
    • 验证用户名唯一

如果界面参数验证通过,获取Redis的验证码code

/**
 *
 * TODO: 重定向携带数据:利用session原理,将数据放在session中。
 * TODO:只要跳转到下一个页面取出这个数据以后,session里面的数据就会删掉
 * TODO:分布下session问题
 * RedirectAttributes:重定向也可以保留数据,不会丢失
 * 用户注册
 * @return
 */
@PostMapping(value = "/register")
public String register(@Valid UserRegisterVo vos, BindingResult result,
                       RedirectAttributes attributes) {

    //如果有错误回到注册页面
    if (result.hasErrors()) {
        Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        attributes.addFlashAttribute("errors",errors);
        //效验出错回到注册页面
        return "redirect:http://auth.gulimall.com/reg.html";
    }

    //1、效验验证码
    String code = vos.getCode();

    //获取存入Redis里的验证码
    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vos.getPhone());
    if (!StringUtils.isEmpty(redisCode)) {
        //截取字符串
        if (code.equals(redisCode.split("_")[0])) {
            //删除验证码;令牌机制
            stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX+vos.getPhone());
            //验证码通过,真正注册,调用远程服务进行注册
            R register = memberFeignService.register(vos);
            if (register.getCode() == 0) {
                //成功
                return "redirect:http://auth.gulimall.com/login.html";
            } else {
                //失败
                Map<String, String> errors = new HashMap<>();
                errors.put("msg", register.getData("msg",new TypeReference<String>(){}));
                attributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }


        } else {
            //效验出错回到注册页面
            Map<String, String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            attributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/reg.html";
        }
    } else {
        //效验出错回到注册页面
        Map<String, String> errors = new HashMap<>();
        errors.put("code","验证码错误");
        attributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.gulimall.com/reg.html";
    }
}

image-20240801035257958

gulimall-member添加注册接口

@PostMapping(value = "/register")
public R register(@RequestBody MemberUserRegisterVo vo) {

    try {
        memberService.register(vo);
    } catch (PhoneException e) {
        return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
    } catch (UsernameException e) {
        return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(),BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
    }
    return R.ok();
}

image-20240801035338605

注册接口实现

@Override
public void register(MemberUserRegisterVo vo) {
    MemberEntity memberEntity = new MemberEntity();

    //设置默认等级
    MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
    memberEntity.setLevelId(levelEntity.getId());

    //设置其它的默认信息
    //检查用户名和手机号是否唯一。感知异常,异常机制
    checkPhoneUnique(vo.getPhone());
    checkUserNameUnique(vo.getUserName());

    memberEntity.setNickname(vo.getUserName());
    memberEntity.setUsername(vo.getUserName());
    //密码进行MD5加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode = bCryptPasswordEncoder.encode(vo.getPassword());
    memberEntity.setPassword(encode);
    memberEntity.setMobile(vo.getPhone());
    memberEntity.setGender(0);
    memberEntity.setCreateTime(new Date());

    //保存数据
    this.baseMapper.insert(memberEntity);
}

image-20240801035405869

手机号异常

public class PhoneException extends RuntimeException {

    public PhoneException() {
        super("存在相同的手机号");
    }
}

用户名异常

public class UsernameException extends RuntimeException {

    public UsernameException() {
        super("存在相同的用户名");
    }
}

9.7MD5&盐值&BCrypt

主要步骤:

  • MD5介绍
  • MD5测试
  • 使用BCryptPasswordEncoder完成密码MD5加密

MD5

  • Message Digest algorithm 5,信息摘要算法

    • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。

    • 容易计算:从原数据计算出MD5值很容易。

    • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。

    • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。

  • 加盐:

    • 通过生成随机数与MD5生成字符串进行组合
    • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

MD5测试

@Test
public void testMD5() {
    // md5加密
    String str1 = DigestUtils.md5Hex("123456");
    System.out.println("str1:"+str1);
    // md5加密
    String str2 = Md5Crypt.md5Crypt("123456".getBytes());
    System.out.println("str2:"+str2);
    // md5盐值加密
    String str3  = Md5Crypt.md5Crypt("123456".getBytes(),"$1$sdahjksdjkhash");
    System.out.println("str3:"+str3);

    // BCryptPasswordEncoder工具类
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String  str4 = bCryptPasswordEncoder.encode("123456");
    String  str5 = bCryptPasswordEncoder.encode("123456");
    System.out.println("str4:"+str4);
    System.out.println("str5:"+str5);

    // matches
    boolean matches1 = bCryptPasswordEncoder.matches("123456", str4);
    boolean matches2 = bCryptPasswordEncoder.matches("123456", str5);
    System.out.println("matches1:"+matches1);
    System.out.println("matches2:"+matches2);

}

image-20240801041424724

使用BCryptPasswordEncoder完成密码MD5加密

image-20240801041519223

9.8注册完成

主要步骤:

  • gulimall-auth-service添加会员服务的注册接口
  • gulimall-auth-service添加Redisjson序列化配置
  • 完成注册功能,调试通过
    • SmsSendController需要@RestController而不是@Controller

gulimall-auth-service添加会员服务的注册接口

image-20240801045736863

gulimall-auth-service添加Redisjson序列化配置

image-20240801045816072

gulimall-auth-service/login.htmlgulimall-auth-service/reg.html添加thymeleaf命名空间

<html lang="en"  xmlns:th="http://www.thymeleaf.org">

image-20240801050755272

SmsSendController需要@RestController而不是@Controller

否则会报Error resolving template [sms/sendCode], template might not exist or might not be accessible by any of the configured Template Resolvers

image-20240801053559448

9.9账号密码登录完成

主要步骤:

  • 1.登录页面添加form表单提交登录信息
  • 2.gulimall-member添加登录接口
  • 3.gulimall-auth-service远程调用登录接口,并完善登录功能

登录页面添加form表单提交登录信息

<form action="/login" method="post">
    <div style="color: red" th:text="${errors != null ? (#maps.containsKey(errors, 'msg') ? errors.msg : '') : ''}"></div>
    <ul>
       <li class="top_1">
          <img src="/static/login/JD_img/user_03.png" class="err_img1"/>
          <input type="text" name="loginacct" value="15727328076" placeholder=" 邮箱/用户名/已验证手机" class="user"/>
       </li>
       <li>
          <img src="/static/login/JD_img/user_06.png" class="err_img2"/>
          <input type="password" name="password" value="123456" placeholder=" 密码" class="password"/>
       </li>
       <li class="bri">
          <a href="/static/login/">忘记密码</a>
       </li>
       <li class="ent">
          <button class="btn2" type="submit">登 &nbsp; &nbsp;录</a></button>
       </li>
    </ul>
</form>

image-20240801163031462

gulimall-member添加登录接口

@Override
public MemberEntity login(MemberUserLoginVo vo) {
    String loginacct = vo.getLoginacct();
    String password = vo.getPassword();
    //1、去数据库查询 SELECT * FROM ums_member WHERE username = ? OR mobile = ?
    MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>()
            .eq("username", loginacct).or().eq("mobile", loginacct));

    if (memberEntity == null) {
        //登录失败
        return null;
    } else {
        //获取到数据库里的password
        String password1 = memberEntity.getPassword();
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //进行密码匹配
        boolean matches = passwordEncoder.matches(password, password1);
        if (matches) {
            //登录成功
            return memberEntity;
        }
    }

    return null;
}

image-20240801163053219

gulimall-auth-service远程调用登录接口

@PostMapping(value = "/member/member/login")
R login(@RequestBody UserLoginVo vo);

image-20240801163120823

gulimall-auth-service完善登录功能

@PostMapping(value = "/login")
public String login(UserLoginVo vo, RedirectAttributes attributes, HttpSession session) {

    //远程登录
    R login = memberFeignService.login(vo);

    if (login.getCode() == 0) {
        return "redirect:http://gulimall.com";
    } else {
        Map<String,String> errors = new HashMap<>();
        errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
        attributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.gulimall.com/login.html";
    }
}

image-20240801163153921

注释MemberEntity以下字段

image-20240801163217984

9.10OAuth2.0

image-20240801163934705

9.11weibo登录测试

  • 登录weibo,打开微博开发平台
  • 开发者信息,需要输入基本信息和身份验证
  • 进入weibo授权页面
  • 用户登录weibo成功获取code
  • 使用code获取access_token,
    • 使用code获取access_token只能用一次
    • 同一个用户的access_token一段时间是不会变化的,即使获取多次

首先注册微博账号,申请开发者权限

image-20240801171237485

9.11.1网站接入

登录微博开发平台:https://open.weibo.com/,选择微连接,选择网站接入,选择立即接入

image-20241008204550502

创建网页应用

image-20241008204902528

这里的App KeyApp Secret

image-20241008204953925

在高级信息里配置登录成功回调和登录失败回调

image-20241008205144665

选择文档,滑动到网页最下面,查看OAuth2.0授权认证

image-20241008205329958

Web网站的授权

image-20241008205445621

总共4步

image-20241008205528788

9.11.1登录测试

引导需要授权的用户到如下地址:

https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI

client_id就是你的App Keyredirect_uri就是你的授权回调页

<a href="https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://gulimall.com/success">
								<img style="width: 50px;height: 18px;" src="/static/login/JD_img/weibo.png"/>
							</a>				

image-20241008210220827

用户进行登录授权

image-20241008214841698

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

登录成功后微博跳转到了http://gulimall.com/success,并带上了code

http://gulimall.com/success?code=598bb71e0ec19cba2369c78d16199eca

image-20241008214907092

然后换取Access Token

https://api.weibo.com/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

文档地址:https://open.weibo.com/wiki/Oauth2/access_token

image-20241008215447237

请求oauth2/access_token,获取access_token

https://api.weibo.com/oauth2/access_token

image-20241008230153687

根据用户ID获取用户信息

文档地址:https://open.weibo.com/wiki/2/users/show

image-20241008230546328

请求users/show,获取用户信息

https://api.weibo.com/2/users/show.json

image-20241008230928295

OAuth授权之后,获取授权用户的UID

文档地址:https://open.weibo.com/wiki/2/account/get_uid

image-20241008231300276

请求account/get_uid,获取授权用户的UID

image-20241008231404026

9.12社交登录回调

流程图

image-20240801174110781

创建OAuth2Controller实现微博登录

@Slf4j
@Controller
public class OAuth2Controller {

    @GetMapping(value = "/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code) throws Exception {
        Map<String, String> map = new HashMap<>();

        map.put("client_id","你的App Key");
        map.put("client_secret","你的App Secret");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code",code);

        //1、根据code换取access_token
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());

        //2、处理

        //2、登录成功跳回首页
        return "redirect:http://gulimall.com";
    }
}

image-20241008232343825

9.13社交登录完成

主要步骤:

  • ums_member添加三个字段,保存社交登录信息

    • socialUid:社交登录UID
    • accessToken:社交登录TOKEN
    • expiresIn:社交登录过期时间
  • 根据social_uid判断当前用户有没有注册

  • 这个用户已经注册过,更新用户的访问令牌的时间和access_token

  • 没有查到当前社交用户对应的记录我们就需要注册一个

    • 根据官方api查询当前社交用户的社交账号信息(昵称、性别等)

ums_member添加三个字段,保存社交登录信息

  • socialUid:社交登录UID
  • accessToken:社交登录TOKEN
  • expiresIn:社交登录过期时间

image-20241008235047573

根据social_uid判断当前用户有没有注册

这个用户已经注册过,更新用户的访问令牌的时间和access_token

没有查到当前社交用户对应的记录我们就需要注册一个

根据官方api查询当前社交用户的社交账号信息(昵称、性别等)

 @Override
    public MemberEntity login(SocialUser socialUser) throws Exception {

        //具有登录和注册逻辑
        String uid = socialUser.getUid();

        //1、判断当前社交用户是否已经登录过系统
        MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));

        if (memberEntity != null) {
            //这个用户已经注册过
            //更新用户的访问令牌的时间和access_token
            MemberEntity update = new MemberEntity();
            update.setId(memberEntity.getId());
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());
            baseMapper.updateById(update);

            memberEntity.setAccessToken(socialUser.getAccess_token());
            memberEntity.setExpiresIn(socialUser.getExpires_in());
            return memberEntity;
        } else {
            //2、没有查到当前社交用户对应的记录我们就需要注册一个
            MemberEntity register = new MemberEntity();
            //3、查询当前社交用户的社交账号信息(昵称、性别等)
            // 远程调用,不影响结果
            try {
                Map<String, String> query = new HashMap<>();
                query.put("access_token", socialUser.getAccess_token());
                query.put("uid", socialUser.getUid());
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String, String>(), query);

                if (response.getStatusLine().getStatusCode() == 200) {
                    //查询成功
                    String json = EntityUtils.toString(response.getEntity());
                    JSONObject jsonObject = JSON.parseObject(json);
                    String name = jsonObject.getString("name");
                    String gender = jsonObject.getString("gender");
                    String profileImageUrl = jsonObject.getString("profile_image_url");

                    register.setNickname(name);
                    register.setGender("m".equals(gender) ? 1 : 0);
                    register.setHeader(profileImageUrl);
                }
            }catch (Exception e){}
            register.setCreateTime(new Date());
            register.setSocialUid(socialUser.getUid());
            register.setAccessToken(socialUser.getAccess_token());
            register.setExpiresIn(socialUser.getExpires_in());

            //把用户信息插入到数据库中
            baseMapper.insert(register);
            return register;
        }
    }

9.14社交登录测试

数据库添加social_uidaccess_tokenexpires_in

image-20241009003100291

login.html修改自己的App Keyredirect_uri

http://auth.gulimall.com/oauth2.0/weibo/success

image-20241009003215024

OAuth2Controller修改自己的App KeyApp Secret

image-20241009003731369

开放平台里高级信息配置自己的授权回调页面

image-20241009003905054

9.15分布式session不共享不同步

主要步骤:

  • session原理
  • session共享问题

session原理

image-20240801211147323

session共享问题

  • 同一个服务,多个实例
  • 不同服务

image-20240801211234678

9.16分布式session解决方案原理

主要步骤:

  • session复制
  • 客户端存储
  • hash一致性
  • 统一存储
  • 不能跨域名共享cookie:子域session共享,放大作用域

session复制image-20240801213107886

客户端存储

image-20240801213213032

hash一致性

image-20240801213229184

统一存储

image-20240801213324772

子域session共享,放大作用域

image-20240801213731303

9.17SpringSession整合

主要步骤:

  • 地址:https://spring.io/projects/spring-session
  • 导入依赖spring-session,配置session
  • 开启Redis作为session存储
  • 登录成功后,保存用户信息到session
  • gulimall-product登录成功后获取session信息
  • 修改session域名
  • 问题:Could not transfer artifact不知道这样的主机。

打开SpringSession官方文档

image-20240801215913021

认证服务gulimall-auth-service和商品服务gulimall-product

导入依赖spring-session,配置session

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

application.yml配置

server:
  servlet:
    session:
      timeout: 30m
spring:
  session:
    store-type: redis      

image-20240802122845072

开启@EnableRedisHttpSession

image-20240802122939633

登录成功后,保存用户信息到session

因为微博登录需要申请开发者权限,这里暂时没有申请成功,使用登录功能一样可以测试session

MemberResponseVo loginUser = login.getData(new TypeReference<MemberResponseVo>() {});
session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);

image-20240802123112141

Redis里也保存成功session数据

image-20240802124443236

gulimall-product登录成功后获取session信息

<li>
  <a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>
  <a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/login.html">你好,请登录</a>
</li>

image-20240802123319338

登录成功后,跳转到http://gulimall.com/,此时session的域名是auth.gulimall.com,因为子域名之间无法共享session,需要修改成父域名.gulimall.com,然后就可以正常获取session里的登录信息了

image-20240802123405079

问题:Could not transfer artifact不知道这样的主机。

Could not transfer artifact org.springframework.session:spring-session-bom:pom:2.5.7 from/to alimaven (http://maven.aliyun.com/nexus/content/repositories/central/): 不知道这样的主机。 (maven.aliyun.com)

导入spring-session一只导入失败,后来注释掉relativePath正常了

image-20240802114748673

9.18自定义SpringSession完成子域Session共享

主要步骤:

  • 解决子域共享问题
  • JSON序列化保存session数据到Redis
  • 清空Redis和浏览器中的session数据

gulimall-auth-servicegulimall-product中添加配置GulimallSessionConfig

  • 设置父域名解决子域共享问题
  • 使用Jackson解决Redis数据序列化问题
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        //放大作用域
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }
    
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

重启gulimall-auth-servicegulimall-product,然后清空Redis和浏览器中的session数据

image-20240802125443355

重新登录

image-20240802125637026

9.19SpringSession原理

主要步骤:

  • 1.EnableRedisHttpSession导入RedisHttpSessionConfiguration
  • 2.RedisHttpSessionConfiguration添加了一个组件RedisIndexedSessionRepository封装Redis操作session的增删改查
  • 3.RedisHttpSessionConfiguration继承了SpringHttpSessionConfigurationSpringHttpSessionConfiguration注入了SessionRepositoryFilter,每个请求都必须经过filter
  • SessionRepositoryFilter创建的时候构造器注入了SessionRepository
  • SessionRepositoryFilter的方法doFilterInternal包装了requestresponse
    • SessionRepositoryRequestWrapper
    • SessionRepositoryResponseWrapper
  • SessionRepositoryFilter的方法getSession是从sessionRepository获取的

EnableRedisHttpSession导入RedisHttpSessionConfiguration

image-20240803143241280

RedisHttpSessionConfiguration添加了一个组件RedisIndexedSessionRepository封装Redis操作session的增删改查

image-20240803143159178

RedisIndexedSessionRepository的主要方法

image-20240803144803765

RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration

image-20240803143325410

SpringHttpSessionConfiguration注入了SessionRepositoryFilter,每个请求都必须经过filter

image-20240803143414394

SessionRepositoryFilter的方法doFilterInternal包装了requestresponse

  • SessionRepositoryRequestWrapper
  • SessionRepositoryResponseWrapper

image-20240803143907626

SessionRepositoryFiltergetSession方法

image-20240803144304980

SessionRepositoryFilter的方法getSession是从sessionRepository获取的

image-20240803145830477

9.20页面效果完成

主要步骤:

  • 1.登录成功设置session信息
  • 2.登录成功不能跳转login.html登录页
  • 3.gulimall-search搜索服务添加SpringSession配置
  • 4.商品搜索页和商品详情页都需要更新登录信息
    • gulimall-searchlist.html
    • gulimall-productitem.html

登录成功设置session信息

image-20240803152345960

登录成功不能跳转login.html登录页

  • 注释GulimallWebConfig跳转login.html自定义导航

  • loginPage方法用于判断跳转login.html时如果登录直接跳转首页

image-20240803152432993

gulimall-search导入SpringSession依赖

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

并添加RedisSpringSession配置

server:
  port: 8208
  servlet:
    session:
      timeout: 30m
spring:
  redis:
    host: 192.168.188.180
    port: 6379
  session:
    store-type: redis

image-20240803152852849

商品搜索页和商品详情页都需要更新登录信息

gulimall-searchlist.html

image-20240803153519803

gulimall-productitem.html

image-20240803153746794

9.21单点登录简介

多个不同域名下,springsession无法共享

单点登录特性:非父子域名下共享登录状态

  • 一处退出,处处退出
  • 一处登录,处处登录

原理:

  • 1.客户端访问认证中心并带上回调url,进行登录
  • 2.登录成功认证中心域名下设置cookie,并跳转url?token=xxx,携带token参数
  • 3.客户端根据tokne请求认证中心获取用户信息【微博是用code获取AcsessToken,然后根据AcsessToken获取信息】
  • 4.客户端2再访问认证中心时,会带上浏览器存储的cookie,从而直接登录通过

image-20240803155341529

9.22框架效果演示

主要步骤:

  • gitee搜索xxl-sso,然后下载
  • 配置hosts文件
  • 配置单点登录服务xxl-sso-server
  • 配置测试客户端xxl-sso-server\xxl-sso-samples\xxl-sso-web-sample-springboot
  • xxl-sso项目打包
  • 运行单点服务和客户端服务

gitee搜索xxl-sso,然后下载

地址:https://gitee.com/xuxueli0323/xxl-sso

image-20240803163228207

配置hosts文件

127.0.0.1           ssoserver.com
127.0.0.1           client1.com
127.0.0.1           client2.com

image-20240803163415918

配置单点登录服务xxl-sso-server

在目录下.\xxl-sso\xxl-sso-server\src\main\resources\application.properties

主要配置运行端口(这里为了防止端口冲突)和redis地址

image-20240803163519792

配置测试客户端xxl-sso-server\xxl-sso-samples\xxl-sso-web-sample-springboot

在目录下.\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\src\main\resources\application.properties

主要配置单点登录服务和redis地址

image-20240803163931358

xxl-sso项目打包

mvn clean package -Dmaven.skip.test=true

image-20240803163305222

运行单点服务

java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar

image-20240803164316712

运行客户端服务1

java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8501

image-20240803164254569

运行客户端服务2

java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8502

image-20240803164357788

访问三个服务,发现登录一个其他服务都是登录状态,退出状态也同步

ssoserver.com:8500/xxl-sso-server/login
ssoserver.com:8500/xxl-sso-server/login?redirect_url=http://client1.com:8501/xxl-sso-web-sample-springboot/
ssoserver.com:8500/xxl-sso-server/login?redirect_url=http://client2.com:8502/xxl-sso-web-sample-springboot/

image-20240803164501367

9.23单点登录流程-1

SSO核心:

  • 1.中央认证服务器:ssoserver.com
  • 2.其他系统想要登录去ssoserver.com,登录成功跳转回来
  • 3.只要有一个系统登录,其他都不用登录
  • 全系统唯一一个sso-sessionid,所有系统域名可能都不相同

主要步骤:

  • 1.创建sso测试客户端peng-sso-client
    • employees接口需要登录成功才能调用,否则跳转到登录页面login.ghtml
    • 跳转到登录页面login.ghtml需要带上当前页面的地址的参数redirect_url
  • 2.创建sso测试服务端peng-sso-serve
    • 访问登录页面login.ghtml的时候直接返回login.ghtml
  • 3.测试

创建sso测试客户端peng-sso-client,配置运行端口为8081

访问/employees如果没有获取到session就跳转到login.html,但是带上当前地址redirect_url=http://client1.com:8081/employees

@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
    return "hello";
}

@GetMapping(value = "/employees")
public String employees(Model model, HttpSession session) {
    Object loginUser = session.getAttribute("loginUser");
    if (loginUser == null) {
        return "redirect:" + "http://ssoserver.com:8080/login.html"+"?redirect_url=http://client1.com:8081/employees";
        // return "redirect:" + "http://localhost:8080/login.html"+"?redirect_url=http://localhost:8081/employees";
    } else {
        List<String> emps = new ArrayList<>();
        emps.add("张三");
        emps.add("李四");
        model.addAttribute("emps", emps);
        return "employees";
    }
}

image-20240803183044642

创建sso测试服务端peng-sso-serve,配置运行端口为8082

@GetMapping("/login.html")
public String loginPage() {
    return "login";
}

image-20240803183236369

启动项目,访问client1.com:8081/employees,发现直接重定向到登录页了

ssoserver.com:8080/login.html
client1.com:8081/employees
client1:8081/hello

http://localhost:8081/hello
http://localhost:8081/employees
http://localhost:8080/login.html

image-20240803183430239

9.24单点登录流程-2

主要步骤:

  • 1.loginPage跳转到login.html时需要获取跳转过来页面的地址redirect_url,因为登录成功需要再跳转回去
  • 2.doLogin登录的时候需要带上redirect_url,然后带上token跳转回去
  • 3.成功跳转到employees时判断token,这里只是简单判断获取到token就算登录成功,把用户信息写到session

loginPage跳转到login.html时需要获取跳转过来页面的地址redirect_url,把redirect_url复制给隐藏域

doLogin登录的时候需要带上redirect_url,然后生成UUID模拟token,存入redis后跳转回原来页面

image-20240803212132376

成功跳转到employees时判断token,这里只是简单判断获取到token就算登录成功,把用户信息写到session

image-20240803212431305

9.25单点登录流程-3

主要步骤:

  • 在创建一个客户端服务peng-sso-client2

  • 首先访问peng-sso-client,因为没有登录会重定向到peng-sso-serve

  • peng-sso-servedoLogin登录成功后

    • peng-sso-serve会使用session保存当前token
    • 带上token重定向peng-sso-client
    • sso_token添加到cookie
  • peng-sso-client登陆成功后重定向/employees(当前服务)时,根据传入的token使用HttpClient发起http请求调用peng-sso-serve获取登录信息设置到session

  • peng-sso-client2访问/boss时,此时peng-sso-serve已存在sso_tokenpeng-sso-serve会带上sso_token转发回来,peng-sso-client2根据传入的token使用HttpClient发起http请求调用peng-sso-serve获取登录信息设置到session

首先访问peng-sso-client,因为没有登录会重定向到peng-sso-serve

peng-sso-servedoLogin登录成功后

  • 带上token重定向peng-sso-client
  • sso_token添加到cookie

image-20240803230651773

peng-sso-servedoLogin登录成功后peng-sso-serve会使用session保存当前token

image-20240803231045514

peng-sso-client登陆成功后重定向/employees(当前服务)时,根据传入的token使用HttpClient发起http请求调用peng-sso-serve获取登录信息设置到session

image-20240803230903950

peng-sso-client2访问/boss时,此时peng-sso-serve已存在sso_tokenpeng-sso-serve会带上sso_token转发回来,peng-sso-client2根据传入的token使用HttpClient发起http请求调用peng-sso-serve获取登录信息设置到session

image-20240803230921985

测试地址

http://client1.com:8081/employees
http://client2.com:8082/boss
http://ssoserver.com:8080/login.html

10.商城业务-购物车

10.1环境搭建

主要步骤:

  • 创建gulimall-cartapplication.yml配置服务注册

  • 配置gulimall-cartpom.xml,此服务暂不需要mybatis-plus

  • 配置hosts文件

  • 上传购物车的静态资源到nginx

  • 配置nginx

  • 配置gulimall-gateway网关服务

  • gulimall-cart添加cartList.htmlsuccess.htmlcartList.html改为index.html方便测试

  • 修改cartList.htmlsuccess.html的静态资源访问地址

  • 测试访问http://cart.gulimall.com/

创建gulimall-cartapplication.yml配置服务注册

image-20240803235921049

配置gulimall-cartpom.xml,此服务暂不需要mybatis-plus

<exclusions>
    <exclusion>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </exclusion>
    <exclusion>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </exclusion>
</exclusions>

image-20240804000002637

管理员运行SwicthHosts配置hosts文件

192.168.188.180     cart.gulimall.com

image-20240804000053483

上传购物车的静态资源到nginx/root/mall/nginx/html/static/cart/目录下

image-20240804000143935

配置nginx,因为*.gulimall.com匹配 cart.gulimall.com,这里不需要多加配置,留意一下即可

image-20240804000233876

配置gulimall-gateway网关服务

        - id: gulimall_cart_route
          uri: lb://gulimall-cart
          predicates:
            - Host=cart.gulimall.com

image-20240804000350900

gulimall-cart添加cartList.htmlsuccess.htmlcartList.html改为index.html方便测试

image-20240804000436750

修改cartList.htmlsuccess.html的静态资源访问地址

修改href

href="
href="/static/cart/

image-20240804000526398

修改src

src="
src="/static/cart/

image-20240804000610733

测试访问http://cart.gulimall.com/

image-20240804000707612

10.2数据模型分析

游客购物车/离线购物车:

  • 1.未登录状态下加入购物车的商品
  • 2.关闭浏览器后再打开,商品仍然存在
  • 3.采用redis【很好的高并发性能,强于MongoDB
  • 4.使用user-key【相当于UUID,存在于cookie中】成为临时用户【如果没有user-key,第一次访问购物车时,会自动分配一个user-key(临时用户身份)】

逻辑:

  • 1)第一次使用购物车功能,创建user-key(分配临时用户身份)
  • 2)访问购物车时,判断当前是否登录状态(session是否存在用户信息)登录状态则获取用户购物车信息
  • 3)未登录状态,则获取临时用户身份,获取游客购物车

用户购物车/在线购物车:

  • 1.会将游客状态下的购物车,整合到登录用户名下的购物车
  • 2.游客购物车被清空(此时退出登录游客购物车已被清空)
  • 3.采用redis
  • 4.因为要获取用户登录状态,所以需要整合springsession

购物车数据结构:

Map<String k1, Map<String k2, CartItemInfo>>

key:用户标示
	登录态:gulimall:cart:userId
	非登录态:gulimall:cart:userKey

value:
	存储一个Hash结构的值,其中该hash结构的key是SkuId,hash结构的value是商品信息,以json字符串格式存储

image-20240804002522602

10.3VO编写

/**
 * 购物车VO
 * 需要计算的属性需要重写get方法,保证每次获取属性都会进行计算
 */
public class CartVO {

    private List<CartItemVO> items; // 购物项集合
    private Integer countNum;       // 商品件数(汇总购物车内商品总件数)
    private Integer countType;      // 商品数量(汇总购物车内商品总个数)
    private BigDecimal totalAmount; // 商品总价
    private BigDecimal reduce = new BigDecimal("0.00");// 减免价格

    public List<CartItemVO> getItems() {
        return items;
    }

    public void setItems(List<CartItemVO> items) {
        this.items = items;
    }

    public Integer getCountNum() {
        int count = 0;
        if (items != null && items.size() > 0) {
            for (CartItemVO item : items) {
                count += item.getCount();
            }
        }
        return count;
    }

    public Integer getCountType() {
        return CollectionUtils.isEmpty(items) ? 0 : items.size();
    }


    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        // 1、计算购物项总价
        if (!CollectionUtils.isEmpty(items)) {
            for (CartItemVO cartItem : items) {
                if (cartItem.getCheck()) {
                    amount = amount.add(cartItem.getTotalPrice());
                }
            }
        }
        // 2、计算优惠后的价格
        return amount.subtract(getReduce());
    }

    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}

/**
 * 购物项VO(购物车内每一项商品内容)
 */
public class CartItemVO {
    private Long skuId;                     // skuId
    private Boolean check = true;           // 是否选中
    private String title;                   // 标题
    private String image;                   // 图片
    private List<String> skuAttrValues;     // 销售属性
    private BigDecimal price;               // 单价
    private Integer count;                  // 商品件数
    private BigDecimal totalPrice;          // 总价

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public Boolean getCheck() {
        return check;
    }

    public void setCheck(Boolean check) {
        this.check = check;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public List<String> getSkuAttrValues() {
        return skuAttrValues;
    }

    public void setSkuAttrValues(List<String> skuAttrValues) {
        this.skuAttrValues = skuAttrValues;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    /**
     * 计算当前购物项总价
     */
    public BigDecimal getTotalPrice() {
        return this.price.multiply(new BigDecimal("" + this.count));
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }
}

image-20240804020333353

10.4ThreadLocal用户身份鉴别

游客购物车/离线购物车:

  • 第一次使用购物车功能,没有登录,创建user-key(分配临时用户身份)
  • 访问购物车时,判断当前是否登录状态(session是否存在用户信息)
  • 登录状态则获取用户购物车信息
  • 未登录状态,则获取临时用户身份,获取游客购物车

项目搭建步骤:

  • 集成Redis
  • 集成SpringSession,配置SpringSession域名和过期时间
  • 创建拦截器获取用户身份信息
    • 创建CartInterceptor拦截器
    • 创建GulimallWebConfig使用CartInterceptor拦截器
  • 创建测试controller

导入RedisSpringSession依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<!--SpringSession-->
 <dependency>
     <groupId>org.springframework.session</groupId>
     <artifactId>spring-session-data-redis</artifactId>
  </dependency>

image-20240805013151683

配置RedisSpringSession

image-20240805014254374

配置SpringSession域名和过期时间

image-20240805014456514

创建拦截器获取用户身份信息

  • 创建CartInterceptor拦截器
  • 创建GulimallWebConfig使用CartInterceptor拦截器

image-20240805014343979

测试,访问http://cart.gulimall.com/

@GetMapping(value = "/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
    //快速得到用户信息:id,user-key
    UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();

    // CartVo cartVo = cartService.getCart();
    // model.addAttribute("cart",cartVo);
    return "cartList";
}

image-20240805014548984

10.5页面环境搭建

主要步骤:

gulimall-product/item.html立即预约,改为加入购物车

image-20240805221708600

gulimall-product/index.html我的购物车

image-20240805214808384

gulimall-cart/success.html首页

image-20240805220134874

gulimall-cart/success.html去购物车结算

image-20240805220518638

gulimall-cart/success.html查看商品详情

image-20240805220733014

gulimall-cart/cartList.html首页

image-20240805220205773

10.6添加购物车

主要步骤:

  • 商品服务gulimall-product/item.html请求购物车服务gulimall-cart添加购物车接口
  • gulimall-cart创建添加购物车接口
  • gulimall-cart/success.html界面显示购物车列表
  • gulimall-cart实现添加购物车接口
    • 使用BoundHashOperations获取购物车redis操作对象,登录使用UserId,未登录使用UserKey
    • 如果redis不存在key就使用redis创建购物车信息
    • 远程调用gulimall-product获取sku基本信息pms_sku_info
    • 远程调用gulimall-product获取sku销售属性pms_sku_sale_attr_value
    • 导入异步编排CompletableFuture,使用CompletableFuture优化gulimall-product接口调用
    • 如果redis存在key就根据key获取此商品修改数量即可
  • 未登录测试
  • 登录测试

商品服务gulimall-product/item.html请求购物车服务gulimall-cart添加购物车接口

image-20240805233321864

gulimall-cart创建添加购物车接口

image-20240805234322259

gulimall-cart/success.html界面显示购物车列表

image-20240805234350606

配置异步线程编排

image-20240805234523744

顺便检查一下RedisSpringSession的配置

image-20240805234638916

gulimall-cart实现添加购物车接口

  • 使用BoundHashOperations获取购物车redis操作对象,登录使用UserId,未登录使用UserKey
  • 如果redis不存在key就使用redis创建购物车信息
  • 远程调用gulimall-product获取sku基本信息pms_sku_info
  • 远程调用gulimall-product获取sku销售属性pms_sku_sale_attr_value
  • 导入异步编排CompletableFuture,使用CompletableFuture优化gulimall-product接口调用
  • 如果redis存在key就根据key获取此商品修改数量即可
 @Autowired
 StringRedisTemplate redisTemplate;

 @Autowired
 private ProductFeignService productFeignService;

 @Autowired
 private ThreadPoolExecutor executor;

@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
    // 获取购物车redis操作对象
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    // 获取商品
    String productRedisValue = (String) cartOps.get(skuId.toString());
     //如果没有就添加数据
    if (StringUtils.isEmpty(productRedisValue)) {

        //2、添加新的商品到购物车(redis)
        CartItemVo cartItemVo = new CartItemVo();
        //开启第一个异步任务
        CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
            //1、远程查询当前要添加商品的信息
            R productSkuInfo = productFeignService.getInfo(skuId);
            SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
            //数据赋值操作
            cartItemVo.setSkuId(skuInfo.getSkuId());
            cartItemVo.setTitle(skuInfo.getSkuTitle());
            cartItemVo.setImage(skuInfo.getSkuDefaultImg());
            cartItemVo.setPrice(skuInfo.getPrice());
            cartItemVo.setCount(num);
        }, executor);

        //开启第二个异步任务
        CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
            //2、远程查询skuAttrValues组合信息
            R skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
            List<String> skustrs = skuSaleAttrValues.getData("skuSaleAttrValues", new TypeReference<List<String>>() {});
            cartItemVo.setSkuAttrValues(skustrs);
        }, executor);

        //等待所有的异步任务全部完成
        CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();

        String cartItemJson = JSON.toJSONString(cartItemVo);
        cartOps.put(skuId.toString(), cartItemJson);

        return cartItemVo;
    } else {
        //购物车有此商品,修改数量即可
        CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
        cartItemVo.setCount(cartItemVo.getCount() + num);
        //修改redis的数据
        String cartItemJson = JSON.toJSONString(cartItemVo);
        cartOps.put(skuId.toString(),cartItemJson);

        return cartItemVo;
    }
}

/**
 * 根据用户信息获取购物车redis操作对象
 */
private BoundHashOperations<String, Object, Object> getCartOps() {
    // 获取用户登录信息
    UserInfoTo userInfo = CartInterceptor.toThreadLocal.get();
    String cartKey = "";
    if (userInfo.getUserId() != null) {
        // 登录态,使用用户购物车
        cartKey = CartConstant.CART_PREFIX + userInfo.getUserId();
    } else {
        // 非登录态,使用游客购物车
        cartKey = CartConstant.CART_PREFIX + userInfo.getUserKey();
    }
    // 绑定购物车的key操作Redis
    BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
    return operations;
}

未登录测试

image-20240805234742185

登录测试

image-20240805234834013

10.7添加购物车细节

主要步骤:

  • 购物车有此商品,修改数量即可

image-20240805234932081

10.8RedirectAttribute

接口防刷:

如果刷新cart.gulimall.com/addToCart?skuId=7&num=1该页面,会导致购物车中此商品的数量无限新增

解决方案:

  • /addToCart请求使用重定向给/addToCartSuccessPage.html
  • /addToCartSuccessPage.html这个请求跳转"商品已成功加入购物车页面"(浏览器url请求已更改),达到防刷的目的

主要步骤:

  • /addToCart使用RedirectAttributes带上skuId,并且执行完成重定向到addToCartSuccessPage.html
  • addToCartSuccessPage查询Redis获取购物车信息

/addToCart使用RedirectAttributes带上skuId,并且执行完成重定向到addToCartSuccessPage.html

  /**
     * 添加商品到购物车
     *
     * @return
     */
    @GetMapping(value = "/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes redirectAttributes
    ) throws ExecutionException, InterruptedException {
        cartService.addToCart(skuId, num);
        redirectAttributes.addAttribute("skuId", skuId);// 会在url后面拼接参数
        return "redirect:http://cart.gulimall.com/addToCartSuccessPage.html";
    }

    /**
     * 跳转到添加购物车成功页面
     * @param skuId
     * @param model
     * @return
     */
    @GetMapping(value = "/addToCartSuccessPage.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,
                                       Model model) {
        //重定向到成功页面。再次查询购物车数据即可
        CartItemVo cartItemVo = cartService.getCartItem(skuId);
        model.addAttribute("cartItem",cartItemVo);
        return "success";
    }

image-20240806001009238

addToCartSuccessPage查询Redis获取购物车信息

@Override
public CartItemVo getCartItem(Long skuId) {
    //拿到要操作的购物车信息
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    String redisValue = (String) cartOps.get(skuId.toString());
    CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);
    return cartItemVo;
}

image-20240806000953786

此时我们刷新购物车界面,商品数量不会增加

image-20240806021140604

10.9获取&合并购物车

主要步骤:

  • 实现获取购物车接口getCart

    • 如果登录,合并在线、临时购物车,addToCart方法支持合并购物车,之后清除临时购物车

    • 封装清空购物车

    • 如果未登录,获取临时购物车数据

  • 渲染购物车界面,展示购物车数据

  • 登录渲染

  • 测试

实现获取购物车接口getCart

  • 如果登录,合并在线、临时购物车,addToCart方法支持合并购物车,之后清除临时购物车

  • 封装清空购物车

  • 如果未登录,获取临时购物车数据

/**
 * 获取用户登录或者未登录购物车里所有的数据
 * @return
 * @throws ExecutionException
 * @throws InterruptedException
 */
@Override
public CartVo getCart() throws ExecutionException, InterruptedException {
    CartVo cartVo = new CartVo();
    UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
    if (userInfoTo.getUserId() != null) {
        //1、登录
        String cartKey = CART_PREFIX + userInfoTo.getUserId();
        //临时购物车的键
        String temptCartKey = CART_PREFIX + userInfoTo.getUserKey();

        //2、如果临时购物车的数据还未进行合并
        List<CartItemVo> tempCartItems = getCartItems(temptCartKey);
        if (tempCartItems != null) {
            //临时购物车有数据需要进行合并操作
            for (CartItemVo item : tempCartItems) {
                addToCart(item.getSkuId(),item.getCount());
            }
            //清除临时购物车的数据
            clearCartInfo(temptCartKey);
        }

        //3、获取登录后的购物车数据【包含合并过来的临时购物车的数据和登录后购物车的数据】
        List<CartItemVo> cartItems = getCartItems(cartKey);
        cartVo.setItems(cartItems);

    } else {
        //没登录
        String cartKey = CART_PREFIX + userInfoTo.getUserKey();
        //获取临时购物车里面的所有购物项
        List<CartItemVo> cartItems = getCartItems(cartKey);
        cartVo.setItems(cartItems);
    }

    return cartVo;
}

/**
 * 根据购物车的key获取
 */
private List<CartItemVo> getCartItems(String cartKey) {
    BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
    List<Object> values = operations.values();
    if (!CollectionUtils.isEmpty(values)) {
        // 购物车非空,反序列化成商品并封装成集合返回
        return values.stream()
                .map(jsonString -> JSONObject.parseObject((String) jsonString, CartItemVo.class))
                .collect(Collectors.toList());
    }
    return null;
}

/**
 * 清空购物车
 */
@Override
public void clearCartInfo(String cartKey) {
    redisTemplate.delete(cartKey);
}

image-20240806022059252

渲染购物车界面,展示购物车数据

image-20240806022304502

登录渲染

image-20240806022400949

测试

未登录购买商品

image-20240806022659962商品skuId=1skuId=10各购买3个

image-20240806022957699

登录后访问购物车,临时购物车已经合并到在线购物车

image-20240806023215531

再次购物skuId=1号商品3个,发现数量成功合并

image-20240806023311953

10.10选中购物车

主要步骤:

  • 页面选中时/取消选中时页面带上skuIdchecked请求checkItem接口
  • 实现checkItem,根据传来的skuId获取数据,然后更新选中状态
  • 测试

页面选中时/取消选中时页面带上skuIdchecked请求checkItem接口

image-20240806024731113

实现checkItem,根据传来的skuId获取数据,然后更新选中状态

image-20240806024904078

测试,点击选中,redis数据正常更新

image-20240806024357683

10.11改变购物项数量

主要步骤:

  • 页面+/-选中时页面带上skuIdnum请求countItem接口
  • 实现countItem,根据传来的skuId获取数据,然后更新数量
  • 测试

页面+/-选中时页面带上skuIdnum请求countItem接口

image-20240806025905032

实现countItem,根据传来的skuId获取数据,然后更新数量

image-20240806025926787

测试,点击+/-,redis数据正常更新

image-20240806030119324

10.12删除购物项

主要步骤:

  • 点击页面删除按钮时页面带上skuId请求deleteItem接口
  • 实现deleteItem,根据传来的skuId获取数据,然后删除数据
  • 测试

点击页面删除按钮时页面带上skuId请求deleteItem接口

image-20240806031025485

实现deleteItem,根据传来的skuId获取数据,然后删除数据

image-20240806031105586

测试

image-20240806030945878

posted @ 2024-10-14 00:58  peng_boke  阅读(10)  评论(0编辑  收藏  举报