03穀粒商城-高階篇三

peng_boke發表於2024-10-14

前言

可以間接性墮落,但總不能一直清醒的墮落吧

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立即預約 box-btns-two
  • gulimall-product/index.html我的購物車
  • gulimall-cart/success.html首頁
    • 首頁
    • 去購物車結算
    • 檢視商品詳情
  • gulimall-cart/cartList.html首頁
  • 測試地址:
    • http://cart.gulimall.com/addToCart
    • http://cart.gulimall.com/cart.html

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

相關文章