沒(méi)有比這更詳細(xì)的單點(diǎn)登錄流程了

1. 單點(diǎn)登錄

單點(diǎn)登錄(Single Sign On),簡(jiǎn)稱(chēng)為 SSO,是比較流行的企業(yè)業(yè)務(wù)整合的解決方案之一。SSO的定義是在多個(gè)應(yīng)用系統(tǒng)中,用戶(hù)只需要登錄一次就可以訪問(wèn)所有相互信任的應(yīng)用系統(tǒng)。

對(duì)于相同父域名下的單點(diǎn)登錄比較簡(jiǎn)單,只需要將cookie的作用域放大到父域名即可。

@Bean
public CookieSerializer cookieSerializer(){
   DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
   cookieSerializer.setDomainName("ylogin.com");
   cookieSerializer.setCookieName("YLOGINESSION");
   return cookieSerializer;![單點(diǎn)登錄.png](https://upload-images.jianshu.io/upload_images/26138896-567d84ffe7300b90.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

}

本文主要分享一下不同應(yīng)用服務(wù)器之間(即不同域名)的單點(diǎn)登錄流程。

1. 單點(diǎn)登錄流程

單點(diǎn)登錄流程圖如下

  1. 假設(shè)現(xiàn)在第一次訪問(wèn)Client1的受保護(hù)的資源,由于我們沒(méi)有登錄,則需要跳轉(zhuǎn)到登錄服務(wù)器進(jìn)行登錄,但是登錄之后應(yīng)該跳到哪里呢?很顯然,需要跳回到我們想要訪問(wèn)的頁(yè)面,所以在重定向到登錄服務(wù)器時(shí)帶上回調(diào)地址redirectURL。
@GetMapping("/abc")
public String abc(HttpServletRequest request,HttpSession session, @RequestParam(value = "token",required = false) String token) throws Exception {
    if (!StringUtils.isEmpty(token)){
        Map<String,String> map = new HashMap<>();
        map.put("token",token);
        HttpResponse response = HttpUtils.doGet("http://auth.ylogin.com", "/loginUserInfo", "GET", new HashMap<String, String>(), map);
        String s = EntityUtils.toString(response.getEntity());
        if (!StringUtils.isEmpty(s)){
            UserResponseVo userResponseVo = JSON.parseObject(s, new TypeReference<UserResponseVo>() {
            });
            session.setAttribute(AuthServerConstant.LOGIN_USER,userResponseVo);
            localSession.put(token,session);
            sessionTokenMapping.put(session.getId(),token);
        }
    }
    UserResponseVo attribute = (UserResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    if (attribute != null){
        return "abc";
    } else {
        // 由于域名不同,不能實(shí)現(xiàn)session共享,無(wú)法在登錄頁(yè)面展示msg
        session.setAttribute("msg","請(qǐng)先進(jìn)行登錄");
        // 帶上回調(diào)地址
        return "redirect:http://auth.ylogin.com/login.html?redirectURL=http://ylogin.client1.com"+request.getServletPath();
    }
}
  1. 瀏覽器展示登錄頁(yè)

  2. 用戶(hù)輸入賬號(hào)密碼進(jìn)行登錄,并在隱藏域提交回調(diào)地址

  3. 登錄服務(wù)器查詢(xún)數(shù)據(jù)庫(kù),驗(yàn)證賬號(hào)及密碼。賬號(hào)密碼正確,則生成一個(gè)令牌sso_token,保存到cookie中(該cookie只存在于登錄服務(wù)器),并將登錄用戶(hù)信息以sso_token為key,保存到redis中(劇透,順便保存回調(diào)地址到redis)。然后攜帶上令牌重定向到回調(diào)地址(即登錄前頁(yè)面)。

@PostMapping("/login")
public String login(UserLoginTo to, RedirectAttributes redirectAttributes, HttpServletResponse response) {
    //遠(yuǎn)程登陸
    R login = userFeignService.login(to);
    if (login.getCode() == 0) {
        UserResponseVo data = login.getData(new TypeReference<UserResponseVo>() {
        });
        log.info("登錄成功!用戶(hù)信息"+data.toString());
        // 保存用戶(hù)信息到redis(key->value:sso_token->登錄用戶(hù)信息)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(token, JSON.toJSONString(data),2, TimeUnit.MINUTES);
        // 添加登錄地址
        addLoginUrl(to.getRedirectURL());
        // 保存令牌到cookie
        Cookie cookie = new Cookie("sso_token", token);
        response.addCookie(cookie);
        // 攜帶令牌重定向到回調(diào)地址
        return "redirect:"+to.getRedirectURL()+"?token="+token;
    } else {
        Map<String, String> errors = new HashMap<>();
        errors.put("msg", login.get("msg", new TypeReference<String>() {
        }));
        redirectAttributes.addFlashAttribute("errors", errors);
        return "redirect:http://auth.ylogin.com/login.html?redirectURL="+to.getRedirectURL();
    }
}
  1. 應(yīng)用服務(wù)器1拿到token,需要向驗(yàn)證服務(wù)器發(fā)起請(qǐng)求(也可以直接到redis中查是否存在這個(gè)key),驗(yàn)證是否存在該token。目的是為了防止偽造令牌。驗(yàn)證通過(guò),則保存用戶(hù)信息到本地session,(下次訪問(wèn)則無(wú)需經(jīng)過(guò)登錄服務(wù)器,判斷session中存在用戶(hù)即可),返回用戶(hù)想到訪問(wèn)的含受保護(hù)資源頁(yè)面。
@ResponseBody
@GetMapping("/loginUserInfo")
public String loginUserInfo(@RequestParam("token") String token){
    String s = redisTemplate.opsForValue().get(token);
    return s;
}
@GetMapping("/abc")
public String abc(HttpServletRequest request,HttpSession session, @RequestParam(value = "token",required = false) String token) throws Exception {
    // 判斷是否攜帶令牌
    if (!StringUtils.isEmpty(token)){
        // 攜帶令牌,可能是已登錄用戶(hù),需向登錄服務(wù)器進(jìn)行確認(rèn)
        Map<String,String> map = new HashMap<>();
        map.put("token",token);
        HttpResponse response = HttpUtils.doGet("http://auth.ylogin.com", "/loginUserInfo", "GET", new HashMap<String, String>(), map);
        String s = EntityUtils.toString(response.getEntity());
        if (!StringUtils.isEmpty(s)){
            // 驗(yàn)證通過(guò),保存登錄用戶(hù)信息到本地session,下次訪問(wèn)則無(wú)需經(jīng)過(guò)登錄服務(wù)器
            UserResponseVo userResponseVo = JSON.parseObject(s, new TypeReference<UserResponseVo>() {
            });
            session.setAttribute(AuthServerConstant.LOGIN_USER,userResponseVo);
            localSession.put(token,session);
            sessionTokenMapping.put(session.getId(),token);
        }
    }
    UserResponseVo attribute = (UserResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    if (attribute != null){
        return "abc";
    } else {
        session.setAttribute("msg","請(qǐng)先進(jìn)行登錄");
        return "redirect:http://auth.ylogin.com/login.html?redirectURL=http://ylogin.client1.com"+request.getServletPath();
    }
}
  1. 用戶(hù)再次發(fā)起請(qǐng)求,訪問(wèn)Client2中受保護(hù)的資源,同樣會(huì)先到登錄服務(wù)器的登錄頁(yè)面,但此時(shí)會(huì)帶上cookie,登錄服務(wù)器一看,有cookie,就知道這是一個(gè)在其他系統(tǒng)登錄過(guò)的用戶(hù),就發(fā)放一個(gè)令牌,重定向到用戶(hù)訪問(wèn)的地址。
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirectURL") String url, @CookieValue(value = "sso_token",required = false) String sso_token){
    // 先判斷是否在其他系統(tǒng)登錄過(guò)
    if (!StringUtils.isEmpty(sso_token)){
        // 添加登錄地址
        addLoginUrl(url);
        System.out.println("已登錄");
        return "redirect:"+url+"?token="+sso_token;
    }
    return "login";
}
  1. 應(yīng)用服務(wù)器2拿到令牌,同樣需要到登錄服務(wù)器進(jìn)行驗(yàn)證,驗(yàn)證成功則保存用戶(hù)信息到本地session,返回訪問(wèn)資源頁(yè)面。

  2. 應(yīng)用服務(wù)器判斷用戶(hù)是否登錄,第一次看是否攜帶令牌,之后就看本地session中有沒(méi)有登錄用戶(hù)的信息。

  3. 登錄服務(wù)器判斷用戶(hù)是否登錄,第一次就到數(shù)據(jù)庫(kù)查詢(xún),之后就看是否攜帶cookie。

2. 單點(diǎn)登出流程

話不多說(shuō),先放個(gè)單點(diǎn)登出的流程圖。

單點(diǎn)登出.png
  1. 用戶(hù)點(diǎn)擊注銷(xiāo)按鈕,攜帶令牌到登錄服務(wù)器進(jìn)行驗(yàn)證,同樣需要攜帶上回調(diào)地址(一般為公共資源頁(yè)面即可),作為登出后展示在瀏覽器的頁(yè)面。

    你是不是有幾個(gè)疑問(wèn)呢。為什么退出登錄也需要攜帶令牌?本地session中只保存了登錄用戶(hù)的基本信息,那要如何攜帶令牌到登錄服務(wù)器呢?不著急,下面就為你解答。

    • 攜帶令牌的目的是為了驗(yàn)證改退出請(qǐng)求是登錄用戶(hù)發(fā)起的,防止其他人惡意請(qǐng)求。

    • 對(duì)于獲取token,我們可以利用SessionID來(lái)獲取token,所以我們必須在登錄成功后,保存用戶(hù)信息到session的同時(shí),也保存SessionID和token的映射關(guān)系(可以使用靜態(tài)map來(lái)保存)。

// SessionID->token
private static final Map<String, String> sessionTokenMapping = new HashMap<>();
@GetMapping("/logout")
public String logout(HttpServletRequest request){
   // 根據(jù)sessionId獲取token令牌
   String sessionId = request.getSession().getId();
   String token = sessionTokenMapping.get(sessionId);
   return "redirect:http://auth.ylogin.com/logOut?redirectURL=http://ylogin.client1.com&token="+token;
}
  1. 登錄服務(wù)器驗(yàn)證成功,向已經(jīng)登陸的所有應(yīng)用服務(wù)器發(fā)起注銷(xiāo)請(qǐng)求(帶上令牌)。所以我們需要知道有哪些應(yīng)用服務(wù)器登陸了。這就是我在上面劇透的,登錄服務(wù)器在驗(yàn)證登錄時(shí)保存應(yīng)用服務(wù)器地址。
private void addLoginUrl(String url){
    String s = redisTemplate.opsForValue().get("loginUrl");
    if (StringUtils.isEmpty(s)){
        List<String> urls = new ArrayList<>();
        urls.add(url);
        redisTemplate.opsForValue().set("loginUrl",JSON.toJSONString(urls));
    } else{
        List<String> urls = JSON.parseObject(s, new TypeReference<List<String>>() {
        });
        urls.add(url);
        redisTemplate.opsForValue().set("loginUrl",JSON.toJSONString(urls));
    }
}
@GetMapping("/logOut")
public String logout(HttpServletRequest request, HttpServletResponse response,@RequestParam("redirectURL") String url, @RequestParam("token") String token) throws Exception {
    Cookie[] cookies = request.getCookies();
    if (cookies != null && cookies.length > 0){
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("sso_token")){
                // 驗(yàn)證令牌
                if (cookie.getValue().equals(token)){
                    String value = cookie.getValue();
                    // 清除各應(yīng)用系統(tǒng)的session
                    String s = redisTemplate.opsForValue().get("loginUrl");
                    Map<String, String> map = new HashMap<>();
                    map.put("token",value);
                    if (!StringUtils.isEmpty(s)){
                        List<String> urls = JSON.parseObject(s, new TypeReference<List<String>>() {
                        });
                        for (String loginUrl : urls) {
                            HttpUtils.doGet(loginUrl, "/deleteSession", "GET",new HashMap<String, String>(), map);
                        }
                    }
                    // 刪除redis中保存的用戶(hù)信息
                    redisTemplate.delete(value);
                    // 清除SSO服務(wù)器的cookie令牌
                    Cookie cookie1 = new Cookie("sso_token", "");
                    cookie1.setPath("/");
                    cookie1.setMaxAge(0);
                    response.addCookie(cookie1);
                }
            }
        }
    }
    // 清除redis保存的登錄url
    redisTemplate.delete("loginUrl");
    return "redirect:"+url;
}
  1. 應(yīng)用服務(wù)器收到登錄服務(wù)器的注銷(xiāo)請(qǐng)求,首先驗(yàn)證令牌,判斷是否是登錄服務(wù)器發(fā)起的注銷(xiāo)請(qǐng)求。
@ResponseBody
@GetMapping("/abc/deleteSession")
public String logout(@RequestParam("token") String token){
    HttpSession session = localSession.get(token);
    //        session.removeAttribute(AuthServerConstant.LOGIN_USER);
    session.invalidate();
    return "logout";
}
  • 這里尤其需要注意,需要獲取指定session。登錄服務(wù)器發(fā)送過(guò)來(lái)的請(qǐng)求,如果直接request.getSession().getId()獲取,這樣獲取到的是新的session,并不是保存用戶(hù)信息的會(huì)話。
  • 為解決這一問(wèn)題,在保存用戶(hù)信息到本地session的同時(shí),使用靜態(tài)map來(lái)保存session,以令牌作為key。
// token->session
private static final Map<String, HttpSession> localSession = new HashMap<>();

至此,單點(diǎn)登錄功能基本實(shí)現(xiàn)。如果感興趣,歡迎到我的github倉(cāng)庫(kù)獲取源碼。如果覺(jué)得有用的話,歡迎start。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容