1、环境搭建
1、创建gulimall-auth-server模块
2、导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-auth-server</artifactId><version>0.0.1-SNAPSHOT</version><name>gulimall-auth-server</name><description>认证中心(社交登录、OAuth2.0、单点登录)</description><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR8</spring-cloud.version></properties><dependencies><dependency><groupId>com.auguigu.gulimall</groupId><artifactId>gulimall-commom</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><snapshots><enabled>true</enabled></snapshots></repository><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url></repository></repositories></project>
3、添加application.properties配置
server.port=20000
spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
4、修改主启动类
package com.atguigu.gulimall.auth;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {public static void main(String[] args) {SpringApplication.run(GulimallAuthServerApplication.class, args);}}
5、修改C:\Windows\System32\drivers\etc\hosts的域名
#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
6、引入login.html和reg.html
7、在虚拟机的/mydata/nginx/html/static/下创建reg,并把静态资源放入reg文件夹下
在虚拟机的/mydata/nginx/html/static/下创建login,并把静态资源放入login文件夹下
8、重启nginx
docker restart nginx
9、修改login.html和reg.html的引用路径(Ctrl+R)
login.html
reg.html
10、在网关模块配置认证的路由
- id: gulimall_auth_routeuri: lb://gulimall-auth-serverpredicates:- Host=auth.gulimall.com
11、测试
先把login.html名字改为index.html,接着启动认证中心和网关服务
http://auth.gulimall1.com/
12、修改点击登录页的谷粒商城logo可以跳到首页
1)、先关闭缓存
#关闭thymeleaf的缓存
spring.thymeleaf.cache=false
2)、修改login.html链接地址
<!--顶部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>
3)、修改首页index.html登录注册的链接地址
<ul><li><a href="http://auth.gulimall.com/login.html">你好,请登录</a></li><li><a href="http://auth.gulimall.com/reg.html">免费注册</a></li><span>|</span><li><a href="/static/#">我的订单</a></li></ul>
再把认证中心的index.html改成login.html
添加“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:
@Controller
public class LoginController {@GetMapping("/login.html")public String loginPage(){return "login";}@GetMapping("/reg.html")public String regPage(){return "reg";}
}
点击测试
修改登录页面login.html的立即注册链接地址
<h5 class="rig"><img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_25.png" /><span><a href="http://auth.gulimall.com/reg.html">立即注册</a></span></h5>
修改注册页面reg.html的跳转首页和请登录链接地址
<header><a href="http://gulimall.com" class="logo"><img src="/static/reg/img/logo1.jpg" alt=""></a><div class="desc">欢迎注册</div><div class="dfg"><span>已有账号?</span><a href="http://auth.gulimall.com/login.html">请登录</a></div></header>
2、注册功能
2.1、验证码倒计时
修改注册页面reg.html的发送验证码倒计时功能
<div class="register-box"><label for="code" class="other_label">验 证 码<input maxlength="20" type="text" placeholder="请输入验证码" class="caa"></label><a id="sendCode">发送验证码</a><div class="tips"></div></div>
/*** 重发验证码倒计时事件*/$(function () {// 点击发送验证码按钮触发下面函数$('#sendCode').click(function () {// 1、倒计时 如果有disabled,说明最近已经点过,则什么都不做if ($(this).hasClass("disabled")) {//正在倒计时中} else {setTimeout("timeoutChangeStyle()", 1000);}});})var num = 60;function timeoutChangeStyle() {// 开启倒计时后设置标志属性disable,使得该按钮不能再次被点击$('#sendCode').attr("class", "disabled");// 当时间为0时,说明倒计时完成,则重置if (num == 0) {$('#sendCode').text("发送验证码");num = 60;$('#sendCode').attr("class", "");} else {// 每秒调用一次当前函数,使得num--var str = num + "s 后再次发送";$('#sendCode').text(str);setTimeout("timeoutChangeStyle()", 1000);}num--;}
页面映射
发送一个请求直接跳转到一个页面并且不传值,我们原先是在controller里创建一个跳转页面的空方法。现在我们使用SpringMVC viewController: 将请求html页面映射过来;不需要写空方法添加“com.atguigu.gulimall.auth.config.GulimallWebConfig”类,代码如下:
@Configuration
public class GulimallConfig implements WebMvcConfigurer {/*** 视图映射* @param registry*/@Overridepublic void addViewControllers(ViewControllerRegistry registry) {/*** @GetMapping("/login.html")* public String loginPage(){* return "login";* }*/registry.addViewController("/login.html").setViewName("login");registry.addViewController("/reg.html").setViewName("reg");}
}
把LoginController的页面跳转方法注释掉,重启测试
2.2、整合短信服务
在阿里云网页购买试用的短信服务[快速对接_API专区_云市场-阿里云
按照代码案例进行开发测试
阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台
访问该地址可以进appcode
1、根据代码案例在代码里进行测试
public static void main(String[] args) {String host = "https://wwsms.market.alicloudapi.com";String path = "/send_sms";String method = "POST";String appcode = "你自己的AppCode";Map<String, String> headers = new HashMap<String, String>();//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105headers.put("Authorization", "APPCODE " + appcode);//根据API的要求,定义相对应的Content-Typeheaders.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");Map<String, String> querys = new HashMap<String, String>();Map<String, String> bodys = new HashMap<String, String>();bodys.put("content", "code:1224");bodys.put("template_id", "wangweisms996"); //注意,模板wangweisms996 仅作调试使用,下发短信不稳定,请联系客服报备自己的专属签名模板,以保障业务稳定使用bodys.put("phone_number", "174*****1651");//可以提交工单联系客服,或者钉钉联系,钉钉号:1ko_t720ssqc54try {/*** 重要提示如下:* 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(response.toString());//获取response的body//System.out.println(EntityUtils.toString(response.getEntity()));} catch (Exception e) {e.printStackTrace();}}
2、在gulimall-third-party中编写发送短信组件,其中host、path、appcode可以在配置文件中使用前缀spring.cloud.alicloud.sms进行配置
spring:cloud:alicloud:sms:host: https://wwsms.market.alicloudapi.compath: /send_smstemplate_id: wangweisms996appcode: XXX
添加“com.atguigu.gulimall.thridparty.component.SmsComponent”类,代码如下:
package com.atguigu.gulimall.thridparty.component;import com.atguigu.gulimall.thridparty.util.HttpUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;@Slf4j
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data //为这些方法生成getter,setter
@Component
public class SmsComponent {private String host; // 【1】请求地址 支持http 和 https 及 WEBSOCKETprivate String path; // 【2】后缀private String template_id;private String appcode; // 【3】开通服务后 买家中心-查看AppCodepublic void sendSmsCode(String phone,String code) {String method = "POST";Map<String, String> headers = new HashMap<String, String>();//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105headers.put("Authorization", "APPCODE " + appcode);//根据API的要求,定义相对应的Content-Typeheaders.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");Map<String, String> querys = new HashMap<String, String>();Map<String, String> bodys = new HashMap<String, String>();bodys.put("content", "code:"+code);bodys.put("template_id", template_id); //注意,模板wangweisms996 仅作调试使用,下发短信不稳定,请联系客服报备自己的专属签名模板,以保障业务稳定使用bodys.put("phone_number", phone);//可以提交工单联系客服,或者钉钉联系,钉钉号:1ko_t720ssqc54try {/*** 重要提示如下:* 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(response.toString());//获取response的body//System.out.println(EntityUtils.toString(response.getEntity()));} catch (Exception e) {e.printStackTrace();}}}
添加“com.atguigu.gulimall.thridparty.controller.SmsSendController”,代码如下:
@RestController
@RequestMapping("/sms")
public class SmsSendController {@AutowiredSmsComponent smsComponent;/*** 提供给别的服务进行调用* @param phone* @param code* @return*/@GetMapping("/sendCode")public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){smsComponent.sendSmsCode(phone,code);return R.ok();}
}
2、在gulimall-commom常量包下创建验证码的常量类
添加“com.atguigu.common.constant.AuthServerConstant”类,代码如下:
public class AuthServerConstant {public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
}
修改“com.atguigu.common.exception.BizCodeEnume”类,添加验证码60秒内重复获取验证码的错误提示常量
public enum BizCodeEnume {UNKNOW_EXCEPTION(10000,"系统未知异常"),VAILD_EXCEPTION(10001,"参数格式校验失败"),SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),PRODUCT_UP_EXCEPTION(11000,"商品上架异常");private int code;private String msg;BizCodeEnume(int code,String msg){this.code = code;this.msg = msg;}public int getCode(){return code;}public String getMsg(){return msg;}
}
3、在gulimall-auth-server中进行远程调用短信验证码服务,并且页面渲染
添加redis依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
配置redis
#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379
远程调用验证码
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {@GetMapping("/sms/sendCode")R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
4、接口防刷
由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。
在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储
如果调用时以当前phone取出的值不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息
60s以后再次调用,需要删除之前存储的phone-code
code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
添加“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:
@Controller
public class LoginController {@AutowiredThirdPartFeignService thirdPartFeignService;@AutowiredStringRedisTemplate stringRedisTemplate;@ResponseBody@GetMapping("/sms/sendcode")public R sendCode(@RequestParam("phone") String phone){// 1、接口防刷String prefixPhone = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone;String redisCode = stringRedisTemplate.opsForValue().get(prefixPhone);if (!StringUtils.isEmpty(redisCode)){long l = Long.parseLong(redisCode.split("_")[1]);if (System.currentTimeMillis() -l < 60000){// 60秒内不能再发return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());}}// 2、验证码的再次校验。redis 存key-phone, value-code sms:code:18896736055 ->12345String code = String.valueOf((int)((Math.random() + 1) * 100000));// redis缓存验证码 防止同一个phone在60s内再次发送验证码 set(K var1, V var2, long var3, TimeUnit var5)stringRedisTemplate.opsForValue().set(prefixPhone,code + "_" + System.currentTimeMillis(),10, TimeUnit.MINUTES);thirdPartFeignService.sendCode(phone,code);return R.ok();}
}
自己总结的逻辑
思路:
1.先从redis获取sms:code:手机号
1.1若有值
1.1.1判断是否为60s内
1.1.1.1 是==》返回异常信息,不能发送
1.1.1.2 否==》存储redis,同时发送sms请求
1.2没有值 ==》直接存储redis,同时发送sms请求
public R sendCode(@RequestParam("phone") String phone) {// 1.验证码防刷String prefixPhone = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone; // sms:code:187xxxxxxxx// 先从redis获取验证码,如果能获取到,说明60s内已经发送过验证码,直接返回,不用再次发送String redisCode = redisTemplate.opsForValue().get(prefixPhone);if (!StringUtils.isEmpty(redisCode)) {// 不为空,说明在10分钟内// 继续判断是否过60s,以防重发,先从redisCode取出时间戳long l = Long.parseLong(redisCode.split("_")[1]);if (System.currentTimeMillis() - l < 60000) {// 60秒内不能再发return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());}}String code = String.valueOf((int) ((Math.random() + 1) * 100000));// 2.验证码校验60s内只能发一次,使用redis sms:code:187xxxxxxxx-> code 存储10分钟 过期// code+ "_" + System.currentTimeMillis()为了记录时间redisTemplate.opsForValue().set(prefixPhone, code + "_" + System.currentTimeMillis(), 10, TimeUnit.MINUTES);R r = feignService.sendCode(phone, code);return R.ok();}
修改reg.html的js
/*** 发送验证码事件*/$(function () {// 点击发送验证码按钮触发下面函数$('#sendCode').click(function () {// 1、倒计时 如果有disabled,说明最近已经点过,则什么都不做if ($(this).hasClass("disabled")) {//正在倒计时中} else {// 2、给指定手机号发送验证码$.get("/sms/sendcode?phone=" + $("#phoneNum").val(), function (data) {if (data.code != 0) {alert(data.msg);}});setTimeout("timeoutChangeStyle()", 1000);}});})var num = 60;function timeoutChangeStyle() {// 开启倒计时后设置标志属性disable,使得该按钮不能再次被点击$('#sendCode').attr("class", "disabled");// 当时间为0时,说明倒计时完成,则重置if (num == 0) {$('#sendCode').text("发送验证码");num = 60;$('#sendCode').attr("class", "");} else {//每秒调用一次当前函数,使得num--var str = num + "s 后再次发送";$('#sendCode').text(str);setTimeout("timeoutChangeStyle()", 1000);}num--;}
5、注册接口编写
在gulimall-auth-server服务中编写注册的主体逻辑:
若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员服务注册
会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
1)、使用JSR303校验要导入validation依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
添加“com.atguigu.gulimall.auth.vo.UserRegistVo”类,代码如下:
@Data
public class UserRegistVo {@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;
}
注:
RedirectAttributes可以通过session保存信息并在重定向的时候携带过去。
重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉。
使用 return "forward:/reg.html"; 会出现:
问题:Request method 'POST' not supported的问题
原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)
校验出错转发到注册页
修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:
/*** 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉* RedirectAttributes redirectAttributes 模拟重定向携带数据* @param vo* @param result* @param redirectAttributes* @return*/@PostMapping("/regist")public String regist(@Valid UserRegistVo vo, BindingResult result,RedirectAttributes redirectAttributes){if (result.hasErrors()){/*** 方法一* Map<String, String> errors = result.getFieldErrors().stream().map(fieldError ->{* String field = fieldError.getField();* String defaultMessage = fieldError.getDefaultMessage();* errors.put(field,defaultMessage);* return errors;* }).collect(Collector.asList());*/// 方法二:// 1、如果校验不通过,则封装校验结果Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));// 将错误信息封装到session中redirectAttributes.addFlashAttribute("errors",errors);/*** 使用 return "forward:/reg.html"; 会出现* 问题:Request method 'POST' not supported的问题* 原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)* 校验出错转发到注册页*///return "reg"; //转发会出现重复提交的问题,不要以转发的方式//使用重定向 解决重复提交的问题。但面临着数据不能携带的问题,就用RedirectAttributesreturn "redirect:http://auth.gulimall.com/reg.html";}// 2、校验验证码String code = vo.getCode();String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());if (!StringUtils.isEmpty(s)) {if (code.equals(s.split("_")[0])) {// 验证码通过,删除缓存中的验证码;令牌机制stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());// 真正注册调用远程服务注册R r = memberFeignService.regist(vo);if (r.getCode() == 0) {//成功return "redirect:http://auth.gulimall.com/login.html";} else {Map<String, String> errors = new HashMap<>();errors.put("msg", r.getData(new TypeReference<String>() {}));redirectAttributes.addFlashAttribute("errors", errors);}} else {Map<String, String> errors = new HashMap<>();errors.put("code", "验证码错误");redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimall.com/reg.html";}} else {Map<String, String> errors = new HashMap<>();errors.put("code", "验证码错误");redirectAttributes.addFlashAttribute("errors", errors);// 校验出错转发到注册页return "redirect:http://auth.gulimall.com/reg.html";}// 注册成功回到登录页return "redirect:http://auth.gulimall.com/login.html";}
2)、添加远程调用会员服务注册用户的接口
添加“com.atguigu.gulimall.auth.feign.MemberFeignService”类,代码如下:
@FeignClient("gulimall-member")
public interface MemberFeignService {@PostMapping("/member/member/regist")public R regist(@RequestBody UserRegistVo vo);
}
通过gulimall-member会员服务注册逻辑
通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间
修改“com.atguigu.gulimall.member.controller.MemberController” 类,代码如下
@PostMapping("/regist")public R regist(@RequestBody MemberRegistVo vo){try{memberService.regist(vo);//异常机制:通过捕获对应的自定义异常判断出现何种错误并封装错误信息}catch (PhoneExistException e){return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());}catch (UserNameExistException e){return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());}return R.ok();}
添加“com.atguigu.gulimall.member.vo.MemberRegistVo”类,代码如下:
@Data
public class MemberRegistVo {private String userName;private String password;private String phone;
}
在gulimall-common添加“com.atguigu.common.exception.BizCodeEnume”类,用户错误信息常量
/**** 错误码和错误信息定义类* 1. 错误码定义规则为5为数字* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式* 错误码列表:* 10: 通用* 001:参数格式校验* 002:短信验证码频率太高* 11: 商品* 12: 订单* 13: 购物车* 14: 物流* 15: 用户*/
public enum BizCodeEnume {UNKNOW_EXCEPTION(10000,"系统未知异常"),VAILD_EXCEPTION(10001,"参数格式校验失败"),SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),USER_EXIST_EXCEPTION(15001,"用户已存在"),PHONE_EXIST_EXCEPTION(15002,"手机号已存在");private int code;private String msg;BizCodeEnume(int code,String msg){this.code = code;this.msg = msg;}public int getCode(){return code;}public String getMsg(){return msg;}
}
修改“com.atguigu.gulimall.member.service.MemberService”类,代码如下:
/*** 注册用户** @param vo*/void regist(MemberRegistVo vo);/*** 校验手机号唯一性* * @param phone* @throws PhoneExistException*/void checkPhoneUnique(String phone) throws PhoneExistException;/*** 校验用户名唯一性* * @param userName* @throws UserNameExistException*/void checkUserNameUnique(String userName) throws UserNameExistException;
修改“com.atguigu.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:
@Overridepublic void regist(MemberRegistVo vo) {MemberDao memberDao = this.baseMapper;MemberEntity entity = new MemberEntity();// 设置默认等级MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();entity.setLevelId(levelEntity.getId());// 检查用户名和手机号是否唯一。为了让controller能感知异常,异常机制checkPhoneUnique(vo.getPhone());checkUserNameUnique(vo.getUserName());entity.setMobile(vo.getPhone());entity.setUsername(vo.getUserName());// 密码加密BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();String encode = passwordEncoder.encode(vo.getPassword());entity.setPassword(encode);memberDao.insert(entity);}@Overridepublic void checkPhoneUnique(String phone) throws PhoneExistException {MemberDao memberDao = this.baseMapper;Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));if (count > 0){throw new PhoneExistException();}}@Overridepublic void checkUserNameUnique(String userName) throws UserNameExistException {MemberDao memberDao = this.baseMapper;Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));if (count > 0){throw new UserNameExistException();}}
添加“com.atguigu.gulimall.member.dao.MemberLevelDao”类,代码如下:
@Mapper
public interface MemberLevelDao extends BaseMapper<MemberLevelEntity> {MemberLevelEntity getDefaultLevel();
}
<select id="getDefaultLevel" resultType="com.atguigu.gulimall.member.entity.MemberLevelEntity">select * from ums_member_level where default_status = 1</select>
添加“com.atguigu.gulimall.member.exception.PhoneExistException”类,代码如下:
public class PhoneExistException extends RuntimeException {public PhoneExistException(){super("手机号存在");}
}
添加“com.atguigu.gulimall.member.exception.UserNameExistException”类,代码如下:
public class UserNameExistException extends RuntimeException {public UserNameExistException(){super("用户名已存在");}
}
3)、修改注册页面reg.html
<form action="/regist" method="post" class="one"><div class="tips" style="color:red"th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:'') : ''}"></div><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="phoneNum" 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><a id="sendCode">发送验证码</a><div class="tips" style="color:red"th:text="${errors != null ? (#maps.containsKey(errors,'code')?errors.code:'') : ''}"></div></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>
注释掉提交按钮事件
// 提交按钮// $("#submit_btn").click(function(e) {// for(var j = 0; j < 5; j++) {// if($('input').eq(j).val().length == 0) {// $('input').eq(j).focus();// if(j == 4) {// $('input').eq(j).parent().next().next("div").text("此处不能为空");// $('input').eq(j).parent().next().next("div").css("color", 'red');// e.preventDefault();// return;// }// $('input').eq(j).parent().next(".tips").text("此处不能为空");// $('input').eq(j).parent().next(".tips").css("color", 'red');// e.preventDefault();// return;// }// }// //协议// if($("#xieyi")[0].checked) {// //向变量stuList数组添加一个数值,数值内部格式Student(name,password,tel,id)// //发送用户信息// stuList.push(new Student($('input').eq(0).val(), $('input').eq(1).val(), $('input').eq(3).val(), stuList.length + 1));// localStorage.setItem('stuList', JSON.stringify(stuList));// alert("注册成功");// window.open("userlist.html", "_blank");// } else {// $("#xieyi").next().next().next(".tips").text("请勾选协议");// $("#xieyi").next().next().next(".tips").css("color", 'red');// e.preventDefault();// return;// }// })
4)、测试校验功能
3、用户名密码登录
在gulimall-auth-server模块中的主体逻辑
通过会员服务远程调用登录接口
如果调用成功,重定向至首页
如果调用失败,则封装错误信息并携带错误信息重定向至登录页
添加“com.atguigu.gulimall.auth.vo.UserLoginVo”类,代码如下:
@Data
public class UserLoginVo {private String loginacct;private String password;
}
添加“com.atguigu.gulimall.auth.feign.MemberFeignService”类,
@PostMapping("/member/member/login")public R login(@RequestBody UserLoginVo vo);
修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:
@PostMapping("/login")public String login(UserLoginVo vo, RedirectAttributes redirectAttributes){// 远程登录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>(){}));redirectAttributes.addFlashAttribute("errors",errors);return "redirect:http://auth.gulimall.com/login.html";}}
在gulimall-member模块中完成登录
校验
当数据库中含有以当前登录名为用户名或电话号且密码匹配时,验证通过,返回查询到的实体
否则返回null,并在controller返回用户名或者密码错误。
添加“com.atguigu.gulimall.member.vo.MemberLoginVo”类,代码如下:
@Data
public class MemberLoginVo {private String loginacct;private String password;
}
在gulimall-common服务中,添加登录错误信息提示的常量
添加“com.atguigu.common.exception.BizCodeEnume”类,代码如下:
LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误")
修改“com.atguigu.gulimall.member.controller.MemberController”类,代码如下:
@PostMapping("/login")public R login(@RequestBody MemberLoginVo vo){MemberEntity login = memberService.login(vo);if(login!=null){// 登录成功 将用户信息返回return R.ok().setData(login);}else{// 登录失败,返回账号或密码错误信息return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());}}
修改“com.atguigu.gulimall.member.service.MemberService”类,代码如下:
MemberEntity login(MemberLoginVo vo);
修改“com.atguigu.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:
/*** 登录功能* @param vo* @return*/@Overridepublic MemberEntity login(MemberLoginVo vo) {String loginacct = vo.getLoginacct();String password = vo.getPassword();// 检查数据库查询MemberDao memberDao = this.baseMapper;// 账号可能是用户名,也可能是手机号 所以查询用orMemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));if (memberEntity == null) {// 代表没有查到该用户信息return null;} else {// 检查到有该用户信息,获取到数据库的密码// 验证密码String DbPassword = memberEntity.getPassword();// 将前端传来的密码盐值加密后和数据库密码进行比较BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();boolean matches = bCryptPasswordEncoder.matches(password, DbPassword);if (matches) {// 登录成功return memberEntity;} else {// 登录失败return null;}}}
修改登录页面login.html
<form action="/login" method="post"><ul><div class="tips" style="color:red"th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:'') : ''}"></div><li class="top_1"><img src="/static/login/JD_img/user_03.png" class="err_img1"/><input name="loginacct" type="text" 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>
4、社交登录
QQ 、微博、 github 等网站的用户量非常大,别的网站为了简化自我网站的登陆与注册逻辑,引入社交登陆功能;
步骤:
1 )、用户点击 QQ 按钮
2 )、引导跳转到 QQ 授权页
3)、用户主动点击授权,跳回之前网页。
4.1、OAuth2.0
OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth2.0:对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
官方版流程:
(A )用户打开客户端以后,客户端要求用户给予授权。
(B )用户同意给予客户端授权。
(C )客户端使用上一步获得的授权,向认证服务器申请令牌。
(D )认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E )客户端使用令牌,向资源服务器申请获取资源。
(F )资源服务器确认令牌无误,同意向客户端开放资源。
OAuth2.0流程:
是csdn拿着client信息获取授权码code,授权码再去获取令牌
1.使用Code换取AccessToken,Code只能用一次
2.同一个用户的accessToken一段时间是不会变化的,即使多次获取
4.2、使用gitee社交登录
进入gitee网页https://gitee.com/oauth/applications/
创建应用
http://auth.gulimall.com/oauth2.0/gitee/success
记住Client ID和Client Secret
进行模拟请求查看前端用户至授权页
client_id: 创建网站应用时的App Key
YOUR_REGISTERED_REDIRECT_URI: 授权回调页(需要和平台高级设置一致)
https://gitee.com/oauth/authorize?client_id=150ffc901d8d8e9cfbea348b87d5b9edcd0f2785f3a50376f4704319d50f9944&redirect_uri=http%3A%2F%2Fauth.gulimall1.com%2Foauth2.0%2Fgitee%2Fsuccess&response_type=code
2)、用户同意授权,页面跳转至 xxx/?code=CODE
如果用户同意授权,页面跳转至
YOUR_REGISTERED_REDIRECT_URI/?code=CODE
code是我们用来换取令牌的参数
http://auth.gulimall1.com/oauth2.0/gitee/success?code=b86cf7db4aa4f6b65b27814939f25d5c9d87c494764d9ee69054fe13d2532bf7
3、使用返回的 code,换取 access token
网址:https://gitee.com/oauth/token post
参数如下
client_id","xxxx";
client_secret","xxxx";
grant_type","authorization_code";
"redirect_uri","http://auth.gulimall1.com/oauth2.0/gitee/success";
"code",code
client_id: 创建网站应用时的app key
client_secret: 创建网站应用时的app secret
YOUR_REGISTERED_REDIRECT_URI: 授权回调页(需要和平台高级设置一致)
code:换取令牌的认证码
返回数据如下
{"access_token":"03011ebd8fdee947b7bdcbfa2ab21dc8",
"token_type":"bearer",
"expires_in":86400,
"refresh_token":"61d6e3c35285b07137b4dab1bbce9cfaf88c639262bc78cdad27fd7382de35fb",
"scope":"user_info",
"created_at":1730896807}
4)、使用 AccessToken 调用开发 API 获取用户信息
获取用户信息接口文档:https://gitee.com/api/v5/user
返回结果
{"id": 8969107,"login": "LtAo857","name": "LtAo","avatar_url": "https://gitee.com/assets/no_portrait.png","url": "https://gitee.com/api/v5/users/LtAo857","html_url": "https://gitee.com/LtAo857","remark": "","followers_url": "https://gitee.com/api/v5/users/LtAo857/followers","following_url": "https://gitee.com/api/v5/users/LtAo857/following_url{/other_user}","gists_url": "https://gitee.com/api/v5/users/LtAo857/gists{/gist_id}","starred_url": "https://gitee.com/api/v5/users/LtAo857/starred{/owner}{/repo}","subscriptions_url": "https://gitee.com/api/v5/users/LtAo857/subscriptions","organizations_url": "https://gitee.com/api/v5/users/LtAo857/orgs","repos_url": "https://gitee.com/api/v5/users/LtAo857/repos","events_url": "https://gitee.com/api/v5/users/LtAo857/events{/privacy}","received_events_url": "https://gitee.com/api/v5/users/LtAo857/received_events","type": "User","blog": null,"weibo": null,"bio": "","public_repos": 4,"public_gists": 0,"followers": 0,"following": 3,"stared": 10,"watched": 21,"created_at": "2021-04-13T18:07:33+08:00","updated_at": "2024-11-06T17:37:55+08:00","email": null
}
此gitee登陆调试完成。
4.3、代码实现
1)、修改登录页面login.html,在登录页引导用户至授权页
<div class="si_out"><ul><li>
<!-- <a href="https://gitee.com/oauth/authorize?client_id=150ffc901d8d8e9cfbea348b87d5b9edcd0f2785f3a50376f4704319d50f9944&redirect_uri=http://auth.gulimall1.com/oauth2.0/gitee/success">--><a href="https://gitee.com/oauth/authorize?client_id=150ffc901d8d8e9cfbea348b87d5b9edcd0f2785f3a50376f4704319d50f9944&redirect_uri=http%3A%2F%2Fauth.gulimall1.com%2Foauth2.0%2Fgitee%2Fsuccess&response_type=code">
<!-- <img src="/static/login/JD_img/qq.png" />--><span>Gitee</span></a></li>
2)、添加社交登录回调接口
认证接口
通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
修改“com.atguigu.gulimall.auth.feign.MemberFeignService”类,代码如下:
@PostMapping("/member/member/oauth2/login")
public R oauth2Login(@RequestBody SocialUser socialUser);
添加“com.atguigu.gulimall.auth.vo.SocialUser”类,代码如下:
package com.atguigu.gulimall.auth.vo;import lombok.Data;@Data
public class SocialUser {private String access_token;private String token_type;private long expires_in;private String refresh_token;private String scope;
}
添加“com.atguigu.gulimall.auth.vo.MemberResponseVO”类,代码如下:
package com.atguigu.gulimall.auth.vo;import lombok.Data;
import lombok.ToString;import java.util.Date;@ToString
@Data
public class MemberResponseVO {private Long id;/*** 会员等级id*/private Long levelId;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 昵称*/private String nickname;/*** 手机号码*/private String mobile;/*** 邮箱*/private String email;/*** 头像*/private String header;/*** 性别*/private Integer gender;/*** 生日*/private Date birth;/*** 所在城市*/private String city;/*** 职业*/private String job;/*** 个性签名*/private String sign;/*** 用户来源*/private Integer sourceType;/*** 积分*/private Integer integration;/*** 成长值*/private Integer growth;/*** 启用状态*/private Integer status;/*** 注册时间*/private Date createTime;private String socialUid;private String accessToken;private long expiresIn;
}
添加“com.atguigu.gulimall.auth.controller.Oauth2Controller”类,代码如下:
pom文件https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.15</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.2.1</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore</artifactId><version>4.2.1</version></dependency><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency><dependency><groupId>org.eclipse.jetty</groupId><artifactId>jetty-util</artifactId><version>9.3.7.v20160115</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.5</version><scope>test</scope></dependency>
@Slf4j
@Controller
public class OAuth2Controller {@AutowiredMemberFeignService memberFeignService;@GetMapping("/oauth2.0/gitee/success")public String auth2Login(@RequestParam("code") String code, RedirectAttributes attributes) throws Exception {Map<String,String> map = new HashMap<>();map.put("client_id","150ffc901d8d8e9cfbea348b87d5b9edcd0f2785f3a50376f4704319d50f9944");map.put("client_secret","179a706e65cf6cb801b3eb4a83585556163944056030578bd02c8c6ad7d01f68");map.put("grant_type","authorization_code");map.put("redirect_uri","http://auth.gulimall1.com/oauth2.0/gitee/success");map.put("code",code);//1、根据code换取accessToken;HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token","post", new HashMap<>(), map, new HashMap<>());Map<String, String> errors = new HashMap<>();//得到access_tokenif(response.getStatusLine().getStatusCode()==200){//EntityUtils是org.apache.http.util下的一个工具类String s = EntityUtils.toString(response.getEntity());System.out.println(s);/*** {"access_token":"cfc13c01b278c2e67451bccc3749510c",* "token_type":"bearer",* "expires_in":86400,* "refresh_token":"4c33f9a58a76e25b120b6b7f19c7c9355ee46b079bf57b7f1b92360a5038ce26",* "scope":"user_info","created_at":1730889826}* */SocialUser socialUser = JSON.parseObject(s,SocialUser.class);//判断这个用户是否是第一次使用当前社交登录R r = memberFeignService.oauth2Login(socialUser);if(r.getCode()==0){//登录成功String jsonString = JSON.toJSONString(r.get("memberEntity"));MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() {});attributes.addFlashAttribute("user", memberResponseVo);return "redirect:http://gulimall1.com";}else{// 2.2 否则返回登录页errors.put("msg", "登录失败,请重试");attributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimall1.com/login.html";}}else{errors.put("msg", "获得第三方授权失败,请重试");attributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimall1.com/login.html";}}}
登录接口
登录包含两种流程,实际上包括了注册和登录
如果之前未使用该社交账号登录,则使用token调用开放api获取社交账号相关信息,注册并将结果返回
如果之前已经使用该社交账号登录,则更新token并将结果返回
添加“com.atguigu.gulimall.member.vo.SocialUser”类,代码如下:
package com.atguigu.gulimall.member.vo;import lombok.Data;@Data
public class SocialUser {private String access_token;private String token_type;private long expires_in;private String refresh_token;private String scope;
}
修改“com.atguigu.gulimall.member.controller.MemberController”类,代码如下:
/*** 社交登录,接受到第三方社交登录传过来的参数* private String access_token;* private String remind_in;* private long expires_in;* private String uid;* private String isRealName;* 逻辑:* 如果之前未使用该社交账号登录,将第三方的数据进行验证,使用token调用开放api获取社交账号相关信息,注册并将结果返回* 如果之前已经使用该社交账号登录,则更新token并将结果返回* @param vo* @return* @throws Exception* @PostMapping("/member/member/oauth2/login")* public R oauth2Login(@RequestBody SocialUser socialUser);*/@RequestMapping("/oauth2/login")public R oauth2Login(@RequestBody SocialUser vo) throws Exception{MemberEntity memberEntity = memberService.login(vo);if(memberEntity!=null){// 登录成功 将用户信息返回return R.ok().setData(memberEntity);}else{// 登录失败,返回账号或密码错误信息return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());}}
修改gulimall_ums.ums_member表结构,sql如下:
ALTER TABLE `gulimall_ums`.`ums_member`
ADD COLUMN `social_uid` varchar(255) NULL COMMENT '社交用户id' AFTER `create_time`,
ADD COLUMN `access_token` varchar(255) NULL COMMENT '访问token' AFTER `social_uid`,
ADD COLUMN `expires_in` int NULL COMMENT '过期时间戳' AFTER `access_token`;
修改“com.atguigu.gulimall.member.entity.MemberEntity”类,新增三个属性,代码如下:
修改“com.atguigu.gulimall.member.service.MemberService”类,代码如下:
MemberEntity login(SocialUser socialUser);
修改“com.atguigu.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:
gitee使用token获取用户信息只需要一个参数
/*** 社交登录* @param vo* @return*/@Overridepublic MemberEntity login(SocialUser vo) throws Exception {// 根据uid判断是否使用该社交登录进行过第一次登录// 若登录则返回用户信息// 若未登录过,则将用户信息(先通过api查询到)再插入数据库String name="";Map<String,String> map=new HashMap<>();map.put("access_token", vo.getAccess_token());HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<String, String>(), map);if(response.getStatusLine().getStatusCode()==200){String s = EntityUtils.toString(response.getEntity());JSONObject jsonObject = JSON.parseObject(s);int id = (int)jsonObject.get("id");name = (String)jsonObject.get("name");vo.setUid(id+"");}LambdaQueryWrapper<MemberEntity> lqw = new LambdaQueryWrapper<>();lqw.eq(MemberEntity::getSocialUid,vo.getUid());MemberEntity entity = this.getOne(lqw);if(entity!=null){//这个用户已经注册//Access_token存在过期时间,所以每一次登录需要更新Access_tokenMemberEntity update = new MemberEntity();update.setId(entity.getId());update.setAccessToken(vo.getAccess_token());update.setExpiresIn(Long.parseLong(vo.getExpires_in()+""));baseMapper.updateById(update);entity.setExpiresIn(Long.parseLong(vo.getExpires_in()+""));entity.setAccessToken(vo.getAccess_token());return entity;}else{//没有就注册MemberEntity memberEntity = new MemberEntity();memberEntity.setAccessToken(vo.getAccess_token());memberEntity.setExpiresIn(Long.parseLong(vo.getExpires_in()+""));memberEntity.setSocialUid(vo.getUid());memberEntity.setNickname(name);baseMapper.insert(memberEntity);return memberEntity;}}
小结
Oauth2.0 ;授权通过后,使用 code 换取 access_token ,然后去访问任何开放 API
1 )、 code 用后即毁
2 )、 access_token 在几天内是一样的
3 )、 uid 永久固定
总结
社交登录(gitee为例):
1.先再gitee的数据管理的第三方用下注册一个自己的应用,填写好回调地址。
2.在前端a标签上,href上链接到gitee的地址(携带应用信息,例如client_id和回调地址)。
3.授权成功后返回到回调地址指定的页面,同时携带code码。
4.在auth服务下写controller接受回调地址的接口,通过code请求access接口,获取accessToken。
5.有了accessToken可以调用gitee的获取用户信息api,获取用户信息。
6.判断用户是否第一个使用社交登录,若是第一次,则将api中的uid添加到实体类中,插入到数据库中, 同时保存accessToken等信息;若不是第一个,更新uid,accessToken等信息。
5、SpringSession
5.1、Session共享问题
1)、session原理
jsessionid相当于银行卡,存在服务器的session相当于存储的现金,每次通过jsessionid取出保存的数据。
问题:但是正常情况下session不可跨域,它有自己的作用范围(在工商办的卡,不能在农行用)
在auth.gulimall下存在jsessionid,但是跳转到gulimall下jsessionid失效。
2)、分布式下session共享问题
同一个服务,复制多份,session不同步问题
不同服务,session不能共享问题
5.2、Session共享问题解决
1)、session复制
缺点:大型分布式集群下,占用大量内存
2)、客户端存储
缺点:不安全,cookie长度有限
3)、hash一致性
4)、后台统一存储
缺点:查询数据库,慢了
5)、不同服务,子域session共享
解决方法:
1、放大域,子域的信息,父域可以使用(存储到redis)
2、发卡的时候(指定域名为父域),即使是子域系统发的卡,也能让父域直接使用。
5.3、SpringSession整合redis
redis存储session
通过SpringSession修改session的作用域
session作用域:当前会话(关闭页面则失效)
1)、环境搭建
gulimall-auth-server模块
pom导入依赖
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency>
修改apllication.properties配置
spring.session.store-type=redis
主配置类添加注解@EnableRedisHttpSession
修改“com.atguigu.gulimall.auth.GulimallAuthServerApplication”类,代码如下:
@EnableRedisHttpSession
@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {public static void main(String[] args) {SpringApplication.run(GulimallAuthServerApplication.class, args);}}
2)、自定义配置
由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com
添加“com.atguigu.gulimall.auth.config.GulimallSessionConfig”类,代码如下:
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer(){return new GenericJackson2JsonRedisSerializer();}
}
把MemberResponseVo类移到gulimall-common服务里,并且序列化
添加“com.atguigu.common.vo.MemberResponseVO”类,代码如下
@ToString
@Data
public class MemberResponseVO implements Serializable {private Long id;/*** 会员等级id*/private Long levelId;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 昵称*/private String nickname;/*** 手机号码*/private String mobile;/*** 邮箱*/private String email;/*** 头像*/private String header;/*** 性别*/private Integer gender;/*** 生日*/private Date birth;/*** 所在城市*/private String city;/*** 职业*/private String job;/*** 个性签名*/private String sign;/*** 用户来源*/private Integer sourceType;/*** 积分*/private Integer integration;/*** 成长值*/private Integer growth;/*** 启用状态*/private Integer status;/*** 注册时间*/private Date createTime;private String socialUid;private String accessToken;private long expiresIn;
}
修改“com.atguigu.gulimall.auth.controller.Oauth2Controller”类,代码如下:
@GetMapping("/oauth2.0/weibo/success")public String weibo(@RequestParam("code") String code, HttpSession session, RedirectAttributes attributes) throws Exception {// 1. 使用code换取token,换取成功则继续2,否则重定向至登录页Map<String,String> map = new HashMap<>();map.put("client_id","798445888");map.put("client_secret","7886f4db232d2e932690e08e346c3e67");map.put("grant_type","authorization_code");map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");map.put("code",code);// 1、根据code换取accessTokenHttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());Map<String, String> errors = new HashMap<>();if (response.getStatusLine().getStatusCode() == 200){// 2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页String jsonString = EntityUtils.toString(response.getEntity());SocialUser socialUser = JSON.parseObject(jsonString, SocialUser.class);// 1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)// 获取用户的登录平台,然后判断用户是否该注册到系统中R r = memberFeignService.oauth2Login(socialUser);// 2.1 远程调用成功,返回首页并携带用户信息if (r.getCode() == 0) {// session 子域共享问题MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {});log.info("登陆成功:用户信息"+loginUser.toString());//TODO 1、默认发的令牌。 session=dakadja; 作用域:当前域。(解决子域session共享问题)//TODO 2、使用json的序列化方式来序列化对象数据到redis中session.setAttribute("loginUser", loginUser);return "redirect:http://gulimall.com";} else {//2.2 否则返回登录页errors.put("msg", "登录失败,请重试");attributes.addFlashAttribute("errors", errors);return "redirect:http://auth.catmall.com/login.html ";}}else {errors.put("msg", "获得第三方授权失败,请重试");attributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimall.com/login.html";}}
gulimall-product模块
添加依赖
<!--整合SpringSession完成session共享问题--><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency>
修改配置
spring:session:store-type: redis #指定session的存储格式
添加注解
配置“com.atguigu.gulimall.product.config.GulimallSessionConfig”类,代码如下:
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer(){return new GenericJackson2JsonRedisSerializer();}
}
3)、修改主页index.html的代码
<li><a th:if="${session.loginUser != null}">欢迎:[[${session.loginUser == null ? '' : session.loginUser.nickname}]]</a><a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser == null}">欢迎,请登录</a></li><li><a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/reg.html">免费注册</a></li>
4)、测试
可以看到当我们社交登录之后,主页显示当前登录的用户名
5.5、SpringSession核心原理
核心原理:
1)、@EnableRedisHttpSession导入RedisHttpSessionConfiguration.class配置
1、给容器中添加了一个组件
SessionRepository=》》》 【RedisIndexedSessionRepository】=>redis操作session.session的增删改查的封装类
2、SessionRepositoryFilter=》Filter: session存储过滤器,每个请求过来都必须经过filter
2.1、创建的时候,就自动从容器中获取到了SessionRepository:
2. 2、原生的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
2. 3、以后获取session.request.getSession()
2. 4、wrapperedRequest.getSession();===>SressionRepository中获取到
3.自动延期。redis中的数据也是有过期时间的
装饰者模式 - SessionRepositoryFilter
1.原生的获取session时是通过HttpServletRequest获取的
2.这里对request进行包装,并且重写了包装request的getSession()方法
6、页面调整
1)、只要登录成功,缓存有用户数据,再点击登录链接,直接调转到首页;把GulimallWebConfig登录页的映射注释掉
修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:
@GetMapping("/login.html")public String loginPage(HttpSession session){Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);if (attribute == null) {//没登录return "login";} else{return "redirect:http://gulimall.com";}}
2)、账号密码方式登录也要显示用户名
正常登录也要显示用户名,返回时也要给他放入用户信息
修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:
@PostMapping("/login")public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session) {log.info("登录请求参数:{}", JSON.toJSONString(vo));//远程登录R r = memberFeignService.login(vo);if (r.getCode() == 0) {MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {});// 成功放到session中session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);return "redirect:http://gulimall.com";} else {Map<String, String> errors = new HashMap<>();errors.put("msg", r.getData("msg", new TypeReference<String>() {}));redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimall.com/login.html";}}
3)、只要登陆成功每个页面都显示用户名
详情页item.html
<li style="border: 0;width: 150px"><a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser == null}" class="aa">你好,请登录</a><a th:else class="aa">你好,[[${session.loginUser.nickname}]]</a></li><li th:if="${session.loginUser == null}"><a href="http://auth.gulimall.com/reg.html" style="color: red;">免费注册</a> |</li>
gulimall-search服务页面显示用户名,需要先搭建好SpringSession环境
导入依赖
<!--整合SpringSession完成session共享问题--><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
修改配置
#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379
#session存储格式
spring.session.store-type=redis
加注解
添加SpringSession配置类
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer(){return new GenericJackson2JsonRedisSerializer();}
}
修改list.html页面
<li style="width: 150px"><a href="http://auth.gulimall.com/login.html" class="li_2"th:if="${session.loginUser == null}">你好,请登录</a><a th:else class="aa">你好,[[${session.loginUser.nickname}]]</a></li><li><a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser == null}">免费注册</a></li>
1-6总结
1.最初始的session不能解决跨网站的使用,所以使用springSession
2.扩大域,需要自定义一个配置类(解析存储到redis中的数据可以被反序列化),添加pom,配置文件session.store.type=redis(使用redis存储)
3.使用HttpSession session的setAttratubes属性可以将数据传到前端的cookie里面,前端通过jQuery的&${session.xxx}可以获取到数据
4.若是使用社交登录,成功的时候将gitee的name设置到会员实体类中,同时insert到数据库。否则更新数据。
5.为防止首页登录后不显示名称(nickname),在注册成功的时候就设置 memberEntity.setNickname(vo.getUserName());
即将nickname设置成注册的用户名。
7.至此,整个流程打通。
7、SSO(单点登陆)
Single Sign On 一处登陆、处处可用
前置概念
单点登录业务介绍
早期单一服务器,用户认证。
缺点:单点性能压力,无法扩展
分布式,SSO(single sign on)模式
多系统
解决 :
用户身份信息独立管理,更好的分布式管理。
可以自己扩展安全策略
跨域不是问题
缺点:
认证服务器访问压力较大。
gitee参考项目:xxl-sso: 一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持"等特性;。现已开放源代码,开箱即用。
xxl-sso流程:
/xxl-sso-server 登录服务器 8080 ssoserver.com
/xxl-sso-web-sample-springboot 项目1 8081 client1.com
/xxl-sso-web-sample-springboot 项目2 8082 client2.com
#----------sso----------
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
核心:三个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;
1)、中央认证服务器;ssoserver.com
2)、其他系统,想要登录去ssoserver.com登录,登录成功跳转回来
3)、只要有一个登录,其他都不用登录
4)、全系统统一一个sso-sessionid;所有系统可能域名都不相同
问题:上面解决了同域名的session问题,但如果taobao.com和tianmao.com这种不同的域名也想共享session呢?
去百度了解下:单点登录(SSO)看这一篇就够了 - 简书
最终解决方案:都去中央认证器
spring session已经解决不了不同域名的问题了。无法扩大域名
sso思路
记住一个核心思想:建议一个公共的登陆点server,他登录了代表这个集团的产品就登录过了
上图是CAS官网上的标准流程,具体流程如下:有两个子系统app1、app21.用户访问app1系统,app1系统是需要登录的,但用户现在没有登录。
2.跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
3.用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
4.SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app1系统,同时将ST作为参数传递给app1系统。
5.app1系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
6.验证通过后,app系统将登录状态写入session并设置app域下的Cookie。至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。1.用户访问app2系统,app2系统没有登录,跳转到SSO。
2.由于SSO已经登录了,不需要重新登录认证。
3.SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
4.app2拿到ST,后台访问SSO,验证ST是否有效。
5.验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。如果想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
SSO-Single Sign On
server:登录服务器、8080 、ssoserver.com
web-sample1:项目1 、8081 、client1.com
web-sample2:项目1 、8082 、client2.com
3个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;
中央认证服务器
其他系统都去【中央认证服务器】登录,登录成功后跳转回原服务
一个系统登录,都登录;一个系统登出,都登出
全系统统一一个sso-sessionId
访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
定向认证:SSO客户端会重定向用户请求到SSO服务器。
用户认证:用户身份认证。
发放票据:SSO服务器会产生一个随机的Service Ticket。
验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。
单点退出:用户退出单点登录。
开源项目
先看一下开源sso的项目xxl-sso: 一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持"等特性;。现已开放源代码,开箱即用。
ssoserver.com 登录认证服务
client1.com
cleitn2.com
修改HOSTS:127.0.0.1ssoserver.com+client1.com+client2.com
server:登录服务器、8080 、ssoserver.com
web-sample1:项目1 、8081 、client1.com
web-sample2:项目1 、8082 、client2.com
# 根项目下
mvn clean package -Dmaven.skip.test=true
# 打包生成了server和client包
# 启动server和client
#server8080 cient1:web-sample8081 cient2:web-sample8082
# 让client12登录一次即可
java -jar server.jar # 8080
java -jar client.jar
# 启动多个web-sample模拟多个微服务
把core项目mvc install 。启动server
老师用的业务流程图
流程
1.发送8081/employees请求,判断没登录就跳转到server.com:8080/login.html登录页,并带上现url
2.server登录页的时候,有之前带过来的url信息,发送登录请求的时候也把url继续带着
doLogin登录成功后返回一个token(保存到server域名下)然后重定向
3.登录完后重定向到带的url参数的地址。
4.跳转回业务层的时候,业务层要能感知是登录过的,调回去的时候带个uuid,用uuid去redis里(课上说的是去server里再访问一遍,为了安全性?)看user信息,保存到它系统里自己的session
5.以后无论哪个系统访问,如果session里没有指定的内容的话,就去server登录,登录过的话已经有了server的cookie,所以不用再登录了。回来的时候就告诉了子系统应该去redis里怎么查你的用户内容
还得得补充一句,老师课上讲得把票据放到controller里太不合适了,你最起码得放到filter或拦截器里
sso解决
client1.com 8081 和 client2.com 8082 都跳转到ssoserver 8080
给登录服务器留下痕迹
登录服务器要将token信息重定向的时候,带到url地址上
其他系统要处理url地址上的token,只要有,将token对应的用户保存到自己的session
自己系统将用户保存在自己的session中
<body><form action="/employee" method="get"><input type="text" name="username" value="test"><button type="submit">查询</button></form>
</body>
@GetMapping(value = "/employees") // a系统
public String employees(Model model,HttpSession session,@RequestParam(value = "redisKey", required = false) String redisKey) {// 有loginToken这个参数,代表去过server端登录过了,server端里在redis里保存了个对象,而key:uuid给你发过来了// 有loginToken这个参数的话代表是从登录页跳回来的,而不是系统a直接传过来的// 你再拿着uuid再去查一遍user object,返回后设置到当前的系统session里// 提个问题:为什么当时不直接返回user对象,而是只返回个uuid?其实也可以,但是参数的地方也得是required = false。可能也有一些安全问题if (!StringUtils.isEmpty(redisKey)) { // 这个逻辑应该写到过滤器或拦截器里RestTemplate restTemplate=new RestTemplate();// 拿着token去服务器,在服务端从redis中查出来他的usernameResponseEntity<Object> forEntity =restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?redisKey="+ redisKey, Object.class);Object loginUser = forEntity.getBody();// 设置到自己的session中session.setAttribute("loginUser", loginUser);}// session里有就代表登录过 // 获得userObject loginUser = session.getAttribute("loginUser");if (loginUser == null) { // 又没有loginToken,session里又没有object,去登录页登录return "redirect:" + "http://ssoserver.com:8080/login.html"+ "?url=http://clientA.com/employees";} else {// 登录过,执行正常的业务List<String> emps = new ArrayList<>();emps.add("张三");emps.add("李四");model.addAttribute("emps", emps);return "employees";}
}
server端
子系统都先去login.html这个请求,
这个请求会告诉登录过的系统的令牌,
如果没登录过就带着url重新去server端,server给一个登录页,如下
<body>
<form action="/doLogin" method="post"><!--刚才要请求数据的url,没有也没关系,就不跳转了呗--><input type="hidden" name="url" th:value="${url}"><!--带上当前登录的username-->
<!-- <input type="hidden" name="user" th:value="${username}">-->用户名:<input name="username" value="test"><br/>密码:<input name="password" type="password" value="test"><input type="submit" value="登录">
</form>
</body>
当点击登录之后,server端返回一个cookie,子系统重新返回去重新请去业务。于是又来server端验证,这回server端有cookie了,该cookie里有用户在redis中的key,重定向时把key带到url后面,子系统就知道怎么找用户信息了
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate stringRedisTemplate;@ResponseBody
@GetMapping("/userInfo") // 得到redis中的存储过的user信息,返回给子系统的session中
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) {// 非空代表就登录过了if (!StringUtils.isEmpty(redisKey)) {// 告诉子系统他的redisKey,拿着该token就可以查redis了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内代表取查完数据库了if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){//简单认为登录正确// 登录成功跳转 跳回之前的页面String redisKey = UUID.randomUUID().toString().replace("-", "");// 存储cookie, 是在server.com域名下存Cookie cookie = new Cookie("redisKey", redisKey);response.addCookie(cookie);// redis中存储stringRedisTemplate.opsForValue().set(redisKey, username+password+"...", 30, TimeUnit.MINUTES);// user中存储的url 重定向时候带着tokenreturn "redirect:" + url + "?redisKey=" + redisKey;}// 登录失败return "login";
}
}