单点登录与社交登录

发布时间 2023-08-27 11:24:04作者: homle

 

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";
}