1. Oauth2.0
对于用户相关的openApi(例如用户昵称,图像等),第三方网站访问前都需要经过用户授权。
(1)基本流程
当用户使用QQ来访问CSDN时,(1)先向资源拥有者(本人)申请请求认证;(2)用户授权后,即用户输入自己的社交账号密码后;(3)对密码和账号进行认证,认证是由认证服务器来完成即QQ服务器;(4)认证通过后,会给CSDN返回一个code码;(5)CSDN使用这个code码就可以访问资源服务器即QQ服务器来获取用户的一些开放保护信息;(6)当QQ服务器认证通过后就会给CSDN返回受保护信息。
(2)weibo示例
a. 在前端登录后引导需要授权的用户到如下地址
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
b. 当用户输入密码同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
c. 访问接口换取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
d. 使用获得的Access Token调用API
2. 使用weibo第三方登录
登录流程
(1)在前端当用户点击登录时跳转到weibo授权地址
<a href="https://api.weibo.com/oauth2/authorize?client_id=440xxxx25&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)当用户授权后,在回调地址接口中接受code,继续访问微博认证服务换取access_token
@GetMapping(value = "/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
Map<String, String> map = new HashMap<>();
map.put("client_id","4xxxxxx5");
map.put("client_secret","exxxxxxxxxxb0517f78532d574086ab");
map.put("grant_type","authorization_code");
map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code",code);
//1、根据用户授权返回的code换取access_token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());
//2、处理
if (response.getStatusLine().getStatusCode() == 200) {
//获取到了access_token,转为通用社交登录对象
String json = EntityUtils.toString(response.getEntity());
//String json = JSON.toJSONString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
//知道了哪个社交用户
//1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息,以后这个社交账号就对应指定的会员)
//登录或者注册这个社交用户
System.out.println(socialUser.getAccess_token());
//调用远程服务
R oauthLogin = memberFeignService.oauthLogin(socialUser);
if (oauthLogin.getCode() == 0) {
MemberResponseVo data = oauthLogin.getData("data", new TypeReference<MemberResponseVo>() {});
log.info("登录成功:用户信息:{}",data.toString());
//1、第一次使用session,命令浏览器保存卡号,JSESSIONID这个cookie
//以后浏览器访问哪个网站就会带上这个网站的cookie
//TODO 1、默认发的令牌。当前域(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象到Redis中
// session.setAttribute(LOGIN_USER,data);
//2、登录成功跳回首页
return "redirect:http://gulimall.com";
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
}
(3)在换取token时,还需要处理实际的业务逻辑,使用access访问微博认证服务器,获取weibo对外开放的用户信息,然后在第三方应用中保存用户的信息进行使用,换取token后就可以跳转到首页
@PostMapping(value = "/oauth2/login")
public R oauthLogin(@RequestBody SocialUser socialUser) throws Exception {
MemberEntity memberEntity = memberService.login(socialUser);
if (memberEntity != null) {
return R.ok().setData(memberEntity);
} else {
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
}
}
@Override
public MemberEntity login(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;
}
}
3. session共享问题
用户登录后,需要在右上角显示登录的用户信息,在一般场景下,用户登录后,把用户信息保存到session中,前端从session中获取值就可以获取到用户信息了。第一次访问服务器后会命令浏览器保存一个jsessionid=123的cookie,以后访问会带上cookie,jessionid=123。当浏览器关闭后就会清除会话cookie。session就相当于服务端的一个map对象,保存在内存中。
问题:在实际场景中登录后并没有保存用户信息。
原因:session在不同域名下不能共享,而且使用这种session共享模式的话,如果使用分布式那么session也只会保存在一台服务器上,不能达到共享的效果。
解决方案:
(1)session复制
可以通过配置tomcat的方式来进行在多台服务器之间复制session,但是使用这种方式也会带来网络之间传输带宽,存储的缺点。
(2)客户端存储
不在服务器端存储session,把session保存到浏览器中,这样就不用再复制session,但是使用这种方式也会带来session保存到浏览器中会被修改和保存时的大小的问题。
(3)hash一致性
通过配置nginx来让用户1的信息就保存到服务器1上,用户2的信息就保存到服务器2上,这样就可以访问时直接去指定服务器中取数据,也不用相互复制。但是也会带来一些问题,如果服务要进行扩展的话那么之前计算方式就得重新进行hash计算。这种方式虽然有确定但是session本来就是有时间的,也是可以使用的。
(4)同一存储
通过把数据保存redis中,但是这种方式需要访问redis也增加了网络调用。
(5)不同服务,子域session共享
基于以上的一些方案的缺点和不足,保存session时,可以扩大jessionid对应的域名,在默认情况下jessionid只是当前系统域名下(auth.gulimall.com),当扩大(.gulimall.com)后,就可以在其他服务即不同域名场景下使用了。所以可以使用SpringSession来解决这个问题。
4. SpringSession session共享
(1)在商品服务和认证服务pom中引入springsession
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
(2)在application.property和启动类上配置
spring.session.store-type=redis
server.servlet.session.timeout=30m
@EnableRedisHttpSession
(3)在商品服务和第三方服务创建配置类,扩大session作用域,指定cookiename,配置在redis中存储方式使用json序列化
@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();
}
}
(4)前端登录页面上从session获取用户信息
<ul>
<li>
<a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>
<a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/login.html">你好,请登录</a>
</li>
<li>
<a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/reg.html" class="li_2">免费注册</a>
</li>
<li>
<a th:if="${session.loginUser != null}" href="http://auth.gulimall.com/loguot.html" class="li_2">立即退出</a>
</li>
<span>|</span>
<li>
<a href="http://member.gulimall.com/memberOrder.html">我的订单</a>
</li>
</ul>
(5)当用户在登录页使用weibo进行登录后,用户信息会保存到redis中,指定令牌为GULISESSION,同时会扩大作用域到.gulimall.com,这样就可以在认证服务和商品服务之间共享session。商品首页右上角就可以显示用户信息了。
5. 单点登录
Single Sign On 一处登陆、处处可用
创建client1,client2,ssoserver应用程序,client1,client2为应用程序,ssoserver为中心认证服务。
(1)当访问client1的受保护的http://localhost:8081/employees接口时,会先判断是否已经登录,如果还没有登录会重定向到新的地址,由统一登录地址拼接接口地址而成,如果已经登录了,那么直接跳转页面
@GetMapping(value = "/employees")
public String employees(Model model, HttpSession session,
@RequestParam(value = "token", required = false) String token) {
if (!StringUtils.isEmpty(token)){
// 去ssoserver登录成功就会携带token
// TODO 1. 去ssoserver获取当前token对应的用户信息
session.setAttribute("loginUser","zhangsan");
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null){
return "redirect:" + ssoServerUrl +"?redirect_url=http://localhost:8081/employees";
}else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
(2)当用户在登录页面进行登录成功后,会生成一个token保存到redis中,同时重定向到接口访问页面
@PostMapping(value = "/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("url") String url,
HttpServletResponse response){
//登录成功
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
String uuid = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(uuid, username);
//登录成功后保存cookie
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
return "redirect:" + url + "?token=" + uuid;
}
//登录失败
return "login";
}
(3)但是如果只是使用这种方式的话,并不能达到单点登录的效果,因为如果这是我们访问client2的受保护的接口还是需要进行登录的。所以我们在登录时还需要保存一个cookie信息到浏览器中,这个cookie信息也会保存在ssoserver域名下的cookie中。这样就可以通过浏览器是否携带cookie信息来判断是否已经登录过了。
访问登录接口,登录成功后保存cookie信息到浏览器中
同时会保存到ssoserver域名的cookie中
(4)所以当我们再次访问client2的接口时。
@GetMapping(value = "/boss")
public String employees(Model model, HttpSession session,
@RequestParam(value = "token", required = false) String token) {
if (!StringUtils.isEmpty(token)){
// 去ssoserver登录成功就会携带token
// TODO 1. 去ssoserver获取当前token对应的用户信息
session.setAttribute("loginUser","zhangsan");
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null){
return "redirect:" + ssoServerUrl +"?redirect_url=http://localhost:8082/boss";
}else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
首先会判断是否携带了token,因为开始没有登录,所以会跳转到login.html页面,在html页面接口中,会先从浏览器中获取cookie,如果可以获取到,那么就重定向到原来的boss接口地址中,同时会拼接到token返回到接口地址中。当再次返回boss地址后由于此时已经携带了token就可以直接进行访问接口了。
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url,
Model model,
@CookieValue(value = "sso_token",required = false) String sso_toke){
if (!StringUtils.isEmpty(sso_toke)){
//说明已经登录过了,浏览器中留下了痕迹
return "redirect:" + url + "?token=" + sso_toke;
}
model.addAttribute("url", url);
return "login";
}