Single sign-on principle and JWT implementation

Single sign-on principle and JWT implementation

1. Single sign-on effect

First, let’s look at a specific case to deepen our understanding of single sign-on. Case address: https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search Import the case code directly into IDEA

image.png

Then modify the configuration information in server and samples respectively.

image.png

image.png

Configure in host file

127.0.0.1 sso.server.com
127.0.0.1 client1.com
127.0.0.1 client2.com

Then start the server and two simple services respectively.

image.png

Access test:

image.png

After one node successfully logs in, other nodes can access it.

image.png

Test it yourself.

2. Single sign-on implementation

After knowing the effect of single sign-on, we can create a single sign-on implementation ourselves. Let’s deepen our understanding of single sign-on.

1. Create project

Create an aggregation project through Maven, and then create three sub-modules in the project, namely the authentication service and the client module.

image.png

Introduce the same dependencies

image.png

2.client1

We first provide relevant interfaces in client1. We provide an interface for anonymous access and an interface that requires authentication for access.

@Controller
public class UserController {<!-- -->

    @ResponseBody
    @GetMapping("/hello")
    public String hello(){<!-- -->
        return "hello";
    }

    @GetMapping("/queryUser")
    public String queryUser(Model model){<!-- -->
        model.addAttribute("list", Arrays.asList("Zhang San", "Li Si", "Wang Wu"));
        return "user";
    }
}

The code in user.html is:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>$Title$</title>
</head>
<body>
  <h1>User management:</h1>
    <ul>
      <li th:each="user:${list}">
        [[${user}]]
      </li>
    </ul>
</body>
</html>

Access test:

image.png

It can be accessed without authentication, so verification logic must be added.

 @GetMapping("/queryUser")
    public String queryUser(Model model, HttpSession session){<!-- -->
        Object userLogin = session.getAttribute("userLogin");
        if(userLogin != null){<!-- -->
            // It means you have logged in, just let it go.
            model.addAttribute("list", Arrays.asList("Zhang San", "Li Si", "Wang Wu"));
            return "user";
        }
        // Indicates that you are not logged in and need to jump to the authentication server for authentication. In order to jump back to the current page after successful login, pass parameters
        return "redirect:http://sso.server:8080/loginPage?redirect=http://client1.com:8081/queryUser";
    }

You can see that when we access the queryUser request, because we are not logged in, we will be redirected to the service in the authentication service for login processing. At this time, you need to enter the server service for processing.

3.server service

On the server side we need to provide two interfaces, one adjusted to the login interface, one to handle the authentication logic and a login page

@Controller
public class LoginController {<!-- -->

    /**
     * Logic to jump to login interface
     * @return
     */
    @GetMapping("/loginPage")
    public String loginPage(@RequestParam(value = "redirect" ,required = false) String url, Model model){<!-- -->
        model.addAttribute("url",url);
        return "login";
    }

    /**
     * Handle login requests
     * @return
     */
    @PostMapping("/ssoLogin")
    public String login(@RequestParam("userName") String userName,
                        @RequestParam("password") String password,
                        @RequestParam(value = "url",required = false) String url){<!-- -->
        if("zhangsan".equals(userName) & amp; & amp; "123".equals(password)){<!-- -->
            // login successful
            return "redirect:" + url;
        }
        //Return to the login page if login fails
        return "redirect:loginPage";
    }

}

Login page code logic

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>sso-server-login</title>
</head>
<body>
   <h1>Server login page</h1>
  <form action="/ssoLogin" method="post" >
      Account:<input type="text" name="userName" ><br/>
      Password:<input type="password" name="password"><br/>
      <input type="hidden" name="url" th:value="${url}">
      <input type="submit" value="Submit">
  </form>
</body>
</html>

Then when we access services that require authentication in client1, we will jump to the login interface.

image.png

Submit the login operation. When we submit a successful login, we should be redirected to the original access address, but the actual situation is a little different from what we thought:

image.png

The logic in the original queryUser is:

image.png

4. Authentication certificate

The above problem is that we successfully logged in to the authentication service, but client1 does not know that the login was successful, so after successful authentication, client1 needs to be given a certificate indicating successful authentication. That is, Token information.

 /**
     * Handle login requests
     * @return
     */
    @PostMapping("/ssoLogin")
    public String login(@RequestParam("userName") String userName,
                        @RequestParam("password") String password,
                        @RequestParam(value = "url",required = false) String url){<!-- -->
        if("zhangsan".equals(userName) & amp; & amp; "123".equals(password)){<!-- -->
            // Generate Token information through UUID
            String uuid = UUID.randomUUID().toString().replace("-","");
            // Store the generated information in the Redis service
            redisTemplate.opsForValue().set(uuid,"zhangsan");
            // login successful
            return "redirect:" + url + "?token=" + uuid;
        }
        //Return to the login page if login fails
        return "redirect:loginPage";
    }

The generated Token is synchronously saved in Redis, and then the token information is carried in the redirected address. Then process it in client1

@GetMapping("/queryUser")
    public String queryUser(Model model,
                            HttpSession session,
                            @RequestParam(value = "token",required = false) String token){<!-- -->
        if(token != null){<!-- -->
            // The token has value, indicating that it has been authenticated.
            // TODO Go to the server to obtain user information based on the token
            session.setAttribute("userLogin","Zhang San");
        }

        Object userLogin = session.getAttribute("userLogin");
        if(userLogin != null){<!-- -->
            // It means you have logged in, just let it go.
            model.addAttribute("list", Arrays.asList("Zhang San", "Li Si", "Wang Wu"));
            return "user";
        }
        // Indicates that you are not logged in and need to jump to the authentication server for authentication. In order to jump back to the current page after successful login, pass parameters
        return "redirect:http://sso.server.com:8080/loginPage?redirect=http://client1.com:8081/queryUser";
    }

Then we can access the service in client1

image.png

5. client2

Controller logic:

@Controller
public class OrderController {<!-- -->

    @GetMapping("/order")
    public String getOrder(HttpSession session, Model model){<!-- -->
        Object userLogin = session.getAttribute("userLogin");
        if(userLogin != null){<!-- -->
            // Indicates that it is authenticated
            model.addAttribute("list", Arrays.asList("order1","order2","order3"));
            return "order";
        }
        return "redirect:http://sso.server.com:8080/loginPage?redirect=http://client2.com:8082/order";
    }
}

order.html page content:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>$Title$</title>
</head>
<body>
  <h1>Order management:</h1>
    <ul>
      <li th:each="order:${list}">
        [[${order}]]
      </li>
    </ul>
</body>
</html>

Through the previous introduction, we can find that clent1 can be accessed after authentication, but when client2 submits a request, it will still jump to the server service for authentication processing.

image.png

The reason for this is that client1 saves the authentication information in the Session after successful authentication, but it cannot be obtained by client2. At this time, we can store a token information in the browser’s Cookie after the Server service login is successful, and then In the interface service before other services jump to the login page, determine whether there is a value in the cookie. If there is, it will be considered that other services have logged in and let it go.

image.png

Verify when submitting request

image.png

image.png

Done

3. JWT implementation

image.png

1.JWT introduction

1.1 What is JWT

Official: JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA .

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as JSON objects. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (using the HMAC algorithm) or using a public/private key pair using RSA or ECDSA.

Popular explanation: JWT is short for JSON Web Token, that is, JSON form is used as token information in Web applications. It is used to safely transmit information as a JSON object between parties. Data encryption can be completed during the data transmission process. , signature and other operations.

1.2 Session-based authentication

The authentication method we first come into contact with is the Session-based authentication method. Each session will be stored in the HttpSession on the server side, which is equivalent to a Map, and then a jsessionid is returned to the client in the form of a cookie, and then each visit It needs to be obtained from the HttpSession based on the jsessionid, and this logic is used to determine whether it is in the authenticated state.

image.png

Problems:

  1. Each user needs to make a record, and the Session will generally be stored in memory, which increases the server overhead.
  2. In a cluster environment, Session needs to be synchronized or processed by distributed Session.
  3. Because it is transmitted based on cookies, if the cookies are decrypted, users are vulnerable to CSRF attacks.
  4. Projects that separate front-end and back-end will be more troublesome

1.3 JWT-based authentication

The specific process is as follows:

image.png

Certification process:

  1. After the user submits the account and password to the back-end service through the form, if the authentication is successful, a corresponding Token information will be generated.
  2. After that, the user’s request for resources will carry this Token value. After the backend obtains it, it will be released if it passes the verification, or it will be rejected if it does not pass the verification.

Advantages of jwt:

  1. Introduction: It can be sent through URL, POST parameters or HTTP header, because the data volume is small and the transmission speed is fast.
  2. Self-contained: The payload contains all the information required by the user to avoid multiple data queries.
  3. Kua Voice: Saved on the client in JSON format.
  4. There is no need for the server to save information, so it is suitable for distributed environments.

1.4 Structure of JWT

Token composition:

  • Header
  • Payload
  • Signature

So the format of JWT is: xxxx.yyyy.zzzz Header.Payload.Signature

Header:

Header usually consists of two parts: the type of token [JWT] and the signature algorithm used. For example, HMAC, SHA256 or RSA, which will use Base64 encoding to form the first part of the JWT structure. Note: Base64 is an encoding and can be translated back to its original form.

{<!-- -->
   "alg":"HS256",
   "typ":"JWT"
}

Payload:

The second part of the token is the payload, which contains the claim. The claim is a claim about the entity (usually user information) and other data. It is encoded using Base64 and forms the second part of the JWT structure.

{<!-- -->
"userId":"123",
"userName":"Bobo Roast Duck",
"admin":true
}

Because it will be encoded by Base64, do not write sensitive information in the payload.

Signature:

Signature part, the first two parts are encoded using Base64, that is, the front end can decode the information in the header and payload. Signature needs to use the encoded header and payload and a secret key we provide, and then use the header specified in The previous algorithm (HS256) is used to sign. The purpose of the signature is to ensure that the JWT has not been tampered with.

image.png

2.JWT implementation

2.1 Basic implementation of JWT

Generate Token token

 /**
     * Generate Token information
     */
    @Test
    void generatorToke() {<!-- -->
        Map<String,Object> map = new HashMap<>();
        map.put("alg","HS256");
        map.put("typ","JWT");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND,60);
        String token = JWT.create()
                .withHeader(map) //Set header
                .withClaim("userid", 666) // Set payload
                //Set expiration time
                .withExpiresAt(calendar.getTime())
                .withClaim("username", "Bobo Roast Duck") // Set payload
                .sign(Algorithm.HMAC256("qwaszx")); // Set signature to keep confidential
        System.out.println(token);
    }

Verify whether it is correct based on the Token.

 /**
     * Verify Token information
     */
    @Test
    public void verifier(){<!-- -->
        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTMwNTE5ODUsInVzZXJpZCI6NjY2LCJ1c2VybmFtZSI6IuazouazoueDpOm4rSJ9.0LW5MFihMeYNfRfez0a68ncaKQ13j 5pSnVZTB7m1CDw";
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("qwaszx")).build();
        DecodedJWT verify = jwtVerifier.verify(token);
        System.out.println(verify.getClaim("userid").asInt());
        System.out.println(verify.getClaim("username").asString());
    }

Exception information of the scenario during verification:

  • SignatureVerificationException Signature inconsistency exception
  • TokenExpiredException Token expired exception
  • AlgorithmMismatchException algorithm mismatch exception
  • InvalidClaimException invalid payload exception

2.2 JWT encapsulation

In order to simplify the operation, we can further encapsulate the above operation to simplify the processing.

package com.bobo.jwt.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

/**
 * Tool class for JWT operation
 */
public class JWTUtils {<!-- -->
    private static final String SING = "123qwaszx";

    /**
     * Generate Token header.payload.sing composition
     * @return
     */
    public static String getToken(Map<String,String> map){<!-- -->
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7); //Default expiration time is 7 days
        JWTCreator.Builder builder = JWT.create();
        // payload settings
        map.forEach((k,v)->{<!-- -->
            builder.withClaim(k,v);
        });
        // Generate Token and return
        return builder.withExpiresAt(instance.getTime())
                    .sign(Algorithm.HMAC256(SING));
    }

    /**
     * Verify Token
     * @return
     * DecodedJWT can be used to obtain user information
     */
    public static DecodedJWT verify(String token){<!-- -->
        // If no exception is thrown, the verification passes, otherwise the verification fails.
        return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
    }
}

2.3 SpringBoot application

First of all, in the login method, if the login is successful, we need to generate the corresponding Token information, and then respond to the Token information to the client.

@PostMapping("/login")
    public Map<String,Object> login(User user){<!-- -->
        Map<String,Object> res = new HashMap<>();
        if("zhang".equals(user.getUserName()) & amp; & amp; "123".equals(user.getPassword())){<!-- -->
            // login successful
            Map<String,String> map = new HashMap<>();
            map.put("userid","1");
            map.put("username","zhang");
            String token = JWTUtils.getToken(map);
            res.put("flag",true);
            res.put("msg","Login successful");
            res.put("token",token);
            return res;
        }
        res.put("flag",false);
        res.put("msg","Login failed");
        return res;
    }

image.png

Then the user needs to carry the token information when submitting a request, and then we need to verify the token before processing the request in the controller. If the verification passes, continue processing the request, otherwise intercept the request.

 @PostMapping("/queryUser")
    public Map<String,Object> queryUser(@RequestParam("token") String token){<!-- -->
        // Verify before obtaining the information
        Map<String,Object> map = new HashMap<>();
        try{<!-- -->
            DecodedJWT verify = JWTUtils.verify(token);
            map.put("state",true);
            map.put("msg","Request successful");
            return map;
        }catch (SignatureVerificationException e){<!-- -->
            e.printStackTrace();
            map.put("msg","Invalid signature");
        }catch (TokenExpiredException e){<!-- -->
            e.printStackTrace();
            map.put("msg","Token expired");
        }catch (AlgorithmMismatchException e){<!-- -->
            e.printStackTrace();
            map.put("msg","Algorithms are inconsistent");
        }catch (Exception e){<!-- -->
            e.printStackTrace();
            map.put("msg","Token is invalid");
        }
        map.put("state",false);
        return map;
    }

image.png

But in the above situation, we see that a large amount of Token verification code and increased redundant code have been added to the controller. At this time, we can consider placing the Token verification code in the interceptor. We create a custom interceptor.

/**
 * Custom interceptor
 * Verify whether the Token information is carried under certain circumstances, and reject it directly if it is not carried
 * Then verify the validity of the Token
 */
public class JWTInterceptor implements HandlerInterceptor {<!-- -->

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {<!-- -->
        String token = request.getParameter("token");
        // Verify before obtaining the information
        Map<String,Object> map = new HashMap<>();
        try{<!-- -->
            DecodedJWT verify = JWTUtils.verify(token);
            return true;
        }catch (SignatureVerificationException e){<!-- -->
            e.printStackTrace();
            map.put("msg","Invalid signature");
        }catch (TokenExpiredException e){<!-- -->
            e.printStackTrace();
            map.put("msg","Token expired");
        }catch (AlgorithmMismatchException e){<!-- -->
            e.printStackTrace();
            map.put("msg","Algorithms are inconsistent");
        }catch (Exception e){<!-- -->
            e.printStackTrace();
            map.put("msg","Token is invalid");
        }
        map.put("state",false);
        //Convert Map to JSON response
        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        return false;
    }
}

To make the interceptor effective, we also need to add the corresponding configuration class.

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {<!-- -->

    @Override
    public void addInterceptors(InterceptorRegistry registry) {<!-- -->
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/queryUser") // Requests that need to be intercepted
                .addPathPatterns("/saveUser") // Requests that need to be intercepted
                .excludePathPatterns("/login"); // Requests that need to be excluded
    }
}

Then add a test method /saveUser

 @PostMapping("/saveUser")
    public String saveUser(){<!-- -->
        System.out.println("---------------->");
        return "success";
    }

Test access and shorten the expiration time to 1 minute

image.png

normal access

image.png

Visit again after the token expires

image.png

Done