前言
可以間接性墮落,但總不能一直清醒的墮落吧
9.商城業務-認證服務
9.1環境搭建
主要步驟:
-
建立
gulimall-auth-service
,application.yml
配置nacos
-
配置
gulimall-auth-service
的pom.xml
,此服務暫不需要mybatis-plus
-
配置
hosts
檔案 -
上傳登入和註冊的靜態資源到
nginx
-
配置
nginx
-
配置
gulimall-gateway
閘道器服務 -
gulimall-auth-service
新增登入頁和註冊頁,登入頁改為index.html
方便測試 -
修改登入頁和註冊頁的靜態資源訪問地址
-
測試訪問http://auth.gulimall.com/
建立gulimall-auth-service
,application.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地址
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>
管理員啟動SwitchHosts
,配置hosts
檔案
192.168.188.180 auth.gulimall.com
上傳登入和註冊的靜態資源到nginx
的/root/mall/nginx/html/static/
目錄下
配置nginx
,因為 auth.gulimall.com
匹配*.gulimall.com
,這裡不需要多加配置,留意一下即可
配置gulimall-gateway
閘道器服務,新增gulimall-auth-service
服務的轉發
- id: gulimall_auth_route
uri: lb://gulimall-auth-service
predicates:
- Host=auth.gulimall.com
gulimall-auth-service
新增登入頁和註冊頁,登入頁就為index.html
方便測試
修改登入頁和註冊頁的靜態資源訪問地址
登入頁
# 靜態資源路徑
href="
href="/static/login/
# 圖片路徑
src="
src="/static/login/
註冊頁
# 靜態資源路徑
href="
href="/static/reg/
# 圖片路徑
src="
src="/static/reg/
測試訪問http://auth.gulimall.com/
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";
}
}
登入頁
<!--頂部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>
登入頁:立即註冊
<h5 class="rig">
<img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_25.png" />
<span><a href="http://auth.gulimall.com/reg.html">立即註冊</a></span>
</h5>
商品服務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>
註冊頁:請登入
<div class="dfg">
<span>已有賬號?</span>
<a href="http://auth.gulimall.com/login.html">請登入</a>
</div>
傳送簡訊倒數計時
$(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");
}
}
然後可以刪掉LoginController
的導航
9.3整合簡訊驗證碼
主要步驟:
- 1.申請阿里雲簡訊驗證碼服務
- 2.整合並測試簡訊驗證碼服務
申請阿里雲簡訊驗證碼服務
地址:https://www.aliyun.com/benefit/waitou/V2?utm_content=se_1018076021
開啟雲市場
點選搜尋框,找到簡訊
隨便選擇一個服務商
選擇免費試用
記住自己的AppCode
,然後進入除錯服務
這裡有事例
整合並測試簡訊驗證碼服務
將事例中的連結程式碼複製到專案
找到HttpUtils
https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
複製到專案
封裝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();
}
}
}
測試SmsComponent
@Autowired
SmsComponent component;
@Test
public void testSms1(){
component.sendCode("15727328076","12345");
}
9.4驗證碼防刷校驗
主要步驟:
gulimall-third-party
封裝傳送簡訊驗證碼介面gulimall-auth-service
封裝遠端介面呼叫gulimall-third-party
傳送簡訊驗證碼gulimall-auth-service
傳送驗證碼介面- 介面防刷,60s內不能重複呼叫,獲取驗證後將驗證碼和當前時間都存入
redis
,前端呼叫時根據key當前手機號獲取存入的驗證碼和時間,如果當前時間-存入的時間小於60s直接返回
- 介面防刷,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();
}
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);
}
介面防刷,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();
}
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;
}
封裝註冊介面
@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";
}
修改前端頁面註冊提交
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";
}
}
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();
}
註冊介面實現
@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);
}
手機號異常
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);
}
使用BCryptPasswordEncoder
完成密碼MD5
加密
9.8註冊完成
主要步驟:
gulimall-auth-service
新增會員服務的註冊介面gulimall-auth-service
新增Redis
的json
序列化配置- 完成註冊功能,除錯透過
SmsSendController
需要@RestController
而不是@Controller
gulimall-auth-service
新增會員服務的註冊介面
gulimall-auth-service
新增Redis
的json
序列化配置
gulimall-auth-service/login.html
和gulimall-auth-service/reg.html
新增thymeleaf
名稱空間
<html lang="en" xmlns:th="http://www.thymeleaf.org">
SmsSendController
需要@RestController
而不是@Controller
否則會報Error resolving template [sms/sendCode], template might not exist or might not be accessible by any of the configured Template Resolvers
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">登 錄</a></button>
</li>
</ul>
</form>
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;
}
gulimall-auth-service
遠端呼叫登入介面
@PostMapping(value = "/member/member/login")
R login(@RequestBody UserLoginVo vo);
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";
}
}
註釋MemberEntity
以下欄位
9.10OAuth2.0
9.11weibo登入測試
- 登入weibo,開啟微博開發平臺
- 開發者資訊,需要輸入基本資訊和身份驗證
- 進入weibo授權頁面
- 使用者登入weibo成功獲取code
- 使用code獲取access_token,
- 使用code獲取access_token只能用一次
- 同一個使用者的access_token一段時間是不會變化的,即使獲取多次
首先註冊微博賬號,申請開發者許可權
9.11.1網站接入
登入微博開發平臺:https://open.weibo.com/,選擇微連線,選擇網站接入,選擇立即接入
建立網頁應用
這裡的App Key
,App Secret
在高階資訊裡配置登入成功回撥和登入失敗回撥
選擇文件,滑動到網頁最下面,檢視OAuth2.0授權認證
Web網站的授權
總共4步
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 Key
,redirect_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>
使用者進行登入授權
如果使用者同意授權,頁面跳轉至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
登入成功後微博跳轉到了http://gulimall.com/success
,並帶上了code
http://gulimall.com/success?code=598bb71e0ec19cba2369c78d16199eca
然後換取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
請求oauth2/access_token
,獲取access_token
https://api.weibo.com/oauth2/access_token
根據使用者ID獲取使用者資訊
文件地址:https://open.weibo.com/wiki/2/users/show
請求users/show
,獲取使用者資訊
https://api.weibo.com/2/users/show.json
OAuth
授權之後,獲取授權使用者的UID
文件地址:https://open.weibo.com/wiki/2/account/get_uid
請求account/get_uid
,獲取授權使用者的UID
9.12社交登入回撥
流程圖
建立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";
}
}
9.13社交登入完成
主要步驟:
-
ums_member
新增三個欄位,儲存社交登入資訊socialUid
:社交登入UID
accessToken
:社交登入TOKEN
expiresIn
:社交登入過期時間
-
根據
social_uid
判斷當前使用者有沒有註冊 -
這個使用者已經註冊過,更新使用者的訪問令牌的時間和
access_token
-
沒有查到當前社交使用者對應的記錄我們就需要註冊一個
- 根據官方
api
查詢當前社交使用者的社交賬號資訊(暱稱、性別等)
- 根據官方
ums_member
新增三個欄位,儲存社交登入資訊
socialUid
:社交登入UID
accessToken
:社交登入TOKEN
expiresIn
:社交登入過期時間
根據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_uid
、access_token
、expires_in
login.html
修改自己的App Key
和redirect_uri
http://auth.gulimall.com/oauth2.0/weibo/success
OAuth2Controller
修改自己的App Key
、App Secret
開放平臺裡高階資訊配置自己的授權回撥頁面
9.15分散式session不共享不同步
主要步驟:
session
原理session
共享問題
session
原理
session
共享問題
- 同一個服務,多個例項
- 不同服務
9.16分散式session解決方案原理
主要步驟:
session
複製- 客戶端儲存
hash
一致性- 統一儲存
- 不能跨域名共享
cookie
:子域session
共享,放大作用域
session
複製
客戶端儲存
hash
一致性
統一儲存
子域session
共享,放大作用域
9.17SpringSession整合
主要步驟:
- 地址:https://spring.io/projects/spring-session
- 匯入依賴
spring-session
,配置session
- 開啟
Redis
作為session
儲存 - 登入成功後,儲存使用者資訊到
session
gulimall-product
登入成功後獲取session
資訊- 修改
session
域名 - 問題:Could not transfer artifact不知道這樣的主機。
開啟SpringSession
官方文件
認證服務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
開啟@EnableRedisHttpSession
登入成功後,儲存使用者資訊到session
因為微博登入需要申請開發者許可權,這裡暫時沒有申請成功,使用登入功能一樣可以測試session
MemberResponseVo loginUser = login.getData(new TypeReference<MemberResponseVo>() {});
session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
Redis
裡也儲存成功session
資料
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>
登入成功後,跳轉到http://gulimall.com/
,此時session
的域名是auth.gulimall.com
,因為子域名之間無法共享session
,需要修改成父域名.gulimall.com
,然後就可以正常獲取session
裡的登入資訊了
問題: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
正常了
9.18自定義SpringSession完成子域Session共享
主要步驟:
- 解決子域共享問題
JSON
序列化儲存session
資料到Redis
- 清空
Redis
和瀏覽器中的session
資料
在gulimall-auth-service
和gulimall-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-service
和gulimall-product
,然後清空Redis
和瀏覽器中的session
資料
重新登入
9.19SpringSession原理
主要步驟:
- 1.
EnableRedisHttpSession
匯入RedisHttpSessionConfiguration
- 2.
RedisHttpSessionConfiguration
新增了一個元件RedisIndexedSessionRepository
封裝Redis
操作session
的增刪改查 - 3.
RedisHttpSessionConfiguration
繼承了SpringHttpSessionConfiguration
,SpringHttpSessionConfiguration
注入了SessionRepositoryFilter
,每個請求都必須經過filter
SessionRepositoryFilter
建立的時候構造器注入了SessionRepository
SessionRepositoryFilter
的方法doFilterInternal
包裝了request
、response
SessionRepositoryRequestWrapper
SessionRepositoryResponseWrapper
SessionRepositoryFilter
的方法getSession
是從sessionRepository
獲取的
EnableRedisHttpSession
匯入RedisHttpSessionConfiguration
RedisHttpSessionConfiguration
新增了一個元件RedisIndexedSessionRepository
封裝Redis
操作session
的增刪改查
RedisIndexedSessionRepository
的主要方法
RedisHttpSessionConfiguration
繼承了SpringHttpSessionConfiguration
,
SpringHttpSessionConfiguration
注入了SessionRepositoryFilter
,每個請求都必須經過filter
SessionRepositoryFilter
的方法doFilterInternal
包裝了request
、response
SessionRepositoryRequestWrapper
SessionRepositoryResponseWrapper
SessionRepositoryFilter
的getSession
方法
SessionRepositoryFilter
的方法getSession
是從sessionRepository
獲取的
9.20頁面效果完成
主要步驟:
- 1.登入成功設定
session
資訊 - 2.登入成功不能跳轉
login.html
登入頁 - 3.
gulimall-search
搜尋服務新增SpringSession
配置 - 4.商品搜尋頁和商品詳情頁都需要更新登入資訊
gulimall-search
的list.html
gulimall-product
的item.html
登入成功設定session
資訊
登入成功不能跳轉login.html
登入頁
-
註釋
GulimallWebConfig
跳轉login.html
自定義導航 -
loginPage
方法用於判斷跳轉login.html
時如果登入直接跳轉首頁
gulimall-search
匯入SpringSession
依賴
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
並新增Redis
和SpringSession
配置
server:
port: 8208
servlet:
session:
timeout: 30m
spring:
redis:
host: 192.168.188.180
port: 6379
session:
store-type: redis
商品搜尋頁和商品詳情頁都需要更新登入資訊
gulimall-search
的list.html
gulimall-product
的item.html
9.21單點登入簡介
多個不同域名下,springsession
無法共享
單點登入特性:非父子域名下共享登入狀態
- 一處退出,處處退出
- 一處登入,處處登入
原理:
- 1.客戶端訪問認證中心並帶上回撥url,進行登入
- 2.登入成功認證中心域名下設定cookie,並跳轉url?token=xxx,攜帶token引數
- 3.客戶端根據tokne請求認證中心獲取使用者資訊【微博是用code獲取AcsessToken,然後根據AcsessToken獲取資訊】
- 4.客戶端2再訪問認證中心時,會帶上瀏覽器儲存的cookie,從而直接登入透過
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
配置hosts
檔案
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
配置單點登入服務xxl-sso-server
在目錄下.\xxl-sso\xxl-sso-server\src\main\resources\application.properties
主要配置執行埠(這裡為了防止埠衝突)和redis
地址
配置測試客戶端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
地址
xxl-sso
專案打包
mvn clean package -Dmaven.skip.test=true
執行單點服務
java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar
執行客戶端服務1
java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8501
執行客戶端服務2
java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8502
訪問三個服務,發現登入一個其他服務都是登入狀態,退出狀態也同步
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/
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";
}
}
建立sso
測試服務端peng-sso-serve
,配置執行埠為8082
@GetMapping("/login.html")
public String loginPage() {
return "login";
}
啟動專案,訪問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
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
後跳轉回原來頁面
成功跳轉到employees
時判斷token
,這裡只是簡單判斷獲取到token
就算登入成功,把使用者資訊寫到session
9.25單點登入流程-3
主要步驟:
-
在建立一個客戶端服務
peng-sso-client2
-
首先訪問
peng-sso-client
,因為沒有登入會重定向到peng-sso-serve
-
peng-sso-serve
的doLogin
登入成功後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_token
,peng-sso-serve
會帶上sso_token
轉發回來,peng-sso-client2
根據傳入的token
使用HttpClient
發起http
請求呼叫peng-sso-serve
獲取登入資訊設定到session
中
首先訪問peng-sso-client
,因為沒有登入會重定向到peng-sso-serve
peng-sso-serve
的doLogin
登入成功後
- 帶上
token
重定向peng-sso-client
- 把
sso_token
新增到cookie
中
peng-sso-serve
的doLogin
登入成功後peng-sso-serve
會使用session
儲存當前token
peng-sso-client
登陸成功後重定向/employees
(當前服務)時,根據傳入的token
使用HttpClient
發起http
請求呼叫peng-sso-serve
獲取登入資訊設定到session
中
peng-sso-client2
訪問/boss
時,此時peng-sso-serve
已存在sso_token
,peng-sso-serve
會帶上sso_token
轉發回來,peng-sso-client2
根據傳入的token
使用HttpClient
發起http
請求呼叫peng-sso-serve
獲取登入資訊設定到session
中
測試地址
http://client1.com:8081/employees
http://client2.com:8082/boss
http://ssoserver.com:8080/login.html
10.商城業務-購物車
10.1環境搭建
主要步驟:
-
建立
gulimall-cart
,application.yml
配置服務註冊 -
配置
gulimall-cart
的pom.xml
,此服務暫不需要mybatis-plus
-
配置
hosts
檔案 -
上傳購物車的靜態資源到
nginx
-
配置
nginx
-
配置
gulimall-gateway
閘道器服務 -
gulimall-cart
新增cartList.html
和success.html
,cartList.html
改為index.html
方便測試 -
修改
cartList.html
和success.html
的靜態資源訪問地址 -
測試訪問http://cart.gulimall.com/
建立gulimall-cart
,application.yml
配置服務註冊
配置gulimall-cart
的pom.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>
管理員執行SwicthHosts
配置hosts
檔案
192.168.188.180 cart.gulimall.com
上傳購物車的靜態資源到nginx
的/root/mall/nginx/html/static/cart/
目錄下
配置nginx
,因為*.gulimall.com
匹配 cart.gulimall.com
,這裡不需要多加配置,留意一下即可
配置gulimall-gateway
閘道器服務
- id: gulimall_cart_route
uri: lb://gulimall-cart
predicates:
- Host=cart.gulimall.com
gulimall-cart
新增cartList.html
和success.html
,cartList.html
改為index.html
方便測試
修改cartList.html
和success.html
的靜態資源訪問地址
修改href
href="
href="/static/cart/
修改src
src="
src="/static/cart/
測試訪問http://cart.gulimall.com/
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字串格式儲存
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;
}
}
10.4ThreadLocal使用者身份鑑別
遊客購物車/離線購物車:
- 第一次使用購物車功能,沒有登入,建立
user-key
(分配臨時使用者身份) - 訪問購物車時,判斷當前是否登入狀態(session是否存在使用者資訊)
- 登入狀態則獲取使用者購物車資訊
- 未登入狀態,則獲取臨時使用者身份,獲取遊客購物車
專案搭建步驟:
- 整合
Redis
- 整合
SpringSession
,配置SpringSession
域名和過期時間 - 建立攔截器獲取使用者身份資訊
- 建立
CartInterceptor
攔截器 - 建立
GulimallWebConfig
使用CartInterceptor
攔截器
- 建立
- 建立測試
controller
匯入Redis
和SpringSession
依賴
<!-- 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>
配置Redis
和SpringSession
配置SpringSession
域名和過期時間
建立攔截器獲取使用者身份資訊
- 建立
CartInterceptor
攔截器 - 建立
GulimallWebConfig
使用CartInterceptor
攔截器
測試,訪問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";
}
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
立即預約,改為加入購物車
gulimall-product/index.html我的購物車
gulimall-cart/success.html
首頁
gulimall-cart/success.html
去購物車結算
gulimall-cart/success.html
檢視商品詳情
gulimall-cart/cartList.html
首頁
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
新增購物車介面
gulimall-cart
建立新增購物車介面
gulimall-cart/success.html
介面顯示購物車列表
配置非同步執行緒編排
順便檢查一下Redis
和SpringSession
的配置
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;
}
未登入測試
登入測試
10.7新增購物車細節
主要步驟:
- 購物車有此商品,修改數量即可
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";
}
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;
}
此時我們重新整理購物車介面,商品數量不會增加
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);
}
渲染購物車介面,展示購物車資料
登入渲染
測試
未登入購買商品
商品skuId=1
和skuId=10
各購買3個
登入後訪問購物車,臨時購物車已經合併到線上購物車
再次購物skuId=1
號商品3個,發現數量成功合併
10.10選中購物車
主要步驟:
- 頁面選中時/取消選中時頁面帶上
skuId
和checked
請求checkItem
介面 - 實現
checkItem
,根據傳來的skuId
獲取資料,然後更新選中狀態 - 測試
頁面選中時/取消選中時頁面帶上skuId
和checked
請求checkItem
介面
實現checkItem
,根據傳來的skuId
獲取資料,然後更新選中狀態
測試,點選選中,redis
資料正常更新
10.11改變購物項數量
主要步驟:
- 頁面+/-選中時頁面帶上
skuId
和num
請求countItem
介面 - 實現
countItem
,根據傳來的skuId
獲取資料,然後更新數量 - 測試
頁面+/-選中時頁面帶上skuId
和num
請求countItem
介面
實現countItem
,根據傳來的skuId
獲取資料,然後更新數量
測試,點選+/-,redis
資料正常更新
10.12刪除購物項
主要步驟:
- 點選頁面刪除按鈕時頁面帶上
skuId
請求deleteItem
介面 - 實現
deleteItem
,根據傳來的skuId
獲取資料,然後刪除資料 - 測試
點選頁面刪除按鈕時頁面帶上skuId
請求deleteItem
介面
實現deleteItem
,根據傳來的skuId
獲取資料,然後刪除資料
測試