目录
前言
QQ、微博、github 等网站的用户量非常大,别的网站为了简化自我网站的登陆与注册逻辑,引入社交登陆功能,步骤:
1、用户点击 QQ 按钮。
2、引导跳转到 QQ 授权页。
3、用户主动点击授权,跳回之前网页。
一、OAuth 2.0
-
OAuth: OAuth (开放授权) 是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
-
OAuth2.0:对于用户相关的 OpenAPI (例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
-
官方版流程:
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
社交登录流程:
二、微博登陆准备工作
1、进入微博开放平台
2、登陆微博,进入微连接,选择网站接入
3、选择立即接入
4、创建自己的应用
5、我们可以在开发阶段进行测试了
记住自己的 app key 和 app secret 我们一会儿用
6、进入高级信息,填写授权回调页的地址
7、添加测试账号
8、进入文档,按照流程测试社交登陆
三、整合微博登录
1、引导用户到如下地址
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
前端中进行编写:
<a href="https://api.weibo.com/oauth2/authorize?client_id=2636917288&response_type=code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success">
<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png"/>
</a>
2. 授权登录后进入回调函数
然后在登录时,我们将登录信息保存到了 session 中,但是在分布式架构下,不同服务之间涉及到 session 不同步问题,我们需要在此处提出 session 共享的解决方案。。。
首先我们来看 session原理:
分布式下的 session 共享问题:
3. Session共享问题解决
① session复制
- 优点
- web-server(Tomcat)原生支持,只需要修改配置文件
- 缺点
- session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
- 任意一台web-server保存的数据都是所有web-server的session总和,受到内存限制无法水平扩展更多的web-server
- 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。
② 客户端存储
- 优点
- 服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源。
- 缺点
- 都是缺点,这只是一种思路。• 具体如下:
- 每次http请求,携带用户在cookie中的完整信息,浪费网络带宽。
- session数据放在cookie中,cookie有长度限制4K,不能保存大量信息• session数据放在cookie中,存在泄漏、篡改、窃取等安全隐患。
- 这种方式不会使用。
③ hash一致性
- 优点:
- 只需要改nginx配置,不需要修改应用代码
- 负载均衡,只要hash属性的值分布是均匀的,多台web-server的负载是均衡的
- 可以支持web-server水平扩展(session同步法是不行的,受内存限制)
- 缺点
- session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
- 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session
- 但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用
④ 统一存储
- 优点:
- 没有安全隐患
- 可以水平扩展,数据库/缓存水平切分即可
- web-server重启或者扩容都不会有session丢失
- 不足
- 增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替换为从Redis查数据的方式。redis获取数据比内存慢很多
- 上面缺点可以用 SpringSession 完美解决
4. 整合 SpringSession
接下来我们来整合 SpringSession 来解决 session 共享问题
引入2个 start 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
application.properties 进行相关配置
A、spring.session.store-type=redis
设置 Spring Session 使用 Redis 进行存储。默认配置就是 redis
B、spring.session.timeout=10m
设置 Spring Session 的过期时间。如果不指定单位模式是 s。
在主启动类上加上注解 @EnableRedisHttpSession
开启将 session 统一存储到 redis 中
同时待解决的还有 不同服务,子域session共享 问题,比如说我们 auth.gulimall.com 中存储session,session 域仅在此域名中,我们想要在 gulimall.com 中获取session 不行。。。,解决方式就是 自定义 SpringSession 配置文件将作用域范围扩大。。
package com.fancy.gulimall.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
5. SpringSession 核心原理
@EnableRedisHttpSession 导入 RedisHttpSessionConfiguration 配置
1、给容器中添加了一个组件
SessionRepository = 》》》【RedisOperationsSessionRepository】==》redis操作session。session的增删改查封装类
2、SessionRepositoryFilter == 》Filter: session'存储过滤器;每个请求过来都必须经过filter
1、创建的时候,就自动从容器中获取到了SessionRepository;
2、原始的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
3、以后获取session。request.getSession();
//SessionRepositoryRequestWrapper
4、wrappedRequest.getSession();===> SessionRepository 中获取到的。
装饰者模式;
自动延期;redis中的数据也是有过期时间。
6. 使用返回的 code,换取 access token
注意,上面这个是 post 请求
{
"access_token": "2.00pDpxyGd3J5bEef6b98778e0ZKsu4",
"remind_in": "157679999",
"expires_in": 157679999,
"uid": "6397634785",
"isRealName": "true"
}
接口的完整代码:
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session, HttpServletResponse servletResponse, HttpServletRequest request) throws Exception {
Map<String,String> header = new HashMap<>();
Map<String,String> query = new HashMap<>();
Map<String,String> map = new HashMap<>();
map.put("client_id","2636917288");
map.put("client_secret","6a263e9284c6c1a74a62eadacc11b6e2");
map.put("grant_type","authorization_code");
map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code",code);
//1、根据code换取accessToken;
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", header, query, map);
//2、处理
if(response.getStatusLine().getStatusCode()==200){
//获取到了 accessToken
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
//知道当前是哪个社交用户
//1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)
//登录或者注册这个社交用户
R oauthlogin = memberFeignService.oauthLogin(socialUser);
if(oauthlogin.getCode() == 0){
MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
});
log.info("登录成功:用户:{}",data.toString());
//1、第一次使用session;命令浏览器保存卡号。JSESSIONID这个cookie;
//以后浏览器访问哪个网站就会带上这个网站的cookie;
//子域之间; gulimall.com auth.gulimall.com order.gulimall.com
//发卡的时候(指定域名为父域名),即使是子域系统发的卡,也能让父域直接使用。
//TODO 1、默认发的令牌。session=dsajkdjl。作用域:当前域;(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象数据到redis中
session.setAttribute("loginUser",data);
// new Cookie("JSESSIONID","dadaa").setDomain("");
// servletResponse.addCookie();
//2、登录成功就跳回首页
return "redirect:http://gulimall.com";
}else {
return "redirect:http://auth.gulimall.com/login.html";
}
}else {
return "redirect:http://auth.gulimall.com/login.html";
}
}
这里登录是远程调用 MemberService 的 oathLogin 方法。。。
@Override
public MemberEntity authLogin(SocialUser socialUser) throws Exception {
//具有登录和注册逻辑
String uid = socialUser.getUid();
//1、判断当前社交用户是否已经登录过系统
MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) {
//这个用户已经注册过
//更新用户的访问令牌的时间和access_token
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
//2、没有查到当前社交用户对应的记录我们就需要注册一个
MemberEntity register = new MemberEntity();
//3、查询当前社交用户的社交账号信息(昵称、性别等)
Map<String,String> query = new HashMap<>();
query.put("access_token",socialUser.getAccess_token());
query.put("uid",socialUser.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String, String>(), query);
if (response.getStatusLine().getStatusCode() == 200) {
//查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
String profileImageUrl = jsonObject.getString("profile_image_url");
register.setNickname(name);
register.setGender("m".equals(gender)?1:0);
register.setHeader(profileImageUrl);
register.setCreateTime(new Date());
register.setSocialUid(socialUser.getUid());
register.setAccessToken(socialUser.getAccess_token());
register.setExpiresIn(socialUser.getExpires_in());
//把用户信息插入到数据库中
this.baseMapper.insert(register);
}
return register;
}
}
同时,我们在其他服务也要配置 SpringSession
Oauth2.0:授权通过后,使用 code 换取 access_token,然后去访问任何开放API
A、code 用后即毁
B、access_token 在几天内是一样的
C、uid 永久固定