谷粒商城项目篇11_分布式高级篇_认证服务(腾讯云短信费服务、Gitee社交账号登录)、分布式Session问题

目录

  1. 认证服务
    • 创建微服务模块
    • 阿里云短信验证码服务
    • 整合代码测试
    • 验证码的再次校验
    • 注册完成登录
      • 普通账号密码登录
      • 社交登录
      • 微博登录(码云登录)
    • 分布式Session问题
  2. 分布式Session
    • 整合SpringSession
    • 使用json序列化方式
    • 解决Session共享域
  3. 单点登录SSO概念

一、认证服务

1.创建微服务模块auth-server

该微服务为web服务,完成注册中心配置,以后的继承第三方登录、单点登录都由该模块来完成。
在这里插入图片描述

整合静态资源页面

  • 导入html和样式资源
  • 配置本机域名映射
  • 配置网关路由
  • 编写各个html之间的跳转逻辑
  • 利用SpringMVC的视图控制器完成页面跳转
package henu.soft.xiaosi.authserver.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyWebConfig implements WebMvcConfigurer {


    /**
     * 视图映射,控制页面跳转
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");

    }
}

在这里插入图片描述

2.阿里云短信验证码服务

阿里短信服务地址:免费100条

在这里插入图片描述

API:使用指南

在这里插入图片描述

3.整合代码测试

短信服务SDK,因为阿里云短信模板一直审核不通过,无奈转置腾讯云

1.阿里云旧版SDK
  • 创建HttpUtils
  • 抽取公共组件原生使用举栗
  • 参考:https://help.aliyun.com/document_detail/112148.html?spm=a2c4g.11186623.6.670.23f55695lsjIYu

package henu.soft.xiaosi.authserver.component;

import henu.soft.xiaosi.authserver.util.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;



@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data
@Component
public class SmsCode {

    private String host;
    private String path;
    private String skin;
    private String sign;
    private String appcode;

    public void sendCode(String phone, String code) {
        String method = "GET";
        Map<String, String> headers = new HashMap<>();
        // 最后在header中的格式(中间是英文空格)为 Authorization:APPCODE 93b7e19861a24c519a7548b17dc16d75
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> queries = new HashMap<String, String>();
        queries.put("code", code);
        queries.put("phone", phone);
        queries.put("skin", skin);
        queries.put("sign", sign);
        //JDK 1.8示例代码请在这里下载:  http://code.fegine.com/Tools.zip
        try {
            HttpResponse response = HttpUtils.doGet(host, path, method, headers, queries);
            //System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。
            //状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
            //获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

alicloud:
      sms:
        host: https://fesms.market.alicloudapi.com
        path: /sms/
        skin: 1
        sign: 175622
        appcode: 93b7e19861a24c519a7548b17dc16d75 # 新版SDK已经取消

测试

@Test
    public void sendSmsCode() {
        smsComponent.sendCode("13838383838", "134531");
    }
2.阿里云新版SDK
  • 导依赖
<!--        阿里云短信-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-openapi</artifactId>
            <version>0.0.19</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dysmsapi20170525</artifactId>
            <version>2.0.4</version>
        </dependency>
  • 登录控制台配置短信模板、accessKeyId、accessKeySecret

在这里插入图片描述

  • 封装组件
package henu.soft.xiaosi.thirdparty.component;

import com.alibaba.fastjson.JSON;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.QuerySendDetailsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody;
import com.aliyun.teaopenapi.models.Config;
import henu.soft.common.utils.R;
import io.prometheus.client.Collector;
import org.springframework.stereotype.Component;


/**
 * 阿里云短信新版SDK,封装组件
 */
@Component
public class NewSmsCode {
    /**
     * 使用AK&SK初始化账号Client
     * @param accessKeyId
     * @param accessKeySecret
     * @return Client
     * @throws Exception
     */
    public com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
        Config config = new Config()
                // 您的AccessKey ID
                .setAccessKeyId(accessKeyId)
                // 您的AccessKey Secret
                .setAccessKeySecret(accessKeySecret);
        // 访问的域名
        config.endpoint = "dysmsapi.aliyuncs.com";
        return new com.aliyun.dysmsapi20170525.Client(config);
    }

    public R newSdkSendCode(String phone, String code) throws Exception {
        Client client = createClient("xxx", "xxx");
        /*
        QuerySendDetailsRequest querySendDetailsRequest = new QuerySendDetailsRequest()
                .setResourceOwnerAccount("1")
                .setResourceOwnerId(1L)
                .setPhoneNumber("17637821720")
                .setBizId("1")
                .setSendDate("xiaosi");
        // 复制代码运行请自行打印 API 的返回值
        client.querySendDetails(querySendDetailsRequest);

        */

        SendSmsRequest request = new SendSmsRequest();
        request.setPhoneNumbers(phone);
        request.setTemplateCode("xxx需要申请,不太好通过");
        request.setTemplateParam(code);
        request.setSignName("阿里云");

        SendSmsResponse sendSmsResponse = client.sendSms(request);

        SendSmsResponseBody body = sendSmsResponse.getBody();
        String s = JSON.toJSONString(body);
        System.out.println(s);

        if (sendSmsResponse.body.code.equals("200") ){

            return R.ok();
        }
        else return R.error("验证码发送失败!");


    }


}

3.腾讯云SDK

基本步骤

  • 配置签名模板
  • 配置AccessKey、AccessSecret
  • 导依赖
  • 使用
  • API参考:https://cloud.tencent.com/document/api/382/55981
  • SDK参考:https://cloud.tencent.com/document/product/382/43194
  • 编码参考:https://www.jb51.net/article/216190.htm

在这里插入图片描述
在这里插入图片描述

   <!-- 腾讯云 Java SDK 依赖 -->
        <dependency>
            <groupId>com.tencentcloudapi</groupId>
            <artifactId>tencentcloud-sdk-java</artifactId>
            <version>3.1.297</version>
        </dependency>

工具类,待会在此基础上封装一个组件使用

package henu.soft.xiaosi.thirdparty.util;


import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import henu.soft.xiaosi.thirdparty.config.SmsConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



/**
 *
 * @description 腾讯云短信工具类
 * @date 2021/8/15 16:21
 */
public class SmsUtil {



    private static final Logger LOGGER = LoggerFactory.getLogger(SmsUtil.class);

    /**
     * 发送短信
     *
     * @param smsConfig      腾讯云短信配置对象
     * @param templateParams 模板参数
     * @param phoneNumbers   手机号数组
     * @return SendStatus[],短信发送状态
     */
    public static SendStatus[] sendSms(SmsConfig smsConfig, String[] templateParams, String[] phoneNumbers) {
        try {
            // 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
            Credential cred = new Credential(smsConfig.getSecretId(), smsConfig.getSecretKey());
            // 实例化一个http选项,可选,没有特殊需求可以跳过
            HttpProfile httpProfile = new HttpProfile();
            // SDK默认使用POST方法
            httpProfile.setReqMethod("POST");
            // SDK有默认的超时时间,非必要请不要进行调整
            httpProfile.setConnTimeout(60);
            // 非必要步骤:实例化一个客户端配置对象,可以指定超时时间等配置
            ClientProfile clientProfile = new ClientProfile();
            // SDK默认用TC3-HMAC-SHA256进行签名,非必要请不要修改这个字段
            clientProfile.setSignMethod("HmacSHA256");
            clientProfile.setHttpProfile(httpProfile);
            // 实例化要请求产品(以sms为例)的client对象,第二个参数是地域信息,可以直接填写字符串ap-guangzhou,或者引用预设的常量
            SmsClient smsClient = new SmsClient(cred, "ap-guangzhou", clientProfile);


            // 实例化一个请求对象
            SendSmsRequest req = new SendSmsRequest();
            // 设置短信应用ID:短信SdkAppId在[短信控制台]添加应用后生成的实际SdkAppId
            req.setSmsSdkAppId(smsConfig.getAppId());
            // 设置短信签名内容:使用UTF-8编码,必须填写已审核通过的签名,签名信息可登录[短信控制台]查看
            req.setSignName(smsConfig.getSign());
            // 设置国际/港澳台短信SenderId:国内短信填空,默认未开通
            req.setSenderId("");

            // 设置模板ID:必须填写已审核通过的模板ID。模板ID可登录[短信控制台]查看
            req.setTemplateId(smsConfig.getTemplateId());
            // 设置下发手机号码,采用E.164标准,+[国家或地区码][手机号]
            req.setPhoneNumberSet(phoneNumbers);

            // 设置模板参数:若无模板参数,则设置为空
            req.setTemplateParamSet(templateParams);

            // 通过client对象调用SendSms方法发起请求。注意请求方法名与请求对象是对应的,返回的res是一个SendSmsResponse类的实例,与请求对象对应
            SendSmsResponse res = smsClient.SendSms(req);
            // 控制台打印日志输出json格式的字符串回包
            LOGGER.info(SendSmsResponse.toJsonString(res));
            return res.getSendStatusSet();
        } catch (TencentCloudSDKException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }


}


配置对应实体类

package henu.soft.xiaosi.thirdparty.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author rtxtitanv
 * @version 1.0.0
 * @name com.rtxtitanv.config.SmsConfig
 * @description 腾讯云短信配置类
 * @date 2021/6/25 16:21
 */
@ConfigurationProperties(prefix = "tencent.sms")
@Configuration
@Data
public class SmsConfig {
    /**
     * 腾讯云API密钥的SecretId
     */
    private String secretId;
    /**
     * 腾讯云API密钥的SecretKey
     */
    private String secretKey;
    /**
     * 短信应用的SDKAppID
     */
    private String appId;
    /**
     * 签名内容
     */
    private String sign;
    /**
     * 模板ID
     */
    private String templateId;
    /**
     * 过期时间
     */
    private String expireTime;

}

  # 自定义腾讯云短信配置
tencent:
  sms:
    # 配置腾讯云API密钥的SecretId
    secretId: xxx
    # 配置腾讯云API密钥的SecretKey
    secretKey: xxxx
    # 配置短信应用的SDKAppID
    appId: 1400xxxx
    # 配置签名内容
    sign: "xxx"
    # 配置模板ID
    templateId: xxx

组件

package henu.soft.xiaosi.thirdparty.component.tencent;

import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import henu.soft.common.utils.R;
import henu.soft.xiaosi.thirdparty.config.SmsConfig;
import henu.soft.xiaosi.thirdparty.util.SmsUtil;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 腾讯云短信服务,发送验证码的组件
 */

@Component
public class SendSmsCode {

    @Resource
    SmsConfig smsConfig;

    public R sendSmsCode(String phoneNumber, String code) {
        // 下发手机号码,采用e.164标准,+[国家或地区码][手机号]
        String[] phoneNumbers = {"+86" + phoneNumber};
        // 生成6位随机数字字符串

        // 模板参数:若无模板参数,则设置为空(参数1为随机验证码,参数2为有效时间)
        String[] templateParams = {code};
        // 发送短信验证码

        SendStatus[] sendStatuses = SmsUtil.sendSms(smsConfig, templateParams, phoneNumbers);
        if ("Ok".equals(sendStatuses[0].getCode())) {

            return R.ok();
        } else {
            return R.error(sendStatuses[0].getMessage());
        }
    }
}

测试

 @Autowired
    SendSmsCode sendSmsCode;

    @Test
    void testSendSmsCode(){
        System.out.println(sendSmsCode.sendSmsCode("17637821720"));

    }

效果
请添加图片描述

请求验证码组件放到第三方服务中,供其他微服务模块调用api

4.验证码的再次校验

下面js代码请求第auth-server服务模块,然后在调用第三方微服务模块发送验证码

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

验证码的验证和60s只能获取一次 都需要使用redis完成,首先将redis配置好

package henu.soft.xiaosi.authserver.web;

import com.alibaba.cloud.commons.lang.StringUtils;
import henu.soft.common.constant.AuthServerConstant;
import henu.soft.common.exception.BizCodeEnume;
import henu.soft.common.utils.R;
import henu.soft.xiaosi.authserver.feign.ThirdPartFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Controller
public class LoginController {

    @Autowired
    ThirdPartFeignService thirdPartFeignService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;


    @GetMapping("/sms/sendcode")
    @ResponseBody
    public R sendCode(@RequestParam("phone") String phone) {

        //1、接口防刷
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if (!StringUtils.isEmpty(redisCode)) {
            //活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
            long currentTime = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis() - currentTime < 60000) {
                //60s内不能再发
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }

        //2、验证码的再次效验 redis.存key-phone,value-code
        int code = (int) ((Math.random() * 9 + 1) * 100000);
        String codeNum = String.valueOf(code);
        String redisStorage = codeNum + "_" + System.currentTimeMillis();

        //存入redis,防止同一个手机号在60秒内再次发送验证码
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone,
                redisStorage, 10, TimeUnit.MINUTES);

        return thirdPartFeignService.sendCode(phone, codeNum);


    }


}

1.调用远程会员微服务注册

然后校验表单内容、校验验证码、远程调用会员微服务校验账户

  • 成功,跳转登录页面
  • 失败,重定向携参到注册页面,显示校验失败的信息(这里原理是session,后续需要处理分布式session)
/**
     * TODO: 重定向携带数据:RedirectAttributes,利用session原理,将数据放在session中。
     * TODO:只要跳转到下一个页面取出这个数据以后,session里面的数据就会删掉
     * TODO:分布下session问题
     * RedirectAttributes:重定向也可以保留数据,不会丢失
     * 用户注册
     *
     * @return
     */
    @PostMapping("/register")
    public String register(@Valid UserRegisterVo vos, BindingResult result, RedirectAttributes attributes) {
        //如果有错误回到注册页面
        if (result.hasErrors()) {
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));

            // 模拟重定向携带数据
            attributes.addFlashAttribute("errors", errors);
            //效验出错回到注册页面
            return "redirect:http://auth.gulishop.cn/reg.html";
        }

        //1、效验验证码
        String code = vos.getCode();

        //获取存入Redis里的验证码
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vos.getPhone());
        if (!StringUtils.isEmpty(redisCode)) {
            //截取字符串
            if (code.equals(redisCode.split("_")[0])) {
                //删除验证码;令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vos.getPhone());
                //验证码通过,真正注册,调用远程服务进行注册
                R register = memberFeignService.register(vos);
                if (register.getCode() == 0) {
                    //成功
                    return "redirect:http://auth.gulishop.cn/login.html";
                } else {
                    //失败
                    Map<String, String> errors = new HashMap<>();
                    TypeReference<String> typeReference = new TypeReference<String>() {
                    };
                    String msg = register.getData(typeReference);
                    errors.put("msg",msg );
                    attributes.addFlashAttribute("errors", errors);
                    return "redirect:http://auth.gulishop.cn/reg.html";
                }
            } else {
                //效验出错回到注册页面
                Map<String, String> errors = new HashMap<>();
                errors.put("code", "验证码错误");
                attributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulishop.cn/reg.html";
            }
        } else {
            //效验出错回到注册页面
            Map<String, String> errors = new HashMap<>();
            errors.put("code", "验证码错误");
            attributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulishop.cn/reg.html";
        }
    }
2.远程服务密码MD5盐值加密

MD5加密:信息摘要算法

  • 压缩性:任意长度的数据,算出的MD5值长度都是固定的
  • 容易计算
  • 抗修改性
  • 强碰撞性
  • 不可逆

MD5存储有可能会被暴力破解,因此需要加盐值

  • 通过生成的随机数与MD5生成字符串进行组合 将密码 和 指定盐 一块MD5加密,数据库要保存加密的内容 和 盐值,以便登录验证
  • 数据库同时存储MD5值与slat值,验证正确性时使用salt进行MD5即可
  • Spring提供的 BCryptPasswordEncoder提供盐值加密
    在这里插入图片描述
3.远程会员微服务保存注册信息

远程member微服务,校验用户名、手机号是否存在,完成账号信息的保存

 @Autowired
    MemberLevelService memberLevelService;

    
    @Override
    public void register(MemberRegisterVo registerVo) {
        //1 检查电话号是否唯一
        checkPhoneUnique(registerVo.getPhone());
        //2 检查用户名是否唯一
        checkUserNameUnique(registerVo.getUserName());
        //3 该用户信息唯一,进行插入
        MemberEntity entity = new MemberEntity();
        //3.1 保存基本信息
        entity.setUsername(registerVo.getUserName());
        entity.setMobile(registerVo.getPhone());
        entity.setCreateTime(new Date());
        //3.2 使用加密保存密码
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encodePassword = passwordEncoder.encode(registerVo.getPassword());
        entity.setPassword(encodePassword);
        //3.3 设置会员默认等级
        //3.3.1 找到会员默认登记
        MemberLevelEntity defaultLevel = memberLevelService.getOne(new QueryWrapper<MemberLevelEntity>().eq("default_status", 1));
        //3.3.2 设置会员等级为默认
        entity.setLevelId(defaultLevel.getId());

        // 4 保存用户信息
        this.save(entity);
    }
    private void checkUserNameUnique(String userName) {
        Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
        if (count > 0) {
            throw new UserExistException();
        }
    }

    private void checkPhoneUnique(String phone) {
        Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (count > 0) {
            throw new PhoneNumExistException();
        }
    }
5.注册完成登录
1.普通账号密码登录

auth-server微服务模块

/**
     * 登录,查询用户信息
     */
    @RequestMapping("/login")
    public String login(UserLoginTo vo, RedirectAttributes attributes, HttpSession session){
        R r = memberFeignService.login(vo);
        if (r.getCode() == 0) {
            String jsonString = JSON.toJSONString(r.get("memberEntity"));
            MemberResponseTo memberResponseTo = JSON.parseObject(jsonString, new TypeReference<MemberResponseTo>() {
            });
            session.setAttribute(AuthServerConstant.LOGIN_USER, memberResponseTo);
            return "redirect:http://gulimall.com/";
        }else {
            String msg = (String) r.get("msg");
            Map<String, String> errors = new HashMap<>();
            errors.put("msg", msg);
            attributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulishop.cn/login.html";
        }
    }

调用远程member微服务模块

 /**
     * 登录,查询用户信息
     */


    @RequestMapping("/login")
    public R login(@RequestBody UserLoginTo loginVo) {
        MemberEntity entity = memberService.login(loginVo);
        if (entity != null) {
            return R.ok().put("memberEntity", entity);
        } else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
        }
    }
/**
     * 登录
     * @param loginVo
     * @return
     */
    @Override
    public MemberEntity login(UserLoginTo loginVo) {
        String loginAccount = loginVo.getLoginAccount();
        //以用户名或电话号登录的进行查询
        MemberEntity entity = this.getOne(new QueryWrapper<MemberEntity>().eq("username", loginAccount).or().eq("mobile", loginAccount));
        if (entity != null) {
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            boolean matches = bCryptPasswordEncoder.matches(loginVo.getPassword(), entity.getPassword());
            if (matches) {
                entity.setPassword("");
                return entity;
            }
        }
        return null;
    }
2.社交登录OAuth2.0

概念

  • OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储
    在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们
    数据的所有内容。
  • OAuth2.0:对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分
    享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向
    用户征求授权

逻辑

(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源

在这里插入图片描述

3.微博登录(码云登录)
  • 参考文档:https://gitee.com/api/v5/oauth_doc#/
1.基本逻辑

在这里插入图片描述
在这里插入图片描述

操作之前需要完成开发者身份验证,微博审核速度较慢,因此使用gitee来第三方登录

在这里插入图片描述

在这里插入图片描述

点击模拟登陆,修改html按钮的跳转授权地址:

https://gitee.com/oauth/authorize?client_id=40bfef56bc4f1ba0749ac79a4186ff3b9f2c08915ffa7e473f66a60b6f194886&redirect_uri=http%3A%2F%2Fgulishop.cn%2Fsuccess&response_type=code

在这里插入图片描述

授权登录之后会跳转到:http://gulishop.cn/success?code=aab8ac8b968300cc064f5ef23b1561676e9dafa2f30b2f34be961e0af3de5ef5

需要将获得的code换取token

在这里插入图片描述

拿着token获取用户数据

在这里插入图片描述

2.代码编写

首先数据库以及对应实体需要增加字段

在这里插入图片描述
在这里插入图片描述

3.autu-server微服务模块
  • 根据code发送post请求获取token
  • 根据token获取用户唯一标识id(对应数据库字段social_uid)
  • 将token和id封装实体,调用远程member会员服务。
package henu.soft.xiaosi.authserver.web;


import com.alibaba.cloud.commons.lang.StringUtils;
import henu.soft.common.constant.AuthServerConstant;
import henu.soft.common.to.MemberResponseTo;

import henu.soft.common.utils.HttpUtil;
import henu.soft.common.utils.R;
import henu.soft.xiaosi.authserver.feign.MemberFeignService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;


import henu.soft.common.to.SocialUserTo;
import jdk.jfr.ContentType;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;


import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

/**
 * 处理社交登录
 */

@Controller
public class OAuth2Controller {


    @Autowired
    private MemberFeignService memberFeignService;
    //http://auth.gulishop.cn/oauth2.0/gitee/success
    @RequestMapping("/oauth2.0/gitee/success")
    public String authorize(@RequestParam("code") String code, HttpSession session) throws Exception {
        //1. 使用code换取token,换取成功则继续2,否则重定向至登录页
        // http://gulishop.cn/oauth2.0/gitee/success?code=5cf1dab80d4b62fca4886c05fc298fe01c16efc5b663a057aa256c1bf5389e96
        Map<String, String> query = new HashMap<>();
        query.put("client_id", "40bfef56bc4f1ba0749ac79a4186ff3b9f2c08915ffa7e473f66a60b6f194886");
        query.put("client_secret", "a3fd669d30272b2e051e12017297189cd4a3944cb48227c8c466f967a302c93b");
        query.put("grant_type", "authorization_code");
        query.put("redirect_uri", "http://auth.gulishop.cn/oauth2.0/gitee/success");
        query.put("code", code);

        //发送post请求换取token
        // https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}
        HttpResponse response = HttpUtil.doPost("https://gitee.com", "/oauth/token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
        Map<String, String> errors = new HashMap<>();
        if (response.getStatusLine().getStatusCode() == 200) {
            //2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页
            String json = EntityUtils.toString(response.getEntity());
            SocialUserTo socialUserTo = JSON.parseObject(json, new TypeReference<SocialUserTo>() {
            });
            // 拿着accessToken查询用户信息
            if (socialUserTo != null && (!StringUtils.isEmpty(socialUserTo.getAccess_token()))) {

                Map<String, String> queryAccessToken = new HashMap<>();
                queryAccessToken.put("access_token", socialUserTo.getAccess_token());

                Map<String, String> queryHeaders = new HashMap<>();
                queryHeaders.put("Content-Type", "application/json;charset=UTF-8");

                HttpResponse response1 = HttpUtil.doGet("https://gitee.com", "/api/v5/user", "get", queryHeaders, queryAccessToken);
                if (response1.getStatusLine().getStatusCode() == 200) {
                    String json1 = EntityUtils.toString(response1.getEntity());

                    // 获取user_info的id
                    SocialUserTo socialUserTo1 = JSON.parseObject(json1, new TypeReference<SocialUserTo>() {
                    });
                    socialUserTo1.setAccess_token(socialUserTo.getAccess_token());
                    socialUserTo1.setExpires_in(socialUserTo.getExpires_in());

                    // TODO 社交账号登录和注册为一体
                    R login = memberFeignService.oauthLogin(socialUserTo1);
                    //2.1 远程调用成功,返回首页并携带用户信息
                    if (login.getCode() == 0) {
                        String jsonString = JSON.toJSONString(login.get("memberEntity"));
                        System.out.println("----------------" + jsonString);
                        MemberResponseTo memberResponseTo = JSON.parseObject(jsonString, new TypeReference<MemberResponseTo>() {
                        });
                        System.out.println("----------------" + memberResponseTo);
                        session.setAttribute(AuthServerConstant.LOGIN_USER, memberResponseTo);
                        return "redirect:http://gulishop.cn";
                    } else {
                        //2.2 否则返回登录页
                        errors.put("msg", "登录失败,请重试");
                        session.setAttribute("errors", errors);
                        return "redirect:http://auth.gulishop.cn/login.html";
                    }
                } else {
                    errors.put("msg", "获得第三方授权失败,请重试");
                    session.setAttribute("errors", errors);
                    return "redirect:http://auth.gulishop.cn/login.html";
                }
            }

        }
        errors.put("msg", "获得第三方授权失败,请重试");
        session.setAttribute("errors", errors);
        return "redirect:http://auth.gulishop.cn/login.html";


    }
}


4.member微服务模块

判断第三方账号是不是第一次登录

  • 注册
  • 更新信息
/**
     * 社交账号登陆、注册合并
     *auth-server传递的 SocialUserTo 包含
     * - id:第三方用户唯一标识
     * - token:令牌
     * - expires_in : 失效时间
     * - name : 用户昵称
     * - avatar_url: 用户头像
     * - 
     * @param
     * @return
     */
    @Override
    public MemberEntity oauthLogin(SocialUserTo socialUserTo) {
        MemberEntity uid = this.getOne(new QueryWrapper<MemberEntity>().eq("social_uid", socialUserTo.getId()));
        //1 如果之前未登陆过,则查询其社交信息进行注册
        if (uid == null) {

            //调用微博api接口获取用户信息
            String json = null;
            try {

                Map<String, String> queryAccessToken = new HashMap<>();
                queryAccessToken.put("access_token", socialUserTo.getAccess_token());

                Map<String, String> queryHeaders = new HashMap<>();
                queryHeaders.put("'Content-Type", "application/json;charset=UTF-8");

                HttpResponse response1 = HttpUtil.doGet("https://gitee.com", "/api/v5/user", "get", queryAccessToken, queryHeaders);

                json = EntityUtils.toString(response1.getEntity());
            } catch (Exception e) {
                e.printStackTrace();
            }

            //封装用户信息并保存
            uid = new MemberEntity();
            uid.setAccessToken(socialUserTo.getAccess_token());
            uid.setSocialUid(socialUserTo.getId());
            uid.setExpiresIn(socialUserTo.getExpires_in());
            MemberLevelEntity defaultLevel = memberLevelService.getOne(new QueryWrapper<MemberLevelEntity>().eq("default_status", 1));
            uid.setLevelId(defaultLevel.getId());

            // 第三方信息
            JSONObject jsonObject = JSON.parseObject(json);
            //获得昵称,头像
            String name = jsonObject.getString("name");
            String profile_image_url = jsonObject.getString("avatar_url");
            // 这个service查询的
            uid.setNickname(name);
            uid.setHeader(profile_image_url);

            this.save(uid);
        } else {
            //2 否则更新令牌等信息并返回
            uid.setAccessToken(socialUserTo.getAccess_token());
            uid.setSocialUid(socialUserTo.getId());
            uid.setExpiresIn(socialUserTo.getExpires_in());
            uid.setHeader(socialUserTo.getAvatar_url());
            uid.setNickname(socialUserTo.getName());
            this.updateById(uid);
        }
        return uid;
    }

结果

在这里插入图片描述

----------------

{"id":2,"levelId":1,"nickname":"xiaosi720","header":"https://gitee.com/assets/no_portrait.png","socialUid":"8638250","accessToken":"840b0ea865990d2dfcb98b6ea96ff4ec","expiresIn":86400}
----------------

MemberResponseTo(id=2, levelId=1, username=null, password=null, nickname=xiaosi720, mobile=null, email=null, header=https://gitee.com/assets/no_portrait.png, gender=null, birth=null, city=null, job=null, sign=null, sourceType=null, integration=null, growth=null, status=null, createTime=null, socialUid=8638250, accessToken=840b0ea865990d2dfcb98b6ea96ff4ec, expiresIn=86400)
6.分布式Session问题

登录成功跳转首页需要更新用户状态,显示用户信息,这里涉及session的知识,分布式session的问题

在这里插入图片描述

在这里插入图片描述

1.解决方法1-session复制

适用场景

  • 服务较少的情况
  • 大量tomcat服务的情况,之间互相存储session浪费大量资源

在这里插入图片描述

2.解决方法2-客户端存储

适用场景

  • 相对较不安全
  • 基本不会使用

在这里插入图片描述

3.解决方案3-hash一致性

适用场景

  • 负载均衡的基础上,对同一个ip地址的请求固定到一台服务器上
  • 横向拓展服务器不太方便
4.解决方案4-统一存储

适用场景

  • 每个域名下,每个tomcat下的session进行统一存储
  • 可以存储到数据库、中间件中

在这里插入图片描述


二、分布式Session

官网:https://docs.spring.io/spring-session/docs/2.5.1/reference/html5/#samples

基本步骤:https://docs.spring.io/spring-session/docs/2.5.1/reference/html5/guides/boot-redis.html

1.整合Spring Session
  • commom模块导依赖

    <!--        分布式session-->
            <dependency>
                <groupId>org.springframework.session</groupId>
                <artifactId>spring-session-data-redis</artifactId>
            </dependency>
    
    
  • 配置auth、product模块

    # Session store type.
    spring.session.store-type=redis
    # Session timeout. If a duration suffix is not specified, seconds is used
    server.servlet.session.timeout=30ms
    #Sessions flush mode.
    spring.session.redis.flush-mode=on_save
    # Namespace for keys used to store sessions.
    spring.session.redis.namespace=spring:session
    
    
  • 主类注解@EnableRedisHttpSession// 开启分布式session

测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.使用json序列化方式
/**
     * 序列化方式
     * @return
     */

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericFastJsonRedisSerializer();
    }
3.解决Session共享域
package henu.soft.xiaosi.authserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;


/**
 * 分布式session
 * 提升session的作用域,便于父域名能够使用session
 */
@Configuration
public class MySessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

        cookieSerializer.setDomainName("gulishop.cn");
        cookieSerializer.setDomainName("GULISHOPSESSION");

        return cookieSerializer;
    }
}

在这里插入图片描述

核心原理参考往期博客:https://blog.csdn.net/qq_24654501/article/details/119740312


三、单点登录SSO

核心

  • 三个系统及时域名不同,想办法给三个系统同步同一认证状态
  • 中央认证服务器、其他系统想要登录取中央服务器登录,登录成功之后跳转回来

在这里插入图片描述

前面的Session作用域扩大也是有局限的,例如多个域名系统之间,而不是一个系统子模块之间,他们之间的Session处理

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scl、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值