电商项目——全文检索-ElasticSearch——第一章——中篇
电商项目——商城业务-商品上架——第二章——中篇
电商项目——商城业务-首页——第三章——中篇
电商项目——性能压测——第四章——中篇
电商项目——缓存——第五章——中篇
电商项目——商城业务-检索服务——第六章——中篇
电商项目——商城业务-异步——第七章——中篇
电商项目——商品详情——第八章——中篇
电商项目——认证服务——第九章——中篇
电商项目——购物车——第十章——中篇
电商项目——消息队列——第十一章——中篇
电商项目——订单服务——第十二章——中篇
电商项目——分布式事务——第十三章——中篇
文章目录
1:环境搭建
前面我们完成了商品详情页功能,在做加入购物车,结账的时候,我们必须要完成登录界面环境搭建,我们要完成登录功能,我们不可以直接就去mall-member中输入账号密码就算是登录了,我们必须还要加入社交登录和单点登录
第一步:创建微服务,引入对应的依赖,和配置nacos
第二步:配置hosts和在nginx中实现动静分离
第三步:配置网关配置和启动mall-auth-server进行测试
- id: mall_host_route
uri: lb://mall-auth-server
predicates:
- Host=auth.mall.com
mall-auth-server
LoginController
@Controller
public class LoginController {
@GetMapping("login.html")
public String login(){
System.out.println("登录成功。。。。。。");
return "login";
}
}
2:好玩的验证码服务倒计时
实现的功能:点击发送验证码会有一个六十秒的倒计时
操作步骤:
mall-auth-searver
reg.html
<a id="sendCode">发送验证码</a>
$(function () {
$("#sendCode").click(function () {
//1:给指定手机号发送验证码
//2:倒计时(查阅w3school
//1000ms等于1s
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--;
}
在我们未来可呢会有多种如下的请求,就是发送一个请求直接跳转到另一个页面。
我们就可以使用springmvc,viewcontroller将请求和页面映射过来
mall-auth-server
LoginController
@Controller
public class LoginController {
@GetMapping("login.html")
public String login(){
System.out.println("登录成功。。。。。。");
return "login";
}
@RequestMapping("/reg.html")
public String reg(){
System.out.println("注册成功。。。。。。");
return "reg";
}
}
mall-auth-server
MallWebConfig
@Configuration
public class MallWebConfig implements WebMvcConfigurer{
/**
* 视图映射器
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//Set the view name to return.:setViewName
//Map a view controller to the given URL path (or pattern) in order to render
// * a response with a pre-configured status code and view.:addViewController
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
3:整合短信验证码
4:验证码防刷校验
实现的功能:前面我们调通了验证码倒计时,现在我们就应该完成点击发送验证码会自动给手机发送验证码请求,然后登陆
操作:
第一步:我们现在就要来编写一个控制器,
mall-third-party
SmsSendController
@RestController
@RequestMapping("/sms")
public class SmsSendController {
@Autowired
SmsComponent smsComponent;
// * 提供给别的服务进行调用
@GetMapping("/sendcode")
public R sendCode(@RequestParam ("phone")String phone,@RequestParam("code") String code){
smsComponent.sendSmsCode(phone,code);
return R.ok();
}
}
- 实际上我们发送验证码是要让后台随机生成一个验证码,然后提交以后是要让后台进行校验的,需要注意的是,第三方服务属于后台集群功能(业务功能实现),我们不应该由第三方服务来给后台发生请求,而应该由页面给我们的各自服务发送请求,需要验证码了再经过我们的各自的服务给我们后台的第三方服务发送请求;
- 所以我们上面写的sendCode方法是给别的服务来调用的,不是提供给页面直接发请求的
- 我们以后就要让认证服务去调用第三方服务的验证码功能接口
第二步:我们来编写mall-auth-server,并且开启OpneFeign,可以远程调用mall-third-party的sendSmsCode方法
mall-auth-server
controller
@Controller
public class LoginController {
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
//模拟假的短信验证码
String code = UUID.randomUUID().toString().substring(0, 5);
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
}
mall-auth-server
ThirdPartFeignService
@FeignClient("mall-third-party")
public interface ThirdPartFeignService {
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone")String phone, @RequestParam("code") String code);
}
第三步:编写mall-auth-server的reg.html
$(function () {
$("#sendCode").click(function () {
//1:给指定手机号发送验证码
//2:倒计时(查阅w3school
//1000ms等于1s
if ($(this).hasClass("disabled")) {
//正在倒计时
} else {
//1:给指定手机号发送验证码
$.get("/sms/sendcode?phone="+$("#phoneNumber").val());
timeoutChangeStyle()
}
});
});
测试是否手机号可以被成功发送(后端还没有完善,但是数据是被成功发送)
现在我们还有两个问题:就是我们的页面中暴露了我们的接口请求(我们要防止接口防刷)还有一个就是,我们的手机发送完验证码以后,还要再等下一个60秒才可以重新发送(验证码的再次校验)
-
接口防刷问题的解决
-
验证码的再次校验问题解决:使用(redis)解决
第一步:导入依赖并且在注册中心配置上redis配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: 192.168.56.10
port: 6379
第二步:我们可以编写一个constant在mall-common中,方便以后我们存储redis键值队
mall-common
public class AuthServerConstant {
public static final String SMS_CODE_CACHE_PREFIX="sms:code";
}
第三步:改造controller和reg.html
mall-auth-server
LoginController
@Controller
public class LoginController {
@Autowired
ThirdPartFeignService thirdPartFeignService;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 发送一个请求直接跳转到另一个页面
* 我们就可以使用springmvc,viewcontroller将请求和页面映射过来
*/
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(s)){
long l = Long.parseLong(s.split("_")[1]);
if (System.currentTimeMillis()-1<60000){
return R.error(BigCodeEnume.VALID_EXCEPTION.getCode(),BigCodeEnume.VALID_EXCEPTION.getMsg());
//60s内不可以在发送
}
}
// todo 1:接口防刷问题的解决
//2:验证码的再次校验问题解决(使用redis解决)存 key-phone value-code sms:code:1999999->1999
//模拟假的短信验证码
String code = UUID.randomUUID().toString().substring(0, 5);
//redis缓存验证码,防止同一个phone在60秒内再次发送验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10, TimeUnit.MINUTES);
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
}
reg.html
$(function () {
$("#sendCode").click(function () {
//1:给指定手机号发送验证码
//2:倒计时(查阅w3school
//1000ms等于1s
if ($(this).hasClass("disabled")) {
//正在倒计时
} else {
//1:给指定手机号发送验证码
$.get("/sms/sendcode?phone="+$("#phoneNumber").val(),function (data) {
//后端发给前端的数据
if(data.code!=0){
alert(data.msg);
}
});
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--;
}
5:一步一坑的注册页环境
mall-auth-server
我们继续来编写注册功能,我们点击注册(reg.html),就要把信息保存在会员服务的数据库中
第一步:封装请求数据的vo(根据界面的参数,我们可以封装如下vo,并且使用JSR30数据校验注解),并且在我们编写好的controller中进行数据校验
UserRegistVo
@Data
public class UserRegistVo {
/**
* 使用jsr30进行数据校验
*/
@NotEmpty(message = "用户名必须提交")
@Length(min=6,max = 18,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;
}
LoginController(代码不完整)
/**
* todo return "redirect:http://auth.mall.com/reg.html"重定向还可以返回给前端页面的原理是什么呢?原理是什么呢?在控制台的application里面(重定向携带数据模拟session),只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉,
*todo 解决分布式情况下的session问题
* RedirectAttributes模拟重定向携带数据的
*/
@PostMapping("/regist")
public String regist(@Valid @RequestBody UserRegistVo userRegistVo, BindingResult result, RedirectAttributes redirectAttributes){
if (result.hasErrors()){
Map<String,String> errors= result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,FieldError::getDefaultMessage));
System.out.println(errors);
// model.addAttribute("errors",errors);
redirectAttributes.addFlashAttribute("errors",errors);
//校验出错,转发到注册页
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
// return "reg";//使用转发方式,会重复提交
//forword/reg.html视图解析器不拼串,来到MallWebConfig里面映射(相当于多绕了一圈)
// return "reg"相当于返回视图逻辑地址,然后拼串
//request method 'post' 不支持
//用户注册-》(post)-》转发forword/reg.html(路径映射默认是get方式访问的)MallWebConfig
}
//真正注册,调用远程服务进行注册
return "redirect:/login.html";
}
第二步:
mall-auth-server
编写reg.html
<form action="/regist" method="post" class="one">
<div class="register-box">
<label class="username_label">用 户 名
<input name="userName" maxlength="20" type="text" placeholder="您的用户名和登录名">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors,'userName')?errors.userName:''):''}">
</div>
</div>
<div class="register-box">
<label class="other_label">设 置 密 码
<input name="password" maxlength="20" type="password" placeholder="建议至少使用两种字符组合">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors,'password')?errors.password:''):''}">
</div>
</div>
<div class="register-box">
<label class="other_label">确 认 密 码
<input maxlength="20" type="password" placeholder="请再次输入密码">
</label>
<div class="tips" >
</div>
</div>
<div class="register-box">
<label class="other_label">
<span>中国 0086∨</span>
<input name="phone" class="phone" id="phoneNumber" maxlength="20" type="text" placeholder="建议使用常用手机">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors,'phone')?errors.phone:''):''}">
</div>
</div>
<div class="register-box">
<label class="other_label">验 证 码
<input name="code" maxlength="20" type="text" placeholder="请输入验证码" class="caa">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors,'code')?errors.code:''):''}">
</div>
<!--id="code"-->
<a id="sendCode">发送验证码</a>
</div>
<div class="arguement">
<input type="checkbox" id="xieyi"> 阅读并同意
<a href="/static/reg/#">《谷粒商城用户注册协议》</a>
<a href="/static/reg/#">《隐私政策》</a>
<div class="tips">
</div>
<br/>
<div class="submit_btn">
<button type="submit" id="submit_btn">立 即 注 册</button>
</div>
</div>
</form>
小知识:return "redirect:http://auth.mall.com/reg.html"重定向还可以返回给前端页面的原理是什么呢?原理是什么呢?在控制台的application里面(重定向携带数据模拟session),只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉,
6:异常机制
接下来完善注册功能
mall-auth-server
LoginController
/**
* todo return "redirect:http://auth.mall.com/reg.html"重定向还可以返回给前端页面的原理是什么呢?原理是什么呢?在控制台的application里面(重定向携带数据模拟session),只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉,
*todo 解决分布式情况下的session问题
* RedirectAttributes模拟重定向携带数据的
*/
@PostMapping("/regist")
public String regist(@Valid @RequestBody UserRegistVo userRegistVo, BindingResult result, RedirectAttributes redirectAttributes){
if (result.hasErrors()){
Map<String,String> errors= result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,FieldError::getDefaultMessage));
System.out.println(errors);
// model.addAttribute("errors",errors);
redirectAttributes.addFlashAttribute("errors",errors);
//校验出错,转发到注册页
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
// return "reg";//使用转发方式,会重复提交
//forword/reg.html视图解析器不拼串,来到MallWebConfig里面映射(相当于多绕了一圈)
// return "reg"相当于返回视图逻辑地址,然后拼串
//request method 'post' 不支持
//用户注册-》(post)-》转发forword/reg.html(路径映射默认是get方式访问的)MallWebConfig
}
//真正注册,调用远程服务进行注册
//1:进行校验验证码
String code = userRegistVo.getCode();
String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + userRegistVo.getPhone());
if (!StringUtils.isEmpty(s)){
if (code.equals(s.split("_")[0])){
//验证码通过以后 真正注册,调用远程服务
//删除验证码;;令牌机制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + userRegistVo.getPhone());
}else {
Map<String,String> errors=new HashMap<>();
errors.put("errors","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
}
}else {
Map<String,String> errors=new HashMap<>();
errors.put("errors","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
}
return "redirect:/login.html";
}
完成如上controller之后,我们还要在它的核心模块里面调用mall-member远程服务,来吧前端的数据传输到mall-member中判断是否可以进行登录
注意:
1: 我们要检查用户名和手机号是否唯一;为了让controller可以感知异常,我们使用异常机制(只要有异常就中断,没异常就就往下走)
2:密码要进行加密存储(下小节完成)
mall-member
MemberRegistVo
@Data
public class MemberRegistVo {
private String userName;
private String password;
private String phone;
private String code;
}
MemberController
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try {
memberService.regist(vo);
}catch (Exception e){
}
return R.ok();
}
MemberServiceImpl
@Override
public void regist(MemberRegistVo vo) {
MemberEntity memberEntity=new MemberEntity();
MemberDao baseMapper = this.baseMapper;
//设置默认等级
MemberLevelEntity levelEntity= memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(levelEntity.getId());
//检查用户名和手机号是否唯一;为了让controller可以感知异常,我们使用异常机制(只要有异常就中断,没异常就就往下走
checkUsernameUnique(vo.getUserName());
checkPhoneUnique(vo.getPhone());
memberEntity.setUsername(vo.getUserName());
memberEntity.setMobile(vo.getPhone());
//密码要进行加密存储
memberEntity.setPassword(vo.getPassword());
}
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException{
MemberDao baseMapper = this.baseMapper;
Integer mobile = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile>0) {
throw new PhoneExistException();
}
}
@Override
public void checkUsernameUnique(String userName) throws UsernameExistException {
MemberDao baseMapper = this.baseMapper;
Integer username = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
if (username>0){
throw new UsernameExistException();
}
}
7:MD5&盐值&BCrypt
- 接下来我们说下密码的加密存储;如果我们的数据库把密码存储成明文(容易造成被黑客攻击),所以我们必须存密文,那么要存储一个什么样的密文???我们有可逆加密(通过加密算法可以从密文推出明文)和不可逆加密(即使知道加密算法也不可以从密文推出明文),所以我们应该把密码字段存储为不可逆的(不可以让别人通过加密算法推出明文)那么我们要怎么存储呢??????我们要使用MD5&MD5盐值加密算法来进行存储(不可逆加密算法),严格意义上MD5是信息摘要算法,它可以根据一个大段文本的值得到一个固定的MD5长度
测试:
@Test
void contextLoads() {
//e10adc3949ba59abbe56e057f20f883e
String s = DigestUtils.md5Hex("123456");
//如果直接用这个MD5加密的值保存到数据库中,还是不安全,网上有很多破解这种加密的
//不是说MD5不可逆嘛》???破解的人就是利用到了MD5的特性抗修改性:(彩虹表暴力破解)
System.out.println(s);
}
- 如果直接用这个MD5加密的值保存到数据库中,还是不安全,网上有很多破解这种加密的,所以我们就要使用盐值加密(随机值),就是我们保存12345的时候,使用盐值加密,我们会在保存的时候在加一个随机值
不是说MD5不可逆嘛》???破解的人就是利用到了MD5的特性抗修改性:(彩虹表暴力破解)
//md5盐值加密
//$1$sssss$vfPken4eKalHjtlQ1OTxM.
//$1$sssss$vfPken4eKalHjtlQ1OTxM.
//验证:123456进行盐值加密(去数据库查询)
String s1 = Md5Crypt.md5Crypt("123456".getBytes(),"$1$sssss");
System.out.println(s1);
- 我们spring给我们整合了一个很好用的编码器
相当于我们有一千多个用户,就算是密码都一样,我们用BCryptPasswordEncoder存的值都不一样,原因就是它在encode里面早就融合盐值是多少(这样我们也不用在数据库里面存储额外的字段,注册的时候存结果,登录的时候就用BCryptPasswordEncoder进行匹配看密码是否正确,)
测试
//$2a$10$LHGlYU2GanGcyI4xUp72Q.DXgIWxWW0l0ZnNwxQU8R3vvAibQ41By
//$2a$10$6g79dDYpXWTB/AWF9ODdSuN93ljfFnREmk1dcQv9D.VfYCmja.hf2
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
//true==>$2a$10$qMjaSwiVLHzQcgalmwwzreFovheOBgOKjYvVbtMJANcxPhD6tkbdC
boolean matches = passwordEncoder.matches("123456", "$2a$10$LHGlYU2GanGcyI4xUp72Q.DXgIWxWW0l0ZnNwxQU8R3vvAibQ41By");
System.out.println(matches+"==>"+encode);
mall-member
MemberServiceImpl
完整的代码
@Override
public void regist(MemberRegistVo vo) {
MemberEntity memberEntity=new MemberEntity();
MemberDao baseMapper = this.baseMapper;
//设置默认等级
MemberLevelEntity levelEntity= memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(levelEntity.getId());
//检查用户名和手机号是否唯一;为了让controller可以感知异常,我们使用异常机制(只要有异常就中断,没异常就就往下走
checkUsernameUnique(vo.getUserName());
checkPhoneUnique(vo.getPhone());
memberEntity.setUsername(vo.getUserName());
memberEntity.setMobile(vo.getPhone());
//密码要进行加密存储
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
memberEntity.setPassword(encode);
//其他的默认信息
//保存用户
baseMapper.insert(memberEntity);
}
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException{
MemberDao baseMapper = this.baseMapper;
Integer mobile = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile>0) {
throw new PhoneExistException();
}
}
@Override
public void checkUsernameUnique(String userName) throws UsernameExistException {
MemberDao baseMapper = this.baseMapper;
Integer username = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
if (username>0){
throw new UsernameExistException();
}
}
8:注册完成
完善mall-member控制层的异常机制调用
mall-member
MemberController
@RestController
@RequestMapping("member/member")
public class MemberController {
@Autowired
private MemberService memberService;
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try {
memberService.regist(vo);
}catch (UsernameExistException e){
System.out.println(e);
return R.error(BigCodeEnume.USER_EXIST_EXCEPTION.getCode(),BigCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}catch (PhoneExistException e){
System.out.println(e);
return R.error(BigCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BigCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
我们要在mall-auth-server微服务中进行远程接口调用(OpenFeign)mall-member微服务
mall-auth-server
MemberFeignService
@FeignClient("mall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
public R regist(@RequestBody UserRegistVo vo);
}
mall-auth-server
LoginController
/**
* todo return "redirect:http://auth.mall.com/reg.html"重定向还可以返回给前端页面的原理是什么呢?原理是什么呢?在控制台的application里面(重定向携带数据模拟session),只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉,
*todo 解决分布式情况下的session问题
* RedirectAttributes模拟重定向携带数据的
*/
@PostMapping("/regist")
public String regist(@Valid @RequestBody UserRegistVo userRegistVo, BindingResult result, RedirectAttributes redirectAttributes){
if (result.hasErrors()){
Map<String,String> errors= result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,FieldError::getDefaultMessage));
System.out.println(errors);
// model.addAttribute("errors",errors);
redirectAttributes.addFlashAttribute("errors",errors);
//校验出错,转发到注册页
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
// return "reg";//使用转发方式,会重复提交
//forword/reg.html视图解析器不拼串,来到MallWebConfig里面映射(相当于多绕了一圈)
// return "reg"相当于返回视图逻辑地址,然后拼串
//request method 'post' 不支持
//用户注册-》(post)-》转发forword/reg.html(路径映射默认是get方式访问的)MallWebConfig
}
//真正注册,调用远程服务进行注册
//1:进行校验验证码
String code = userRegistVo.getCode();
String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + userRegistVo.getPhone());
if (!StringUtils.isEmpty(s)){
if (code.equals(s.split("_")[0])){
//验证码通过以后 真正注册,调用远程服务
//删除验证码;;令牌机制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + userRegistVo.getPhone());
R regist = memberFeignService.regist(userRegistVo);
if (regist.getCode()==0){
//成功
return "redirect:http://auth.mall.com/login.html";
}else {
Map<String,String> errors=new HashMap<>();
errors.put("msg",regist.getData(new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.mall.com/reg.html";
}
}else {
Map<String,String> errors=new HashMap<>();
errors.put("errors","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
}
}else {
Map<String,String> errors=new HashMap<>();
errors.put("errors","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.mall.com/reg.html";//我们使用重定向的方式(重定向不可以直接到reg,只可以到我们的路径映射地址(转发默认是在请求域中的model获取得到,而重定向获取不到,所以我们不可以使用model,进行修改
}
}
9:账号密码登录
完善登录功能
mall-auth-server
UserLoginVo
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
LoginController
//我们从表单提交的不是json,而是kv所以,我们不可以使用@requestbody
@PostMapping("/login")
public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){
//远程登录
R login = memberFeignService.login(vo);
if (login.getCode()==0){
//成功
return "redirect:http://zlj.mall.com";
}else {
HashMap<String,String> errors=new HashMap<>();
errors.put("errors",login.getData(new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
//失败!!!
return "redirect:http://zlj.mall.com/login.html";
}
login.html
<form action="/login" method="post">
<div style="color: red" th:text="${errors!=null?(#maps.containsKey(errors,'errors')?errors.errors:''):''}">
</div>
<ul>
<li class="top_1">
<img src="/static/login/JD_img/user_03.png" class="err_img1" />
<input type="text" name="loginacct" placeholder=" 邮箱/用户名/已验证手机" class="user" />
</li>
<li>
<img src="/static/login/JD_img/user_06.png" class="err_img2" />
<input type="password" name="password" placeholder=" 密码" class="password" />
</li>
<li class="bri">
<a href="/static/login/">忘记密码</a>
</li>
<li class="ent"><button type="submit" class="btn2"><a >登 录</a></button></li>
</ul>
</form>
MemberFeignService
@FeignClient("mall-member")
public interface MemberFeignService {
@PostMapping("/member/member/login")
public R login(UserLoginVo memberLoginVo);
}
mall-member
MemberController
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo memberLoginVo){
MemberEntity memberEntity= memberService.login(memberLoginVo);
if (memberEntity!=null){
return R.ok();
}else {
return R.error(BigCodeEnume.USERORPASSWORD_EXIST_EXCEPTION.getCode(),BigCodeEnume.USERORPASSWORD_EXIST_EXCEPTION.getMsg());
}
}
MemberServiceImpl
@Override
public MemberEntity login(MemberLoginVo memberLoginVo) {
String loginacct = memberLoginVo.getLoginacct();
String password = memberLoginVo.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//去数据库查询
MemberDao baseMapper = this.baseMapper;
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));
if (memberEntity==null){
//登录失败
return null;
}else {
//判断用户输入密码和数据库查询的密码是否一样
String password1 = memberEntity.getPassword();
boolean matches = passwordEncoder.matches(password, password1);
if (matches){
return memberEntity;
}else {
return null;
}
}
}
测试:
问题:
10:OAuth2.0简介
csdn想要找qq进行登录,所以csdn就是client,它想要找qq服务器要账号进行登录,所以client是第三方应用(qq,微博,自己做的网站)
11: weibo登录测试
准备工作:网站想要使用weibo进行登录,就必须要去微博进行申请认证权限,现在我们就要进微博的开发平台,去给网站做一个微博的开放权限
如上的zljmall应用就可以使用weibo的社交登录功能
授权回调页:微博登录成功以后要跳回那个页面
OAuth2.0概述
参考上面文档的Web网站的授权来进行操作
第一步:引导需要授权的用户到如下地址:
mall-auth-server
编写login.html
<li>
<a href="https://api.weibo.com/oauth2/authorize?client_id=258385928&response_type=code&redirect_uri=http://mall.com/sucess
">
<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png" />
</a>
</li>
进行测试:点击微博,回调如下效果
第二步: 如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
继续如上第一步测试:点击授权,我跳转到如下地址
http://mall.com/sucess?code=068bdd2142f67b99d531696306d5641f
第三步: 换取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
使用postman进行如下操作
{
"access_token": "2.00ZXZCqG0yyJUR39ec702bd5Indy3D",
"remind_in": "157679999",
"expires_in": 157679999,
"uid": "6265779741",
"isRealName": "true"
}
access_token的作用:我们就可以查看微博应用控制台中的接口管理的已有权限
第四步:使用获得的Access Token调用API
总结:
12:社交登录回调
按照上面步骤
mall-auth-server
reg.html
<li>
<a href="https://api.weibo.com/oauth2/authorize?client_id=258385928&response_type=code&redirect_uri=http://mall.com/oauth2.0/weibo/sucess
">
<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png" />
</a>
</li>
第二步编写一个controller,并且导入HttpUtils类
第六步,如上图,跳回了一个地址
http://mall.com/oauth2.0/weibo/sucess?code=618dd991afe06e0dd397058383f8c050
第七步:处理登录成功的回调
OAuth2Controller
@Controller
public class OAuth2Controller {
//http://mall.com/oauth2.0/weibo/sucess?code=618dd991afe06e0dd397058383f8c050
@GetMapping("/oauth2.0/weibo/sucess")
public String weibo(@RequestParam("code") String code) throws Exception {
//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
//1:根据code换取accessToken
Map<String,String> map=new HashMap<>();
map.put("client_id","258385928");
map.put("client_secret","7950ff21002cdb2bc5992d8d8fd11a77");
map.put("grant_type","authorization_code");
map.put("redirect_uri","http://mall.com/oauth2.0/weibo/sucess");
map.put("code",code);
HttpResponse response = HttpUtils.doPost("api.weibo.com", "/oauth2/access_token", "post", null, null, map);
//2:登录成功就跳回首页
return "redirect:http://zlj.mall.com";
}
}
13:社交登录完成
@Data
public class SocicalUser {
private String access_token;
private String remind_in;
private long expires_in;
private String uid;
private String isRealName;
}
对应的实体类也要增加三个字段
MemberEntity
//...
//社交登录用户的唯一id
private String socialUid;
//访问令牌
private String accessToken;
//访问令牌过期时间
private Long expiresIn;
MemberController
@PostMapping("/oauth2/login")
public R oauth2login(@RequestBody SocicalUser socicalUser) throws Exception {
MemberEntity memberEntity= memberService.loginOauth2(socicalUser);
if (memberEntity!=null){
return R.ok().setData(memberEntity);
}else {
return R.error(BigCodeEnume.USERORPASSWORD_EXIST_EXCEPTION.getCode(),BigCodeEnume.USERORPASSWORD_EXIST_EXCEPTION.getMsg());
}
}
MemberServiceImpl
@Override
public MemberEntity loginOauth2(SocicalUser socicalUser) throws Exception {
//登录和注册合并逻辑
String uid = socicalUser.getUid();
//1:判断当前社交用户是否已经登录过系统
MemberDao memberDao = this.baseMapper;
//如果统计出来,当前的uid在数据库中有对应的uid,说明我们注册过
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) {
//这个用户已经注册
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socicalUser.getAccess_token());
update.setExpiresIn(socicalUser.getExpires_in());
update.setSocialUid(socicalUser.getUid());
memberDao.updateById(update);
memberEntity.setAccessToken(socicalUser.getAccess_token());
memberEntity.setExpiresIn(socicalUser.getExpires_in());
memberEntity.setSocialUid(socicalUser.getUid());
return memberEntity;
} else {
//没有查到当前社交用户对应的记录,我们就需要注册一个
MemberEntity regist = new MemberEntity();
//https://api.weibo.com/2/users/show.json?access_token=2.00ZXZCqG0yyJUR39ec702bd5Indy3D
//&uid=
//try catch里面就算报错了也没事,关键是最后4行代码
try {
Map<String, String> query = new HashMap<>();
query.put("access_token", socicalUser.getAccess_token());
query.put("uid", socicalUser.getUid());
HttpResponse response = HttpUtils.doGet("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");
//..
regist.setNickname(name);
regist.setGender("m".equals(gender) ? 1 : 0);
}
}catch(Exception e){
}
//这些代码一定要执行
regist.setSocialUid(socicalUser.getUid());
regist.setAccessToken(socicalUser.getAccess_token());
regist.setExpiresIn(socicalUser.getExpires_in());
memberDao.insert(regist);
return regist;
}
}
14:社交登录测试成功
mall-auth-server
MemberFeignService
@FeignClient("mall-member")
public interface MemberFeignService {
//..
@PostMapping("/member/member/oauth2/login")
public R oauth2login(@RequestBody SocicalUser socicalUser);
}
OAuth2Controller
@GetMapping("/oauth2.0/weibo/sucess")
public String weibo(@RequestParam("code") String code, RedirectAttributes redirectAttributes) throws Exception {
//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
//1:根据code换取accessToken
Map<String,String> map=new HashMap<>();
map.put("client_id","258385928");
map.put("client_secret","7950ff21002cdb2bc5992d8d8fd11a77");
map.put("grant_type","authorization_code");
map.put("redirect_uri","http://mall.com/oauth2.0/weibo/sucess");
map.put("code",code);
HttpResponse response = HttpUtils.doPost("api.weibo.com", "/oauth2/access_token", "post", null, null, map);
//2:处理
if (response.getStatusLine().getStatusCode()==200){
/**获取到的响应(access_token)通过得到code
* {
"access_token": "2.00ZXZCqG0yyJUR39ec702bd5Indy3D",
"remind_in": "157679999",
"expires_in": 157679999,
"uid": "6265779741",
"isRealName": "true"
}
*/
//获取到accessToken
String json= EntityUtils.toString(response.getEntity());
//把json字符串转化成SocicalUser
SocicalUser socicalUser = JSON.parseObject(json, SocicalUser.class);
//知道当前是哪个社交用户
//1)当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)
//登录或者注册这个社交用户
R login = memberFeignService.oauth2login(socicalUser);
if (login.getCode()==0){
MemberRespVo data = login.getData(new TypeReference<MemberRespVo>() {
});
log.info("登录成功:用户{}"+data.toString());
//成功
//成功
return "redirect:http://zlj.mall.com";
}else {
HashMap<String,String> errors=new HashMap<>();
errors.put("errors",login.getData(new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
//失败!!!
return "redirect:http://auth.mall.com/login.html";
}
}else {
return "redirect:http://auth.mall.com/login.html";
}
// //3登录成功就跳回首页
//
// return "redirect:http://zlj.mall.com";
}
总结:
授权机制
15:分布式session不共享不同步问题
前面我们集成了社交登录功能
进行测试
- 我们先来看一下session的原理,服务器为了保存浏览器进行此次交互的数据,我们服务器创建了session,session是这样工作的,首先浏览器访问服务器进行了第一次登录操作,如果登录成功,我们把数据保存在服务器的session中(session就是服务器里面的内存数据(相当于map)),浏览器器有很多(张三,李四。。。)都要访问我们的商城进行登录(只有这一个服务器),所以我们的这个服务器为了保存张三,李四这些人的状态,服务器会为每一个用户创建一个session对象(相当于map)
- 假设第一个人,进行登录,登录成功了,我们的浏览器就会保存这一个session状态,那我们的浏览器怎么知道下一次它是登录了那个用户???我们创建好session以后,会命令浏览器保存一个cookie,类似于,我们去银行里面存钱,银行为了标识我们是哪一个人,银行为我们办了一张银行卡,以后我们存的钱都是对应于这一张银行卡下面的,我们下一次只要带着这张银行卡去办理业务就可以了;如上是一个常规的流程
如上测试中,我们看到了cookie的作用域,说明了我们session的第一个问题就是:不可以跨不同域名共享,我们在auth.mall.com中进行登录,服务器只会向auth.mall.com中保存一个cookie,相当于这是工商银行的卡,可是登录成功以后跳转到zlj.mall.com中,这个时候它带上的cookie就相当于是兴业银行的卡,去访问服务器,当然是访问不到数据的(你想用兴业银行的卡,去工商银行里面取钱是不可以的)
session的第二个问题就是session不同步问题,如上左图,浏览器第一次进行登录负载均衡到第一个会员服务,第一个内存服务在它自己的内存空间里存了一个session,但是现在是分布式集群环境,下一次请求再次进来落到了第二个会员服务,我们第一次带的cookie在第二个会员服务中是不可用的(第二个服务器里的数据是读取不到第一个服务器里面的内存数据的),这就是session不同步问题;
所以我们必须要解决session的上面两个问题才可以
16:分布式session解决方案原理
17:SpringSession整合
HttpSession with Redis
第一步:增加session依赖,放在mall-common中
<!--整合springsession完成session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
第二步:在添加了所需的依赖项之后,我们可以创建我们的Spring引导配置。由于一流的自动配置支持,设置Redis支持的Spring会话非常简单,只需向应用程序添加一个配置属性即可;mall-auth-server中进行如下配置
spring.session.store-type=redis # Session store type.
第三步:
spring:
redis:
host: 192.168.56.10
port: 6379
第四步:配置如下@EnableRedisHttpSession即可
@EnableFeignClients
@EnableRedisHttpSession //整合redis作为session存储
@EnableDiscoveryClient
@SpringBootApplication
public class MallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(MallAuthServerApplication.class, args);
}
}
在mall-product中也按照如上进行配置
核心代码如下
在mall-product
index.html
<li>
<a href="http://auth.mall.com/login.html">你好,请登录:[[${session.loginUser.nickname}]]</a>
</li>
mall-auth-server
OAuth2Controller
if (login.getCode()==0){
//MemberRespVo数据要实现序列化,存储到redis里面
MemberRespVo data = login.getData(new TypeReference<MemberRespVo>() {
});
//第一次使用session:命令浏览器保存卡号 JSESSIONID这个cookie
//以后浏览器访问那个网站就会带上这个网站的cookie
//子域之间 zlj.mall.com auth.mall.com order.mall.com
//发卡的时候(指定域名为父域名)即使是子域系统发的卡,也可以让父域直接使用
//todo 1:默认发的令牌:session=dsajkdjl:作用域:当前域(必须解决子域session共享问题)
//todo 2:使用json的序列化方式来序列化对象到redis中
session.setAttribute("loginUser",data);
log.info("登录成功:用户{}"+data.toString());
//成功
return "redirect:http://zlj.mall.com";
}
测试出现如下效果
18:自定义SpringSession完成子域session共享
实现如下的两个功能
todo 1:默认发的令牌:session=dsajkdjl:作用域:当前域(必须解决子域session共享问题)
todo 2:使用json的序列化方式来序列化对象到redis中
给mall-product和mall-auth-server中都复制一份,
MallSessionConfig
@Configuration
public class MallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("MALLSESSION");
serializer.setDomainName("mall.com");
return serializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
Spring Session官方文档
进行测试
19:SpringSession原理
* springsession的核心原理:
* 1)@EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
* sessionRepository==》RedisHttpSessionConfiguration给容器中添加了一个组件:RedisIndexedSessionRepository:==》redis操作session,session的增删改查封装类
*
* 2:RedisHttpSessionConfiguration 继承了SpringHttpSessionConfiguration这里面有一个
* SessionRepositoryFilter组件==》filiter:session存储过滤器:每个请求过来都必须经过filiter
* 1:创建的时候,就自动从容器中获取到了sessionRepository
* 2:SessionRepositoryFilter类里面调用了doFilterInternal方法==》原始的request,response都被这个方法包装成了SessionRepositoryRequestWrapper SessionRepositoryResponseWrapper
* 3:以后获取session。request.getSession
* 4:wrappedRequest.getSession()===>SessionRespository中获取到
*
* 装饰者模式
* 自动延期,redis中的数据也是有过期时间的
20:页面效果完成
使用账户登录,进行测试
http://zlj.mall.com/底下成功显示如下字体,我们使用了SpringSession,也成功的在redis中存储了session,并且也成功返回给前端一个cookie(自己定义的)
欢迎:zlj
21:单点登录简介
在更大的系统下,上面的SpringSession还是会出现问题,如下在多系统中出现的问题
- 第一个问题:如果访问旗下的任何一个产品都要在注册一个用户的话,非常麻烦
- 第二个问题:如果在旗下每次登陆一个系统都要重新登录,也很麻烦
我们最终希望达到的效果:我们登录了谷粒商城-电商系统,然后会传给认证中心,然后其他的系统都可以使用, - 那我们是否可以使用springsession解决问题呢??其实是不可以的,因为我们的session传给浏览器的cookie,我们放大它的作用域,只可以到一级域名.mall.com,不可以一直放大,如果访问的是谷粒学院,谷粒凑,我们是获取不到电商系统里的session(不可以访问的),所以springsession不可以解决我们的多系统(单点登录问题)
- 单点登录:一处登录处处使用
22:框架效果演示
https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search
下载到我们的电脑上
第一步:在对应的文件xxs-sso启动cmd执行mvn clean package -Dmaven.skip.test=true,然后在去对应的target里面启动各个jar包
java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8082
java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8081
java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar
第二步:测试
只要你登录了下面地址的任何一个,其他两个都不需要我们去登陆,这就是单点登录
http://localhost:8080/xxl-sso-server/
http://localhost:8082/xxl-sso-web-sample-springboot/
http://localhost:8081/xxl-sso-web-sample-springboot/
或者我只要点击log out 其他的也会退出,这就是单点登录
23:单点登录流程