OAuth2
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。
1、使用Code换取AccessToken,Code只能用一次
2、同一个用户的accessToken一段时间是不会变化的,即使多次获取
客户端的授权模式:
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
接入其他社交平台
可参照其文档进行操作
如:gitee: https://gitee.com/api/v5/oauth_doc#/
微博:https://open.weibo.com/connect
具体步骤:
- 引导需要授权的用户到如下地址:
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=授权后跳转的uri
示例:
https://api.weibo.com/oauth2/authorize?
client_id=刚才申请的APP-KEY &
response_type=code&
redirect_uri=http://gulimall.com/success
-
如果用户同意授权(输入账号密码),带着code,页面跳转至 gulimall.com/success/?code=CODE
跳回我们网站的时候,带了一个code码,这个code码可以理解为用户登录的sessionID -
POST拿着code码换取Access Token
https://api.weibo.com/oauth2/access_token?
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
grant_type=authorization_code&
redirect_uri=YOUR_REGISTERED_REDIRECT_URI&
code=CODE
其中client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET可以使用basic方式加入header中,返回值
{
"access_token": "SlAV32hkKG",
"remind_in": 3600, # 也是声明周期,但将废弃
"expires_in": 3600 # access_token的生命周期;
}
- 使用获得的Access Token调用API,可以获取头像等信息 https://open.weibo.com/wiki/2/users/show
代码编写
注意点:
- 登录成功得到了code,这不应该提供给用户
- 拿着code还有其他信息APP-KEY去获取token,更不应该给用户看到
- 应该回调的是后台的controller,在后台处理完token逻辑后返回
- 把成功后回调改为:gulimall.com/oauthn2.0/weibo/success
/weibo/success
- 通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
进行账号保存,主要有uid、token、expires_in - 若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
登录成功跳转到首页,但是怎么保证没有验证情况下访问不了首页:用shiro等拦截器功能
@GetMapping("/weibo/success") // Oath2Controller
public String weiBo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 根据code换取 Access Token
Map<String,String> map = new HashMap<>();
map.put("client_id", "1294828100");
map.put("client_secret", "a8e8900e15fba6077591cdfa3105af44");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
Map<String, String> headers = new HashMap<>();
// 去获取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);
if(response.getStatusLine().getStatusCode() == 200){
// 获取响应体: Access Token
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
// 相当于我们知道了当前是那个用户
// 1.如果用户是第一次进来 自动注册进来(为当前社交用户生成一个会员信息 以后这个账户就会关联这个账号)
R login = memberFeignService.login(socialUser);
if(login.getCode() == 0){
MemberRsepVo rsepVo = login.getData("data" ,new TypeReference<MemberRsepVo>() {});
log.info("\n欢迎 [" + rsepVo.getUsername() + "] 使用社交账号登录");
// 第一次使用session 命令浏览器保存这个用户信息 JESSIONSEID 每次只要访问这个网站就会带上这个cookie
// 在发卡的时候扩大session作用域 (指定域名为父域名)
// TODO 1.默认发的当前域的session (需要解决子域session共享问题)
// TODO 2.使用JSON的方式序列化到redis
// new Cookie("JSESSIONID","").setDomain("gulimall.com");
session.setAttribute(AuthServerConstant.LOGIN_USER, rsepVo);
// 登录成功 跳回首页
return "redirect:http://gulimall.com";
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}
token保存
- 登录包含两种流程,实际上包括了注册和登录
- 如果之前未使用该社交账号登录,则使用token调用开放api获取社交账号相关信息(头像等),注册并将结果返回
- 如果之前已经使用该社交账号登录,则更新token并将结果返回
@RequestMapping("/oauth2/login")
public R login(@RequestBody SocialUser socialUser) {
MemberEntity entity=memberService.login(socialUser);
if (entity!=null){
return R.ok().put("memberEntity",entity);
}else {
return R.error();
}
}
@Override // 已经用code生成了token
public MemberEntity login(SocialUser socialUser) {
// 微博的uid
String uid = socialUser.getUid();
// 1.判断社交用户登录过系统
MemberDao dao = this.baseMapper;
MemberEntity entity = dao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
MemberEntity memberEntity = new MemberEntity();
if(entity != null){ // 注册过
// 说明这个用户注册过, 修改它的资料
// 更新令牌
memberEntity.setId(entity.getId());
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
// 更新
dao.updateById(memberEntity);
entity.setAccessToken(socialUser.getAccessToken());
entity.setExpiresIn(socialUser.getExpiresIn());
entity.setPassword(null);
return entity;
}else{ // 没有注册过
// 2. 没有查到当前社交用户对应的记录 我们就需要注册一个
HashMap<String, String> map = new HashMap<>();
map.put("access_token", socialUser.getAccessToken());
map.put("uid", socialUser.getUid());
try {
// 3. 查询当前社交用户账号信息(昵称、性别、头像等)
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), map);
if(response.getStatusLine().getStatusCode() == 200){
// 查询成功
String json = EntityUtils.toString(response.getEntity());
// 这个JSON对象什么样的数据都可以直接获取
JSONObject jsonObject = JSON.parseObject(json);
memberEntity.setNickname(jsonObject.getString("name"));
memberEntity.setUsername(jsonObject.getString("name"));
memberEntity.setGender("m".equals(jsonObject.getString("gender"))?1:0);
memberEntity.setCity(jsonObject.getString("location"));
memberEntity.setJob("自媒体");
memberEntity.setEmail(jsonObject.getString("email"));
}
} catch (Exception e) {
log.warn("社交登录时远程调用出错 [尝试修复]");
}
memberEntity.setStatus(0);
memberEntity.setCreateTime(new Date());
memberEntity.setBirth(new Date());
memberEntity.setLevelId(1L);
memberEntity.setSocialUid(socialUser.getUid());
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
// 注册 -- 登录成功
dao.insert(memberEntity);
memberEntity.setPassword(null);
return memberEntity;
}
}