background

Single Sign On is also called Single Sign On, or SSO for short. Single Sign On can be shared based On user session. There are two kinds of Single Sign On.

For example, now there is a first-level domain name www.imooc.com, which is an educational website, but MOOCs has other product lines, which can provide services to users by building second-level domain names, such as: Music.imooc.com, blog.imooc.com, etc., respectively, for music class and for class blogs, users only need to login in one site, other sites will subsequently and log in.

That is to say, the user from beginning to end only under a certain site after logging in, so he produced by session, and is Shared with other sites, has realized the single point website login, and indirect login to other sites, so this is actually a single sign-on, their conversation is Shared, will have the same user session.

Cookie + Redis implements SSO

So the distributed session back-end we implemented before is based on Redis, so that the session can flow through any system of the back-end and can obtain the user data information in the cache. The front-end can ensure that the data can be obtained under the first level and second level of the same domain name by using cookies. In this way, The information userID and token in the cookie can be carried when sending the request, so that the request from the front end and the back end can be obtained. In this way, in fact, after the user logs in and registers at a certain end, there will be user information in the cookie and Redis, as long as the user does not log out. Then you can log in at any site.

So this principle is mainly the dependency of cookie and website, top-level domain www.imooc.com and *. Imooc.com cookie value can be shared, can be carried to the back end, such as set to.imooc.com,.t.mukewang.com, That’s OK. For example, a music.imooc.com cookie cannot be shared by mtv.imooc.com. They do not affect each other. To share a cookie, set it to.imooc.com.

What about different top-level domains?

In the last section, single sign-on was based on the same top-level domain, so what if the top-level domain is different? Such aswww.imooc.comWhat if you want to share a session with www.mukewang.com? ! As shown in the figure below, the cookie at this time because of different top-level domain names, can not achieve cookie cross-domain, each site requests to the server, cookies can not be synchronized. For example, a user under www.imooc.com will have a cookie after he initiates a request, but he visits www.abc.com again. Because the cookie cannot be carried, he will ask you to log in again.

So how to meet different top-level domain names but to achieve single sign-on? Consider the following picture:

As shown in the figure above, the login between multiple systems will be verified by an independent login system, which is like an intermediary company, integrating all people. You need to see the house and get the keys after the intermediary allows, so as to realize the unified login. Central Authentication Service (CAS) is a single sign-on (SSO) solution that can be used for single sign-on between different top-level domains.

Process analysis

The first login process is as follows:

1) The user needs to log in to the restricted resource to access system A through the browser. At this time, the user checks the login and finds that the user has not logged in. Then, the user obtains the ticket and finds that there is no ticket.

2) System A discovers that the request requires login and redirects the request to the authentication center to obtain the global ticket operation. If no, log in.

3) The authentication center presents the login page, and the user logs in. After the login succeeds, the authentication center redirects the request to System A with the authentication pass token attached. At this time, the authentication center generates A global ticket.

4) At this time, the login check is carried out again, and it is found that the user has not logged in, and then the ticket operation is obtained again. At this time, the ticket (token) can be obtained. System A communicates with the authentication center to verify the validity of the token and prove that the user has logged in.

5) System A returns the limited resources to the user

When a logged in user accesses system B in the application group for the first time:

1) When the browser accesses another application B, it needs to log in to the restricted resource. At this time, the login check is performed, and the login is not found. Then, the ticket is obtained, but no ticket is found.

2) System B discovers that the request requires login, redirects the request to the authentication center, obtains the global ticket operation, obtains the global ticket, can obtain, the authentication center finds that the login.

3) The certification authority issues a temporary ticket (token) and carries the token to redirect to system B.

4) At this time, the login check is carried out again, and it is found that the user has not logged in, and then the ticket operation is obtained again. At this time, the ticket (token) can be obtained. System B communicates with the authentication center to verify the validity of the token and prove that the user has logged in.

5) System B returns the restricted resources to the client.

The meaning of the global ticket is to determine whether the user has logged in the authentication center. The meaning of a provisional note is to issue a login authentication to the user.

code


@Controller
public class SSOController {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisOperator redisOperator;

    public static final String REDIS_USER_TOKEN = "redis_user_token";
    public static final String REDIS_USER_TICKET = "redis_user_ticket";
    public static final String REDIS_TMP_TICKET = "redis_tmp_ticket";

    public static final String COOKIE_USER_TICKET = "cookie_user_ticket";

    @GetMapping("/login")
    public String login(String returnUrl, Model model, HttpServletRequest request, HttpServletResponse response) {

        model.addAttribute("returnUrl", returnUrl);

        // 1. Obtain the userTicket. If the userTicket can be obtained from the cookie, it proves that the user has logged in
        String userTicket = getCookie(request, COOKIE_USER_TICKET);

        boolean isVerified = verifyUserTicket(userTicket);
        if (isVerified) {
            String tmpTicket = createTmpTicket();
            return "redirect:" + returnUrl + "? tmpTicket=" + tmpTicket;
        }

        // 2. The user has never logged in to the CAS. If the user logs in to the CAS for the first time, the UNIFIED login page of the CAS is displayed
        return "login";
    }

    /** * Verify CAS global user ticket *@param userTicket
     * @return* /
    private boolean verifyUserTicket(String userTicket) {

        // 0. Verify that the CAS ticket cannot be empty
        if (StringUtils.isBlank(userTicket)) {
            return false;
        }

        // 1. Verify whether the CAS ticket is valid
        String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
        if (StringUtils.isBlank(userId)) {
            return false;
        }

        // 2. Verify that the user session corresponding to the ticket exists
        String userRedis = redisOperator.get(REDIS_USER_TOKEN + ":" + userId);
        if (StringUtils.isBlank(userRedis)) {
            return false;
        }

        return true;
    }

    /** * Unified login interface of CAS * Destination: * 1. Create a global session for the user after login -> uniqueToken * 2. Create a global ticket for a user to indicate whether to log in to the CAS server -> userTicket * 3. Create a temporary ticket for the user to use for boundback -> tmpTicket */
    @PostMapping("/doLogin")
    public String doLogin(String username, String password, String returnUrl, Model model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        model.addAttribute("returnUrl", returnUrl);

        // 0. Check that the user name and password are not empty
        if (StringUtils.isBlank(username) ||
                StringUtils.isBlank(password)) {
            model.addAttribute("errmsg"."Username or password cannot be empty.");
            return "login";
        }

        // 1. Log in
        Users userResult = userService.queryUserForLogin(username,
                MD5Utils.getMD5Str(password));
        if (userResult == null) {
            model.addAttribute("errmsg"."Incorrect user name or password");
            return "login";
        }

        // 2. Implement the user's Redis session
        String uniqueToken = UUID.randomUUID().toString().trim();
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(userResult, usersVO);
        usersVO.setUserUniqueToken(uniqueToken);
        redisOperator.set(REDIS_USER_TOKEN + ":" + userResult.getId(),
                JsonUtils.objectToJson(usersVO));

        // 3. Generate a ticket ticket, a global ticket, indicating that the user has logged in to the CAS
        String userTicket = UUID.randomUUID().toString().trim();

        // 3.1 The global ticket of the user must be placed in the cookie of the CAS
        setCookie(COOKIE_USER_TICKET, userTicket, response);

        // 4. UserTicket is associated with the user ID and added to redis, indicating that the user has a ticket and can play in various scenic spots
        redisOperator.set(REDIS_USER_TICKET + ":" + userTicket, userResult.getId());

        // 5. Generate a temporary ticket and jump back to the website of the calling end. It is a one-time temporary ticket issued by the CAS end
        String tmpTicket = createTmpTicket();

        /** * userTicket: indicates the login status of a user on the CAS. The user has logged in. * tmpTicket: indicates the ticket issued to the user for one-time authentication

        /** * for example: * We went to the zoo to play, and bought a unified ticket at the gate, this is the CAS system global ticket and user global session. * There are some small attractions in the zoo. You need to get a one-time ticket with your ticket. With this ticket, you can go to some small attractions. * Such a small scenic spots are actually the corresponding sites here. * When we have finished using the temporary note, we need to destroy it. * /

// return "login";
        return "redirect:" + returnUrl + "? tmpTicket=" + tmpTicket;
    }


    @PostMapping("/verifyTmpTicket")
    @ResponseBody
    public IMOOCJSONResult verifyTmpTicket(String tmpTicket, HttpServletRequest request, HttpServletResponse response) throws Exception {

        // Use a one-time temporary ticket to verify that the user is logged in, and if so, return the user session information to the site
        // After the use, the provisional note needs to be destroyed
        String tmpTicketValue = redisOperator.get(REDIS_TMP_TICKET + ":" + tmpTicket);
        if (StringUtils.isBlank(tmpTicketValue)) {
            return IMOOCJSONResult.errorUserTicket("Customer ticket exception");
        }

        // 0. If the temporary ticket is OK, you need to destroy it and obtain the global userTicket in the CAS cookie to obtain the user session
        if(! tmpTicketValue.equals(MD5Utils.getMD5Str(tmpTicket))) {return IMOOCJSONResult.errorUserTicket("Customer ticket exception");
        } else {
            // Destroy the provisional notes
            redisOperator.del(REDIS_TMP_TICKET + ":" + tmpTicket);
        }

        // 1. Verify and obtain the userTicket of the user
        String userTicket = getCookie(request, COOKIE_USER_TICKET);
        String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
        if (StringUtils.isBlank(userId)) {
            return IMOOCJSONResult.errorUserTicket("Customer ticket exception");
        }

        // 2. Verify that the user session corresponding to the ticket exists
        String userRedis = redisOperator.get(REDIS_USER_TOKEN + ":" + userId);
        if (StringUtils.isBlank(userRedis)) {
            return IMOOCJSONResult.errorUserTicket("Customer ticket exception");
        }

        // If the authentication succeeds, return OK with the user session
        return IMOOCJSONResult.ok(JsonUtils.jsonToPojo(userRedis, UsersVO.class));
    }

    @PostMapping("/logout")
    @ResponseBody
    public IMOOCJSONResult logout(String userId, HttpServletRequest request, HttpServletResponse response) throws Exception {

        // 0. Obtain the CAS user ticket
        String userTicket = getCookie(request, COOKIE_USER_TICKET);

        // 1. Clear the userTicket, redis/cookie
        deleteCookie(COOKIE_USER_TICKET, response);
        redisOperator.del(REDIS_USER_TICKET + ":" + userTicket);

        // 2. Clear the user's global session (distributed session)
        redisOperator.del(REDIS_USER_TOKEN + ":" + userId);

        return IMOOCJSONResult.ok();
    }

    /** * Create temporary note *@return* /
    private String createTmpTicket(a) {
        String tmpTicket = UUID.randomUUID().toString().trim();
        try {
            redisOperator.set(REDIS_TMP_TICKET + ":" + tmpTicket,
                    MD5Utils.getMD5Str(tmpTicket), 600);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return tmpTicket;
    }

    private void setCookie(String key, String val, HttpServletResponse response) {

        Cookie cookie = new Cookie(key, val);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        response.addCookie(cookie);
    }

    private void deleteCookie(String key, HttpServletResponse response) {

        Cookie cookie = new Cookie(key, null);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        cookie.setMaxAge(-1);
        response.addCookie(cookie);
    }

    private String getCookie(HttpServletRequest request, String key) {

        Cookie[] cookieList = request.getCookies();
        if (cookieList == null || StringUtils.isBlank(key)) {
            return null;
        }

        String cookieValue = null;
        for (int i = 0 ; i < cookieList.length; i ++) {
            if (cookieList[i].getName().equals(key)) {
                cookieValue = cookieList[i].getValue();
                break; }}returncookieValue; }}Copy the code

Reference links:

Blog.csdn.net/no_game_no_… www.jianshu.com/p/75edcc05a…