笔记链接:谷粒商城-个人笔记(高级篇三)
总说:
p213和p214的这些操作是完成用户在注册页面的发送验证码的操做:前台发送
/sms/sendcode
的请求给后台的gulimall-auth-server,然后gulimall-auth-server会先验证一下验证码是否在60秒前发送过,如果没有就使用OpenFeign远程调用gulimall-thrid-party的sendCode方法完成第三方服务的发送验证码功能
总说:
p215和p216和p217和p218的这些操作是完成用户在注册页面的注册功能:我们之前把验证码发给用户了,用户会填好验证码和注册信息后发送给gulimall-auth-server,然后gulimall-auth-server会进行初步校验(校验密码格式、手机号格式 + 验证码校验),校验通过会利用OpenFeign远程调用gulimall-member的regist()方法来进行会员注册,会员注册肯定有失败有成功,对于那些注册失败我们使用异常机制
给用户密码加密的三种方式对比:MD5加密、盐值加密、BCrypt加密
本届内容就是完成测试
总说:
前台发送 /login到后台的gulimall-auth-server模块中,然后gulimall-auth-server会使用OpenFeign远程调用gulimall-member的login()方法,在该方法中进行用户名密码比对完成登录
P221讲了去微博开放平台进行社交登录申请与测试
有两个地址很重要,
(1)是“ 在登录页引导用户至授权页”的地址:
这一步是前台完成的,前台html中的url要写成
Get
https://api.weibo.com/oauth2/authorize?client_id=1917008757&response_type=code&redirect_uri=http://gulimall.com/oauth2.0/weibo/success
client_id
:是你创建网站应用时的app key,
redirect_uri
是用户使用微博登录后重定向到哪里去。
我们指定redirect_uri=http://gulimall.com/oauth2.0/weibo/success
也就是说用户用户使用微博登录后,相当于发送 /oauth2.0/weibo/success
到后台的gulimall-auth-server
模块中,那么gulimall-auth-server
会使用code换取token
,这就涉及到换取token的url:
(2)是换取token的url
这一步是后台完成的,后台发送这样的url才能获取到token
POST
https://api.weibo.com/oauth2/access_token?client_id=1917008757&client_secret=94d9cc62c60d5f9f3d0c62389593024f&grant_type=authorization_code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success&code=CODE
client_id
: 创建网站应用时的app key;
client_secret
: 创建网站应用时的app secret
redirect_uri
: 认证完成后的跳转链接(需要和平台高级设置一致);
code
:换取令牌的认证码
后台发送这么个请求就可以根据用户授权返回的code换取token(换回来的不仅仅是token,还有uid用户id、expires_in令牌的过期时间等等),拿到token就可以向微博官方发送别的请求换取用户信息
p222—p224就是编码与测试
总说:
①前台发送
/oauth2.0/weibo/success
到后台的gulimall-auth-server
模块中,然后gulimall-auth-server
会先使用code换取token
,然后拿着token到OpenFeign远程调用gulimall-member
的oauth2Login()
方法,在该方法中会先判断用户是否是第一次用微博登录,如果是第一次的话我们就得给该用户注册(到微博里面查询该用户的基本信息存储到咱们的数据库里面);如果该用户之前已经用微博登陆过,那就到数据库中更新一下token
②前台用户用微博登录后我们会拿到用户的code,后台用code到微博里面换取token这样才能用token访问到用户基本信息;用户每登陆一次访问微博的token就会变一次,所以当用户下次用微博登陆时我们需要到数据库更新一下token
(1)session不能跨域问题
(2) 分布式下session共享问题
多台服务器都有会员服务,你在A服务器上把用户信息保存到内存上了,下次如果落在B服务器上,即使浏览器带着cookie来了,由于B服务器内存肯定没有存储用户信息,这也是问题。
session复制
用户登录后,A服务器得到session后,把session也复制到别的机器上,显然这种处理很不好
客户端存储
把session存储到浏览器上,肯定相当不安全
hash一致性
根据用户,到指定的机器上登录。但是远程调用还是不好解决
redis统一存储
最终的选择方案,把session放到redis中,这样每个微服务都可以获取到session
总说:
浏览器会在
auth.gulimall.com
里面登录成功,auth.gulimall.com
会将登陆成功的用户的从数据库查到的用户相关信息存到session里面,而且存session时不是存到自己的内存里面而是存到redis里面,然后auth.gulimall.com
给浏览器发cookie,而且发的cookie的作用域不能仅仅是auth.gulimall.com
而是要放大服务到.gulimall.com
,此时浏览器访问其它任何服务都会带上这个cookie。
如果你把redis里面的session情况,那就是把登陆过的用户信息清空,虽然前台的浏览器访问后台时携带了cookie信息,但是到redis里面查不到用户信息,所以你就得重新登陆。而且我们设置了redis里面的session默认30分钟过期,也就是30分钟后redis里面的用户信息就没有了
springsession只能把auth.gulimall.com作用域放大到gulimall.com,解决了同域名的共享session问题,但要是访问同样是尚硅谷的atguigu.com怎么办呢?这种不同的域名也想共享session该怎么做呢?
你在新浪微博里面注册登录了,同时就要保证在新浪体育、新浪新闻里面全都可以拿到session数据
两个域名不一样的服务端client1和client2,还有一个负责登录的ssoserver,还有一个浏览器,它们四个之间的故事
先说明一下这个路径的含义:
http://ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees
的含义就是让你访问http://ssoserver.com:8080/login.html
登陆页面,而redirect_url=http:I/client1.com:8081/employees
的含义是当你完成登陆后会重定向到http:I/client1.com:8081/employees
的位置
第1-11步的解析:只有登陆了才能查看员工信息。一开始浏览器访问client1.com的员工信息
http:I/client1.com:8081/employees
,client1会根据这个url有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees
,ssoserver.com会判断是否登陆过,没有登陆过就展示这个登陆页面,用户会输入账号密码进行登录,提交登陆请求http:/ssoserver.com:8080/doLogin?usermame,password,redirect_url
给ssoserver.com,那么ssoserver.com会保存用户状态到redis,同时ssoserver.com会命令重定向到http: /lclient1.com:8081/employees?token=dadadadsdeuieu
(浏览器访问路径),同时ssoserver.com会命令浏览器保存sso_token=dadadadsdeuieu
这样式的cookie。浏览器这次就可以访问员工信息了,他的访问路径是刚刚提到的http://lclient1.com:8081/employees?token=dadadadsdeuieu
比一开始访问员工信息的http:I/client1.com:8081/employees
多了token=dadadadsdeuieu
,这就回到第2步了,client1会根据有没有token参数判断是否登录,这次client1会觉得它登陆过了就可以访问员工信息了。
第12-19步解析:这次浏览器要访问客户端2的boss信息
http:I/client2.com:8081/boss
,client2会根据有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client2.com:8081/boss
,ssoserver.com会判断是否登陆过,由于浏览器有sso_token=dadadadsdeuieu
这样式的cookie,而且从redis能查到,说明它之前在client1或者client2登陆过,ssoserver.com会命令重定向到http:/lclient2.com:8082/boss?token=dadadadsdeuieu
,所以浏览器就会访问http://lclient2.com:8082/boss?token=dadadadsdeuieu
,这就回到了第2步,client2会根据有没有token参数判断是否登录,登陆过就响应页面。
所以说,以后浏览器无论访问client1还是client2,由于浏览器中保存了cookie,所以ssoserver.com就会判定它登陆过,所以以后都不用登陆。
client1的代码:
@GetMapping(value = "/employees")
public String employees(Model model,
HttpSession session,
@RequestParam(value = "redisKey", required = false) String redisKey) {
if (!StringUtils.isEmpty(redisKey)) {
//redisKey非空(也就是token非空),说明去过server端登录过了
// 拿着token去服务器,在服务端从redis中查出来用户的username
RestTemplate restTemplate=new RestTemplate();
ResponseEntity<Object> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?redisKey="+ redisKey, Object.class);
Object loginUser = forEntity.getBody();
session.setAttribute("loginUser", loginUser);//设置到自己的session中
}
//尝试从自己的session中获取"loginUser"
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null) {
//又没有token,session里又没有"loginUser",让它去登录页登录
return "redirect:" + "http://ssoserver.com:8080/login.html" + "?url=http://clientA.com/employees";
} else {
//自己的session里有"loginUser",即使没有token也说明登录过
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees"; //转到前端页面,前端会把数据拿出来展示
}
}
client2的代码:
代码一模一样,就是改一下访问路径@GetMapping(value = "/boss")
ssoserver的代码:
<body>
<form action="/doLogin" method="post">
<input type="hidden" name="url" th:value="${url}">
用户名:<input name="username" value="test"><br/>
密码:<input name="password" type="password" value="test">
<input type="submit" value="登录">
form>
body>
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ResponseBody
@GetMapping("/userInfo") //client1或client2会调用这个方法得到redis中的存储过的user信息
public Object userInfo(@RequestParam("redisKey") String redisKey){
// 拿着其他域名转发过来的token去redis里查
Object loginUser = stringRedisTemplate.opsForValue().get(redisKey);
return loginUser;
}
@GetMapping("/login.html") // 子系统都来这
public String loginPage(@RequestParam("url") String url,
Model model,
@CookieValue(value = "redisKey", required = false) String redisKey) {
//这是从浏览器中拿到的cookie,非空代表就登录过了
if (!StringUtils.isEmpty(redisKey)) {
//非空代表就登录过了
return "redirect:" + url + "?redisKey=" + redisKey;
}
model.addAttribute("url", url);
//没登录过才去登录页
return "login";
}
@PostMapping("/doLogin") //在前端输入用户名和密码后就会来到这里,进行server端统一认证
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletResponse response,
@RequestParam(value="url",required = false) String url){
//确认用户后,生成cookie,浏览器中存储,redis中也存储
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//非空就简单认为登录正确
String redisKey = UUID.randomUUID().toString().replace("-", "");//用uuid代替token
Cookie cookie = new Cookie("redisKey", redisKey);
response.addCookie(cookie);//浏览器中存储cookie
stringRedisTemplate.opsForValue().set(redisKey, username+password+"...", 30, TimeUnit.MINUTES);//redis中存储
return "redirect:" + url + "?redisKey=" + redisKey;//重定向时候带着token
}
// 登录失败,再次登录
return "login";
}
}
演示
代码用的网友的,截屏用到老师的,网友喜欢自己起名字,把token改为redisKey什么的,不要计较细节上的不同
总说:
本节内容就是说明了用户购物车里的信息应该使用哪个数据库存储(MySQL还是Redis?),以及使用了Redis后是用List存储这些信息呢还是使用Hash存储这些信息?以及购物车VO、购物项VO的编写
在(3)VO编写中为什么不用@Data而是自己写getter和setter方法?因为在购物项VO中计算“总价=单价x数量”这是需要手动计算的,使用@Data只会覆盖了自定义的setter方法;而且在购物车VO中应该是获取总价、获取商品数量,所以有的属性不应该有setter方法。
总说:
在购物车的所有Controller执行之前,我们先执行一个拦截器。在拦截器里判断用户是否登录,从session中获取不到用户信息就说明他没有登录,没有登录的话就从浏览器中获取一下user-key,如果浏览器中没有user-key那就说明用户是第一次
没有登录
的状态下进入京东,我们就得创建一个cookie名字叫做user-key,而且设置cookie的作用域、过期时间,假如明天他来了,我们能从浏览器中获取到该用户的user-key。
一个用户进来我们执行的 “ 拦截器—Controller—Service—Dao ” 这一套流程让同一个线程执行,这就使用了ThreadLocal技术,ThreadLocal是同一个线程共享数据,这个线程里面的数据会共享,使用过程就是:
ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();//创建一个threadLocal
threadLocal.set(userInfoTo);//把要共享的数据设置进去
....
UserInfoTo userInfoTo = threadLocal.get();//后期就可以获取到这个共享的数据
拦截器写了,Controller还有service什么的都没有写,在此之前,我们先打通整个页面(首页可以进入商品页,从商品页添加商品到购物车,然后点击购物车就可以进入购物车页),本节内容是前台代码。
总说:
因为是拦截器先执行的,所以先得到拦截器ThreadLocal的返回结果
UserInfoTo userInfoTo = threadLocal.get()
,如果userInfoTo.getUserId()
不为空表示账号用户,反之为临时用户 ,然后决定用临时购物车还是用户购物车。将用户购物车信息存到redis中,redis中肯定需要键值对,账号用户的购物车的redis中的key是gulimall:cart:1
(1是用户id,表示1号用户的购物车);临时用户的redis中的key是gulimall:cart:uuid
其中uuid就是我们拦截器里存下的user-key。redisTemplate.boundHashOps(cartKey)是说以后所有对redis的增删改查都是针对redia中key为cartKey的增删改查。上面的一套逻辑被封装到getCartOps()
方法里面了。
添加新商品到购物车,第一步先看redis里面能不能查到skuid,查不到说明购物车里面之前没有添加过此商品,那就需要远程查询此商品的一系列信息;能查到说明购物车有此商品,将数据取出修改数量即可。
//原文中的博客少了对getCartItem(Long skuId)方法的解释,你也许会问哪里少了getCartItem()?这还用问吗,你一个Ctrl+F搜一下不久知道了
@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;
}
测试
总说:
若用户未登录,则直接使用user-key获取购物车数据;否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
csdn上的笔记只是为了清楚地记录了老师这节课操作了什么,而具体代码我的建议就是尽量看idea上的代码,别看csdn上的代码,
//原文中的博客少了对getCartItems(String cartKey) 方法的解释
* 获取购物车里面的数据
private List<CartItemVo> getCartItems(String cartKey) {
//获取购物车里面的所有商品
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
String str = (String) obj;
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
//原文中的博客少了对clearCart()方法的解释
@Override
public void clearCart(String cartKey) {
redisTemplate.delete(cartKey);
}
测试