[java-project-gl]单点登录与社交登录

一、注册登录

(一)验证码功能

1、注册页面

reg.html展示页面:

<a id="sendCode"> 发送验证码 </a>

reg.html的 验证码的相关javascript:

			$(function () {
				$("#sendCode").click(function () {
					//2、倒计时
					var str = "60s 后再次发送验证码";
					if($(this).hasClass("disabled")){
						//正则倒计时
					}else{
						//1、给指定手机号发送验证码
						$.get("/sms/sendcode?phone="+$("#phoneNum").val(),function (data) {
							if (data.code !=0){
								alert(data.msg);
							}
						});
						timeoutChangeStyle();
					}

				});
			})
			//发送验证码的【<a class="disabled" id="sendCode"> 发送验证码 </a>】,不能够再点击
			//时间重置60秒后,【<a class="" id="sendCode"> 发送验证码 </a>】,可以再点击

			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 -- ;
			}
		</script>

2、第三方短信验证码功能

gulimall-third-party

相关配置

application.yml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alicloud:
      sms:
        host: https://smsmsgs.market.alicloudapi.com
        path: /sms/
        skin: 1
        sign: 1
        appcode: 你的appcode

配置类,输入 “手机号 ” 和 “验证码 ” 即可发送

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;

@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data
@Component
public class SmsComponent {
    private String host;
    private String path;
    private String skin;
    private String sign;
    private String appcode;

    public void sendSmsCode(String phone,String code){
        String urlSend = host + path + "?code=" + code +"&phone="+phone +"&sign="+sign +"&skin="+skin;   // 【5】拼接请求链接
        try {
            URL url = new URL(urlSend);
            HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
            httpURLCon.setRequestProperty("Authorization", "APPCODE " + appcode);// 格式Authorization:APPCODE (中间是英文空格)
            int httpCode = httpURLCon.getResponseCode();
            if (httpCode == 200) {
                String json = read(httpURLCon.getInputStream());
                System.out.println("正常请求计费(其他均不计费)");
                System.out.println("获取返回的json:");
                System.out.print(json);
            } else {
                Map<String, List<String>> map = httpURLCon.getHeaderFields();
                String error = map.get("X-Ca-Error-Message").get(0);
                if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
                    System.out.println("AppCode错误 ");
                } else if (httpCode == 400 && error.equals("Invalid Url")) {
                    System.out.println("请求的 Method、Path 或者环境错误");
                } else if (httpCode == 400 && error.equals("Invalid Param Location")) {
                    System.out.println("参数错误");
                } else if (httpCode == 403 && error.equals("Unauthorized")) {
                    System.out.println("服务未被授权(或URL和Path不正确)");
                } else if (httpCode == 403 && error.equals("Quota Exhausted")) {
                    System.out.println("套餐包次数用完 ");
                } else {
                    System.out.println("参数名错误 或 其他错误");
                    System.out.println(error);
                }
            }

        } catch (MalformedURLException e) {
            System.out.println("URL格式错误");
        } catch (UnknownHostException e) {
            System.out.println("URL地址错误");
        } catch (Exception e) {
            // 打开注释查看详细报错异常信息
            // e.printStackTrace();
        }
    }

    private static String read(InputStream is) throws IOException {
        StringBuffer sb = new StringBuffer();
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line = null;
        while ((line = br.readLine()) != null) {
            line = new String(line.getBytes(), "utf-8");
            sb.append(line);
        }
        br.close();
        return sb.toString();
    }
}

启动类加上@EnableDiscoveryClient,供别的微服务调用

3、验证码调用

gulimall-auth-server

feign的接口调用,调用上面的微服务方法

@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
    
}

application.properties

spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=20000

spring.thymeleaf.cache=false

spring.redis.host=192.168.109.129
spring.redis.port=6379

controller,请求逻辑操作

@Controller
public class LoginController {

    @Autowired
    ThirdPartFeignService thirdPartFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 验证码
     * @param phone
     * @return
     */
    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        //TODO 1、接口防刷。


        String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);

        if (!StringUtils.isEmpty(redisCode)){
            long l = Long.parseLong(redisCode.split("_")[1]);
            if(System.currentTimeMillis() - l < 60000){   //当前系统时间 - 获取验证码时的时间  <  60
                //60秒内不能再发
                return  R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }


        //2、验证码的再次校验。redis。存key-phone  value-code     sms:code:12345678910 ->4567
        String code = UUID.randomUUID().toString().substring(0,5)+"_"+System.currentTimeMillis();

        //redis缓存验证码,防止同一个phone在60秒内再次发送验证码

        redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10, TimeUnit.MINUTES);

        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }

}

(二)注册功能

1、MD5&MD5盐值加密

MD5:

  • Message Digest algorithm 5,信息摘要算法
    • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
    • 容易计算:从原数据计算出MD5值很容易。
    • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
    • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
    • 不可逆

加盐:

  • 通过生成随机数与MD5生成字符串进行组合
  • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

实例:

    @Test
    void contextLoads() {
//        String s = DigestUtils.md5Hex("123456");

        //MD5不能直接进行密码的加密存储

        //盐值加密;随机值  加盐:$1$+8位字符
        //$1$vhYTUk3c$ZN00M9zNL0n8uZiQvYtLk1   123456
        //盐值: $1$vhYTUk3c 一样,  123456加密出来的密码也一样
        //验证: 123456进行盐值(去数据库查) 加密
        String s1 = Md5Crypt.md5Crypt("123456".getBytes(),"$1$vhYTUk3c");
        System.out.println(s1);

输出

$1$vhYTUk3c$ZN00M9zNL0n8uZiQvYtLk1

只要盐值: \(1\)vhYTUk3c 是一样的,在加密的时候, 同一个数据加的多次密,密码就是一样的

spring的MD5+盐值加密:

    @Test
    void contextLoads() {

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("123456");
        boolean matches = passwordEncoder.matches("123456", "$2a$10$ZpZVqw6qiR4FyalLPDHt2.xs.uZALykb/CCH1fXM4W5WweG9I/04i");

        System.out.println(encode+"=>"+matches);

    }

输出

$2a$10$.bPSXYRm0H779/IqnzdegeEDFs58py20v1Eie2LgZGaI5JkuRgAFC=>true

每次加密时,随机盐值,出来的密码结果也不一样

验证时,可通过spring的passwordEncoder这个方法解密;只需要传入 原密码 + 放在数据库的加密密码 匹配即可

返回true 或者 false

2、注册页面

reg.html 注册的html

		<header>
			<a href="http://gulimall.com" class="logo"><img src="E:/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>
		<section>
			<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>
			<div class="two">
			<div class="right_r">
				<div class="right_r1">
					<img src="E:/static/reg/img/a65a18e877a16246a92e1b755bd88a03_03.png"/>
					<span>企业用户注册</span>
				</div>
				<div class="right_r2">
					<img src="E:/static/reg/img/a65a18e877a16246a92e1b755bd88a03_06.png"/>
					<span>INTERNATIONAL <br /> CUSTOMERS</span>
				</div>
			</div>
			</div>
		</section>

异常错误的枚举类 package com.atguigu.common.exception包下BizCodeEnume.java

/***
 * 错误码和错误信息定义类
 * 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;
    }
}

提交表单 controller处理请求,返回数据

   /**
     *
     * //TODO 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面取出这个数据之后,session里面的数据就会删掉
     * //TODO 1、分布式下的session问题
     *
     * RedirectAttributes redirectAttributes  模拟重定向携带数据
     * @param vo
     * @param result
     * @param redirectAttributes
     * @return
     */
    //@Valid 标注这个方法需要校验     BindingResult result 校验的结果
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes){
        if(result.hasErrors()){


            /**
             * .map(fieldError -> {
             *                 String field = fieldError.getField();
             *                 String defaultMessage = fieldError.getDefaultMessage();
             *                 errors.put(field,defaultMessage);
             *
             *             })
             */
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));

            //model.addAttribute("errors",errors);
            redirectAttributes.addFlashAttribute("errors",errors);
            //校验出错,转发到注册页
            //Request method 'Post' not supported
            //用户注册-> /regist[post]  --->>转发/reg.html (路径映射默认都是get方式访问的。) ,所以这里不能用"forword:reg.html",需要用"reg"
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //真正注册。调用远程服务进行注册
        //1、校验验证码
        String code = vo.getCode();


        String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(s)){

            if (code.equals(s.split("_")[0])){
                //删除验证码;令牌机制
                redisTemplate.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);
                    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";
            }
        }else {
            Map<String, String> errors = new HashMap<>();
            errors.put("code","验证码错误");

            redirectAttributes.addFlashAttribute("errors",errors);
            //校验出错,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }


        //注册成功回到首页,回到登录页  //   /login的 /  代表以项目的域名为准/ +  login.html
        //return "redirect:/login.html";  //重定向

    }

远程调用接口

  • package com.atguigu.gulimall.auth.feign包 发送短信的接口
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);

}

具体逻辑看package com.atguigu.gulimall.thirdparty.controller.sendCode.java

  • package com.atguigu.gulimall.auth.feign包 发送请求数据,注册完成的接口
@FeignClient("gulimall-member")
public interface MemberFeignService {

    @PostMapping("/member/member/regist")
    R regist(@RequestBody UserRegistVo vo);
}

远程调用的member服务下的controller 发送请求数据,注册完成的controller

    @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();
    }

远程调用的member服务下的controller的memberService.regist的实现

   /**
     * 注册功能
     * @param vo
     */
    @Override
    public 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());

        entity.setNickname(vo.getUserName());

        //密码要进行加密存储。
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

        //其他的默认信息

        //保存
        memberDao.insert(entity);

    }

具体实现逻辑看package com.atguigu.gulimall.member.service.impl.MemberServiceImpl.java 源码

(三)登录功能

login.html登录html

					<form action="/login" method="post">
						<div style="color: red" th:text="${errors!=null?(#maps.containsKey(errors,'msg')?errors.msg:''):''}"></div>
					<ul>
						<li class="top_1">
							<img src="E:/static/login/JD_img/user_03.png" class="err_img1" />
							<input type="text" name="loginacct" placeholder=" 邮箱/用户名/已验证手机" class="user" />
						</li>
						<li>
							<img src="E:/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 class="btn2" type="submit"><a>登 &nbsp; &nbsp;录</a></button></li>
					</ul>
					</form>

登录的controller

    /**
     * 登录功能
     * @param vo
     * @param redirectAttributes
     * @return
     */
    @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";
        }


    }

远程调用,登录接口

package com.atguigu.gulimall.auth.feign包下接口

    @PostMapping("/member/member/login")
    R login(@RequestBody UserLoginVo vo);

member下的controller

    /**
     * 登录功能
     * @param vo
     * @return
     */
    //@RequestBody代表前面会穿一个json数据
    @PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo){
        MemberEntity entity = memberService.login(vo);
        if(entity!=null){
            //登录成功
            return R.ok().setData(entity);
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }


    }

具体实现逻辑package com.atguigu.gulimall.member.service.impl的login方法

后续会有session优化,详细看product,search,member,auth微服务相关注册登录的controller,service,以及html修改,pom依赖,application.properters配置

二、社交登陆&单点登陆

(一)、社交登陆

1、OAuth2.0

QQ、微博、github等网站的用户量非常大,别的网站为了简化自我网站的登陆与注册逻辑,引入社交登陆功能;步骤:
1)、用户点击QQ按钮

2)、引导跳转到QQ授权页

3)、用户主动点击授权,跳回之前网页。

1、OAuth2.0

  • OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。

  • OAuth2.0:对于用户相关的OpenAPl(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。

  • 官方版流程:

1、使用Code换取Access Token,Code只能用一次

2、同一个用户的accessToken一段时间是不会变化的,即使多次获取

2、微博登录准备工作

1、进入微博开放平台

2、登陆微博,进入微连接,选择网站接入

然后选择立即接入

填写上应用的网站名字--》进入页面后点击高级信息,填上OAuth2.0认证成功登录的页面和失败的页面

然后是操作步骤:

  • 1.引导需要授权的用户到如下地址:

URL

https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI

实例:这段代码可以放在点击微博登录跳转的链接:例如:

<li>
	<a href="https://api.weibo.com/oauth2/authorize?client_id=3521584417&response_type=code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success">
	<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png" />
	</a>
</li>
  • 2.如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE

实例:浏览器跳转页面

http://gulimall.com/oauth2.0/weibo/success?code=50d30c4a5b71cffcc79e0278647edc9c
  • 3.换取Access Token

URL

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

其中client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET可以使用basic方式加入header中,返回值

实例:

上面需要填写自己的client_id、client_secret、grant_type、redirect_uri、code

例如:

https://api.weibo.com/oauth2/access_token?client_id=3521584417&client_secret=123456789abcdefg&grant_type=authorization_code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success&code=50d30c4a5b71cffcc79e0278647edc9c

然后就会返回:

JSON

{
    "access_token": "2.00aaaacccxdcccccccccc",
    "remind_in": "157679d99",
    "expires_in": 157679d99,
    "uid": "573101886d",
    "isRealName": "true"
}
  • 4.使用获得的Access Token调用API

具体步骤参考官网[文档][https://open.weibo.com/wiki/首页]

3、项目社交登录

项目实例:

login.html的登录相关信息

<li>
							<a href="https://api.weibo.com/oauth2/authorize?client_id=3521584417&response_type=code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success">
								<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png" />
							</a>
						</li>

controller 社交登录成功回调

/**
 * 处理社交登录请求
 */
@Controller
@Slf4j
public class OAuth2Controller {

    @Autowired
    MemberFeignService memberFeignService;

    /**
     * 社交登录成功回调
     * @param code
     * @return
     * @throws Exception
     */
    @GetMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code) throws Exception {

        Map<String,String> header = new HashMap<>();
        Map<String,String> map = new HashMap<>();
        map.put("client_id","3521584416");
        map.put("client_secret","a6dc00a56e5ec9b05f2dfd103d9be612");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code",code);

        //1、根据code换取accessToken;
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", header, null, map);

        //2、处理
        if (response.getStatusLine().getStatusCode()==200){
            //获取到了accessToken
            String json = EntityUtils.toString(response.getEntity());
            SocialUserVo socialUserVo = JSON.parseObject(json, SocialUserVo.class);  //利用fastJson把json转为实体类

            //知道当前是哪个社交用户
            //1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户用户一个会员信息账号,以后这个社交账号就对应指定的会员)
            //登录或者注册这个社交用户
            R oauthlogin = memberFeignService.oauthlogin(socialUserVo);
            if(oauthlogin.getCode() == 0){

                MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
                });

                System.out.println("登陆成功: 用户信息" + data);
                log.info("登陆成功:用户:{}",data.toString());
                //2、登陆成功,跳回首页
                return "redirect:http://gulimall.com";
            }else {
                return "redirect:http://auth.gulimall.com/login.html";
            }
        }else{
            return "redirect:http://auth.gulimall.com/login.html";
        }

    }
}

具体逻辑参考package com.atguigu.gulimall.auth.controller.OAuth2Controller.java

远程调用 feign 接口

@FeignClient("gulimall-member")
public interface MemberFeignService {

    @PostMapping("/member/member/oauth2/login")
    R oauthlogin(@RequestBody SocialUserVo socialUserVo) throws Exception;
}

远程调用的member下的controller

    /**
     * 社交登录
     * @param socialUserVo
     * @return
     * @throws Exception
     */
    @PostMapping("/oauth2/login")
    public R oauthlogin(@RequestBody SocialUserVo socialUserVo) throws Exception {
        MemberEntity entity = memberService.login(socialUserVo);
        if (entity != null) {
            //登录成功
            return R.ok().setData(entity);
        } else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }

    }

远程调用的member下的controller的调用memberService.login的实现方法memberServiceImpl.java

    /**
     * 社交账号登录
     * @param socialUserVo
     * @return
     */
    @Override
    public MemberEntity login(SocialUserVo socialUserVo) throws Exception {
        //登录和注册合并逻辑
        String uid = socialUserVo.getUid();
        //1、判断当前社交用户是否已经登陆过系统
        MemberDao memberDao = this.baseMapper;
        MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
        if(memberEntity != null){

            System.out.println("这个用户已经注册过了");

            //这个用户已经注册过了
            MemberEntity update = new MemberEntity();
            update.setId(memberEntity.getId());
            update.setAccessToken(socialUserVo.getAccess_token());
            update.setExpiresIn(socialUserVo.getExpires_in());

            memberDao.updateById(update);

            memberEntity.setAccessToken(socialUserVo.getAccess_token());
            memberEntity.setExpiresIn(socialUserVo.getExpires_in());
            return memberEntity;
        }else {
            System.out.println("没有查到当前社交用户信息,需要注册");

            //2、没有查到当前社交用户对应的记录我们就需要注册一个
            MemberEntity regist = new MemberEntity();
            try{
            //3、查询当前社交用户的社交账号信息(昵称,性别等)
            Map<String,String> query = new HashMap<>();
            query.put("access_token",socialUserVo.getAccess_token());
            query.put("uid",socialUserVo.getUid());
            HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String, String>(), query);

            if (response.getStatusLine().getStatusCode() == 200) {
                //查询成功
                String json = EntityUtils.toString(response.getEntity());
                JSONObject jsonObject = JSON.parseObject(json);//利用jaonfast  把json转为Object
                //昵称
                String name = jsonObject.getString("name");
                //性别
                String gender = jsonObject.getString("gender");

                System.out.println("查询到的社交账户昵称:" + name + "  性别:" + "gender");

                //......
                regist.setNickname(name);
                regist.setGender("m".equals(gender) ? 1 : 0);
                //......
            }
            }catch (Exception e){

            }
            regist.setSocialUid(socialUserVo.getUid());
            regist.setAccessToken(socialUserVo.getAccess_token());
            regist.setExpiresIn(socialUserVo.getExpires_in());
            memberDao.insert(regist);

            return regist;
        }

    }

具体逻辑参考package com.atguigu.gulimall.member.service.impl.memberServiceImpl.java

三、分布式session共享

1、分布式session不共享问题

2、session共享问题解决

1、同一个服务,session共享

同一个域名下的集群搭建,即同一个服务,复制多份

方案一:

方案二:

方案三:

方案4:

2、不同服务,子域session共享

不同域名的session共享

方案一:

//1、第一次使用session,命令浏览器保存卡号。JSESSIONID这个cookie;
//以后浏览器访问哪个网站就会带上这个网站的cookie;
//子域之间;gulimall.com(指定域名为父域名)  auth.gulimall.com    order.gulimall.com
//在发卡的时候,都扩大到父域名,这样即使是子域系统发的卡,也能让父域直接使用。
项目实例

**package com.atguigu.gulimall.auth的配置 **

域名auth.gulimall.com

pom.xml 依赖

        <!--  1、 整合SpringSession完成session共享问题     -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

application.properties

spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=20000

spring.thymeleaf.cache=false
spring.redis.host=192.168.109.129

spring.redis.port=6379

# session使用redis
spring.session.store-type=redis
# session超时时间  30分钟
server.servlet.session.timeout=30m

配置类

自定义配置session的域访问,和cookie名字,以及储存session到redis序列化为json

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){

        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setDomainName("gulimall.com");
        defaultCookieSerializer.setCookieName("GULISESSION");


        return defaultCookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

启动类加注解@EnableRedisHttpSession //整合redis作为session存储

储存session的的Vo实体类需要各个微服务都可以访问到,放到common里,需要加上implements Serializable,可以序列化

package com.atguigu.common.vo;
@ToString
@Data
public class MemberRespVo implements Serializable {

    /**
     * id
     */
    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;

    /**
     * 社交用户唯一uid
     */
    private String socialUid;

    /**
     * 社交登录访问令牌
     */
    private String accessToken;

    /**
     * 社交访问令牌过期时间
     */
    private Long expiresIn;
}

controller的不用社交账号,自己注册的登录,登录成功后把从远程member查到的login用户信息放到session中

    /**
     * 登录功能
     * @param vo
     * @param redirectAttributes
     * @return
     */
    @PostMapping("/login")
    public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session){

        //远程登录
        R login = memberFeignService.login(vo);
        if (login.getCode() == 0){

            MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
            });
            //成功放到session中
            session.setAttribute(AuthServerConstant.LOGIN_USER,data);
            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";
        }

controller用社交账户登录

    /**
     * 社交登录成功回调
     * @param code
     * @return
     * @throws Exception
     */
    @GetMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code, HttpSession session, HttpServletResponse servletresponse, HttpServletRequest request) throws Exception {

        Map<String,String> header = new HashMap<>();
        Map<String,String> map = new HashMap<>();


        map.put("client_id","3521584416");
        map.put("client_secret","a6dc00a56e5ec9b05f2dfd103d9be612");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code",code);

        //1、根据code换取accessToken;
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", header, null, map);

        //2、处理
        if (response.getStatusLine().getStatusCode()==200){
            //获取到了accessToken
            String json = EntityUtils.toString(response.getEntity());
            SocialUserVo socialUserVo = JSON.parseObject(json, SocialUserVo.class);  //利用fastJson把json转为实体类

            //知道当前是哪个社交用户
            //1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户用户一个会员信息账号,以后这个社交账号就对应指定的会员)
            //登录或者注册这个社交用户
            R oauthlogin = memberFeignService.oauthlogin(socialUserVo);
            if(oauthlogin.getCode() == 0){

                MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
                });

                System.out.println("登陆成功: 用户信息" + data);
                log.info("登陆成功:用户:{}",data.toString());
                //1、第一次使用session,命令浏览器保存卡号。JSESSIONID这个cookie;
                //以后浏览器访问哪个网站就会带上这个网站的cookie;
                //子域之间;gulimall.com(指定域名为父域名)  auth.gulimall.com    order.gulimall.com
                //在发卡的时候,即使是子域系统发的卡,也能让父域直接使用。
                //TODO 1、默认发的令牌。session=dsajkdjl。作用域:当前域;(解决子域session共享问题)
                //TODO 2、使用JSON的序列化方式序列化对象数据到redis中
                session.setAttribute("loginUser",data);
//                new Cookie("JSESSIONID","dadaa").setDomain("");
//                servletresponse.addCookie();
                //2、登陆成功,跳回首页
                return "redirect:http://gulimall.com";
            }else {
                return "redirect:http://auth.gulimall.com/login.html";
            }
        }else{
            return "redirect:http://auth.gulimall.com/login.html";
        }

    }

package com.atguigu.gulimall.product的配置

域名 gulimall.com

pom.xml依赖

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

application.properties配置同上

配置类:

自定义配置session的域访问,和cookie名字,以及储存session到redis序列化为json

package com.atguigu.gulimall.product.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){

        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setDomainName("gulimall.com");
        defaultCookieSerializer.setCookieName("GULISESSION");


        return defaultCookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

启动类加注解@EnableRedisHttpSession //整合redis作为session存储

首页 index.html

<li>
            <a href="http://auth.gulimall.com/login.html">你好,请登录[[${session.loginUser==null?'':session.loginUser.nickname}]]</a>
          </li>

访问:http://auth.gulimall.com/login.html 微博授权登录后,

会跳转首页:http://gulimall.com/ 并且会带上微博的昵称, 这就共享session了

详细看源码:package com.atguigu.gulimall.auth.controller.OAuth2Controller.java 社交登录

3、SpringSession核心原理

SessionRepositoryFilter

 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
     
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
     	
     //包装原始请求对象
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response); 
     
     //包装原始响应对象
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

     
        try {
            //包装后的对象应用到了我们后面的整个执行链
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();
        }

    }
* 核心原理
* 1)、@EnableRedisHttpSession导入了 RedisHttpSessionConfiguration配置
*      1、给容器中添加了一个组件
*          SessionRepository=》》》【RedisIndexedSessionRepository】==》redis操作session。 session的增删改查的封装类
*      2、SessionRepositoryFilter==》Filter : session存储过滤器; 每个请求过来都必须经过filter
*          1、创建的时候,就自动从容器中获取到了SessionRepository;
*          2、原生的request,都被包装。 SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
*          3、以后获取session。 request.getSession();
*          //SessionRepositoryRequestWrapper
*          4、wrappedRequest.getSession(); ===》 SessionRepository 获取到的。
*   装饰者模式;
*
*   自动延期; redis的数据也是有过期时间的

根据SpringSession,前端的注册登录还有一点优化,详细看product,search,member,auth微服务相关注册登录的controller,service,以及html修改,pom依赖,application.properters配置;

主要是把auth的相关注册登录依赖:application.properters配置,以及pom依赖、config配置类 复制下导入search、product下,然后修改登录注册页面的html

html修改,因为配置好后,都可以共享父类http://gulimall.com的session,所以只有登录了就可以取出数据

<li style="border: 0;">
    
	<a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}" class="aa">你好,请登录</a>
    
	<a th:if="${session.loginUser!=null}" class="aa" style="width: 100px">[[${session.loginUser.nickname}]]</a>
</li>
<li>
    <a th:if="${session.loginUser==null}" href="http://auth.gulimall.com/reg.html" style="color: red;">免费注册</a> 
|</li>

四、单点登录

具体看package com.atguigu.gulimall.ssoclient、package com.atguigu.gulimall.ssoserver

posted on 2023-03-08 12:31  共感的艺术  阅读(93)  评论(0编辑  收藏  举报