目录
- 认证服务
- 创建微服务模块
- 阿里云短信验证码服务
- 整合代码测试
- 验证码的再次校验
- 注册完成登录
- 普通账号密码登录
- 社交登录
- 微博登录(码云登录)
- 分布式Session问题
- 分布式Session
- 整合SpringSession
- 使用json序列化方式
- 解决Session共享域
- 单点登录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处理