1. Single sign-on

Single Sign On, or SSO for short, is one of the more popular solutions for enterprise business integration. SSO is defined as one login in multiple applications that allows users to access all trusted applications.

For single sign-on under the same parent domain name is relatively simple, just need to enlarge the cookie scope to the parent domain name.

@Bean
public CookieSerializer cookieSerializer(a){
    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    cookieSerializer.setDomainName("ylogin.com");
    cookieSerializer.setCookieName("YLOGINESSION");
    return cookieSerializer;
}
Copy the code

This article focuses on the single sign-on process between different application servers (that is, different domain names).

1. Single sign-on process

The flow chart of single sign-on is as follows

  1. Suppose we are now accessing Client1’s protected resource for the first time. Since we are not logged in, we need to jump to the login server to log in, but where should we jump to after logging in? Obviously, we need to jump back to the page we want to visit, so bring the callback address redirectURL when redirecting to the login server.

    @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 {
            // Because the domain name is different, the session cannot be shared and MSG cannot be displayed on the login page
            session.setAttribute("msg"."Please log in first.");
            // Bring the callback address
            return "redirect:http://auth.ylogin.com/login.html?redirectURL=http://ylogin.client1.com"+request.getServletPath(); }}Copy the code
  2. The browser displays the login page

  3. The user enters the account password to log in and submits the callback address in the hidden domain

  4. Log in to the server to query the database and verify the account and password. If the account password is correct, a token sSO_token is generated and stored in a cookie (the cookie only exists on the login server), and the login user information is stored in Redis with the sSO_token as the key (the story is revealed and the callback address is saved to Redis). It then carries the token to redirect to the callback address (the pre-login page).

    @PostMapping("/login")
    public String login(UserLoginTo to, RedirectAttributes redirectAttributes, HttpServletResponse response) {
        // Remote login
        R login = userFeignService.login(to);
        if (login.getCode() == 0) {
            UserResponseVo data = login.getData(new TypeReference<UserResponseVo>() {
            });
            log.info("Login successful! User information"+data.toString());
            // Save user information to redis(key->value:sso_token-> login user information)
            String token = UUID.randomUUID().toString().replace("-"."");
            redisTemplate.opsForValue().set(token, JSON.toJSONString(data),2, TimeUnit.MINUTES);
            // Add the login address
            addLoginUrl(to.getRedirectURL());
            // Save the token to cookie
            Cookie cookie = new Cookie("sso_token", token);
            response.addCookie(cookie);
            // Carries the token redirect to the callback address
            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(); }}Copy the code
  5. After obtaining the token, application server 1 sends a request to the authentication server (or directly check whether the key exists in redis) to verify whether the token exists. The purpose is to prevent forged tokens. If the authentication succeeds, the user information is saved to the local session, and the page containing protected resources that the user wants to access is returned.

    @ResponseBody
    @GetMapping("/loginUserInfo")
    public String loginUserInfo(@RequestParam("token") String token){
        String s = redisTemplate.opsForValue().get(token);
        return s;
    }
    Copy the code
    @GetMapping("/abc")
    public String abc(HttpServletRequest request,HttpSession session, @RequestParam(value = "token",required = false) String token) throws Exception {
        // Determine whether a token is carried
        if(! StringUtils.isEmpty(token)){// Carries a token, which may be a logged-in user, and needs to confirm with the logged-in server
            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)){// If the authentication succeeds, the login user information is saved to the local session. The next login does not need to go through the login server
                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"."Please log in first.");
            return "redirect:http://auth.ylogin.com/login.html?redirectURL=http://ylogin.client1.com"+request.getServletPath(); }}Copy the code
  6. When a user initiates a request to access the protected resource in Client2, the user will also be sent to the login page of the login server first. However, the user will be sent to the login page of the login server with a cookie. When the login server sees the cookie, it will know that the user has logged in to another system and issue a token to redirect to the address accessed by the user.

    @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirectURL") String url, @CookieValue(value = "sso_token",required = false) String sso_token){
        // Check whether you have logged in to another system
        if(! StringUtils.isEmpty(sso_token)){// Add the login address
            addLoginUrl(url);
            System.out.println("Logged in");
            return "redirect:"+url+"? token="+sso_token;
        }
        return "login";
    }
    Copy the code
  7. Application server 2 also needs to authenticate the token from the login server. If the authentication succeeds, the user information is saved to the local session and the page for accessing resources is returned.

  8. The application server determines whether the user is logged in, first to see if the token is present, and then to see if the user’s information is present in the local session.

  9. Login server to determine whether the user login, the first time to the database query, then to see whether to carry cookies.

2. Single signout process

Without further ado, let’s start with a flow chart for single sign-out.

  1. The user clicks the logout button, carries the token to the login server for verification, and also needs to carry the previous callback address (generally for the public resources page), which will be displayed in the browser after logging out.

    Do you have a few questions? Why do I need to carry a token to log out? The local session only stores the basic information of the logged-in user. How to carry the token to the logged-in server? Don’t worry, the following answer for you.

    • The purpose of carrying a token is to verify that the change-out request was initiated by the logged-in user and to prevent malicious requests from others.

    • To obtain the token, we can use the SessionID to obtain the token. Therefore, we must save the user information to the session and the mapping between the SessionID and the token (static map can be used to save the mapping) after the successful login.

    // SessionID->token
    private static final Map<String, String> sessionTokenMapping = new HashMap<>();
    Copy the code
    @GetMapping("/logout")
    public String logout(HttpServletRequest request){
        // Get token according to sessionId
        String sessionId = request.getSession().getId();
        String token = sessionTokenMapping.get(sessionId);
        return "redirect:http://auth.ylogin.com/logOut?redirectURL=http://ylogin.client1.com&token="+token;
    }
    Copy the code
  2. Verify that the login server is successful, and send a logout request (with token) to all the application servers that have logged in. So we need to know which application servers are logged in. This is what I revealed above, the login server saves the application server address when authenticating the login.

    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)); }}Copy the code
    @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")) {// Validate the token
                    if (cookie.getValue().equals(token)){
                        String value = cookie.getValue();
                        // Clear the session of each application system
                        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".newHashMap<String, String>(), map); }}// Delete user information saved in redis
                        redisTemplate.delete(value);
                        // Clears the SSO server's cookie token
                        Cookie cookie1 = new Cookie("sso_token"."");
                        cookie1.setPath("/");
                        cookie1.setMaxAge(0); response.addCookie(cookie1); }}}}// Clear the login URL saved by Redis
        redisTemplate.delete("loginUrl");
        return "redirect:"+url;
    }
    Copy the code
  3. After receiving the logout request from the login server, the application server verifies the token to determine whether the logout request is initiated by the login server.

    @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";
    }
    Copy the code
    • In particular, you need to get the specified session. If the request sent by the login server is directly obtained by request.getSession().getid (), a new session is obtained instead of the session that stores user information.

    • To solve this problem, a static map is used to save the session and the token is used as the key while saving the user information to the local session.

      // token->session
      private static final Map<String, HttpSession> localSession = new HashMap<>();
      Copy the code

So far, the single sign-on function is basically realized. If you are interested, please visit my Github repository to get the source code. Start is welcome if you find it useful.