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

技术标签: 分布式session问题  Gitee第三方登录  腾讯云短信  分布式  

目录

  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处理

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_24654501/article/details/119684583

智能推荐

TDC-GP22的研究_我信张i的博客-程序员宝宝_tdc-gp22

本资源包含TDC-GP22的使用手册,TDC芯片寄存器的官方配置,本人基于stm32写的TDC-GP22寄存器配置程序,TDC-GP22的接线图和一个用文档方式写的注意事项文件:url80.ctfile.com/f/25127180-557023468-dd5834(访问密码:551685)以下内容无关:setState同步异步场景React通过this.state来访问state,通过this.setState()方法来更新state,当this.setState()方法被调用的时候,Reac

UE4 Pak 相关知识总结_kuangben2000的博客-程序员宝宝

UE4 Pak 相关知识总结https://arcecho.github.io/2017/07/02/UE4-Pak-%E7%9B%B8%E5%85%B3%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/每天进补一点点! Beranda ArsipUE4 Pak 相关知识总结Diposting di 2017-07-02 | Edited on 2019-03-08 | Di Tech近来研究了下UE4中Pak文件的工作流程,并对UE4中的文件系统了解了下。这

分析params_s方法_weixin_30677073的博客-程序员宝宝

/** * 解析启动模式参数 * @param $opt */ static public function params_s($opt) { //判断传入了s参数但是值,则提示错误 if ((isset($opt["s"]) &amp;&amp; !$opt["s"]) || (isset($opt[...

视频录制小结_weixin_30458043的博客-程序员宝宝

1.解决视频录制前后画面缩放问题就是把下面的两个参数设置成一样的,具体设置成多少要看需求 &lt;1&gt;.parmeters.setPreviewSize(640, 480); &lt;2&gt;.mediaRecorder.setVideoSize(640, 480);2.视频是展示在SurfaceView上面的,所以视频录制画面的大小,缩放情况和SurfaceView布局是密切...

LeeCode(No2 - Add Two Numbers)_weixin_30299539的博客-程序员宝宝

LeeCode是一个有意思的编程网站,主要考察程序员的算法第二题:You are given twonon-emptylinked lists representing two non-negative integers. The digits are stored inreverse orderand each of their nodes contain a sing...

随便推点

Banana PI 开源硬件生产介绍_sinovoip的博客-程序员宝宝

摘要             为了这个开源硬件项目,我们并不是做一个单纯的硬件,我们是从设计,生产,测试全面一体化的进行。 严格进行品质控制。下面介绍一下我们为生产准备的情况。方便大家对我们这个开源项目有一个直观的了解。                  项目从2013年七月份启动,经过五次改版本,最终硬件定型板为1.4版本。此版本过了所有的EMI,掩图测试。并进行了CE,FCC,

NFT发展史:像素头像的发家史?_扇贝科技的博客-程序员宝宝_像素头像nft

说个你可能不敢相信的事,2021年6月10日,#7523号加密朋克图像就以1175万美元拍出,你可能会觉得离谱,不就是一张电子图片吗,怎么可能?但他就是真实发生的。说起这个就不得不说说NFT了。 #7523去年3月,Beeple的无聊...

LOJ 2172 「FJOI2016」所有公共子序列问题——序列自动机_weixin_30629977的博客-程序员宝宝

题目:https://loj.ac/problem/2172在两个序列自动机上同时走,这样暴搜。先走字典序小的字符,一边搜一边输出,就是按字典序排序的。方案数很多,需要高精度?空间很小,要压位。1e9的20位恰好够。不开 n*n 的DP数组,给出现的状态分配一个位置,开 3e6 的DP数组,空间就能了。#include&lt;cstdio&gt;#include&lt;...

判断listview滚动到最顶部最底部_weixin_30480075的博客-程序员宝宝

下面的方法在ListView外面能用, 自定义ListView里面不能用. privateOnScrollListenermyListener=newOnScrollListener(){publicvoidonScrollStateChanged(AbsListViewview,intscrollState){switch(...

Hello world !_weixin_30340617的博客-程序员宝宝

今天开通了博客看Python的视频里老师说从开始就要养成写bolg和github的习惯他说他当时去汽车之家就没带简历~ 哈哈 光是博客就收了他那我今年大三了对编程能力还不是很强。学的智能科学与技术。之后每次做完一个小作业 都会记录再博客上,渐渐的发现我也可以做一名有趣的程序员让编程改变世界 ! keep up !BTW I'm a poper ... c...

Redis工具类最全_绿帽大牛的博客-程序员宝宝_redis工具包

package com.slzy.biz.common.util.redis;import java.util.List;import java.util.Map;import java.util.Set;import java.util.concurrent.TimeUnit;import com.slzy.biz.common.util.redis.justin.CachetValueAbstract;import org.springframework.beans.factory.an.

推荐文章

热门文章

相关标签