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
Then modify the configuration information in server and samples respectively.
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.
Access test:
After one node successfully logs in, other nodes can access it.
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.
Introduce the same dependencies
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:
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.
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:
The logic in the original queryUser is:
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
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.
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.
Verify when submitting request
Done
3. JWT implementation
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.
Problems:
- Each user needs to make a record, and the Session will generally be stored in memory, which increases the server overhead.
- In a cluster environment, Session needs to be synchronized or processed by distributed Session.
- Because it is transmitted based on cookies, if the cookies are decrypted, users are vulnerable to CSRF attacks.
- Projects that separate front-end and back-end will be more troublesome
1.3 JWT-based authentication
The specific process is as follows:
Certification process:
- 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.
- 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:
- Introduction: It can be sent through URL, POST parameters or HTTP header, because the data volume is small and the transmission speed is fast.
- Self-contained: The payload contains all the information required by the user to avoid multiple data queries.
- Kua Voice: Saved on the client in JSON format.
- 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.
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; }
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; }
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
normal access
Visit again after the token expires
Done