All front and back ends are separated, let's not do page jumps! All JSON interaction


This is the fourth article in this series. Some friends can't find the previous article. Songge will give you an index:

  1. Dig a big hole and Spring Security will start!
  2. Song Ge takes you through Spring Security, do n’t ask how to decrypt the password
  3. Teach you to customize the form login in Spring Security

Two days ago, a small friend asked Songge on WeChat. After the front-end and back-end development were separated, did the authentication use traditional sessions or tokens like JWT to solve it?

This does represent two different directions.

The traditional way of recording user authentication information through session can be understood as a stateful login, and JWT represents a stateless login. There may be some friends who are not familiar with this concept, so I will first come to the popular science stateful and stateless login.

1. Stateless login

1.1 What is stateful

Stateful service, that is, the server needs to record the client information of each session, so as to identify the client's identity, and process the request according to the user's identity. A typical design is the Session in Tomcat. For example, login: After the user logs in, we save the user's information in the server session, and give the user a cookie value, record the corresponding session, and then the next request, the user carries the cookie value (this step is automatically completed by the browser) , We can identify the corresponding session to find the user's information. This method is currently the most convenient, but it also has some shortcomings, as follows:

  • The server saves a lot of data, increasing the pressure on the server
  • The server saves user status, and does not support clustered deployment

1.2 What is stateless

Each service in the microservice cluster provides a RESTful interface to the outside world. One of the most important specifications of the RESTful style is: the statelessness of services, namely:

  • The server does not save any client requester information
  • Each request of the client must have self-describing information, through which to identify the identity of the client

So what are the benefits of this statelessness?

  • The client request does not depend on the server information, multiple requests do not need to access the same server
  • The server cluster and status are transparent to the client
  • The server can be arbitrarily migrated and scaled (clusterized deployment is convenient)
  • Reduce server storage pressure

1.3 How to achieve statelessness

Stateless login process:

  • First, the client sends the account name / password to the server for authentication
  • After the authentication is passed, the server encrypts and encodes the user information into a token and returns it to the client
  • In the future, every time the client sends a request, it needs to carry the authenticated token
  • The server decrypts the token sent by the client, determines whether it is valid, and obtains user login information

1.4 Respective advantages and disadvantages

The biggest advantage of using session is convenience. You don't have to do too much processing, everything is the default. Song Ge's previous articles in this series are also based on session.

But there is another fatal problem when using session. If your front end is Android, iOS, applet, etc., these apps naturally have no cookies. If you want to use session, you need these engineers to adapt on their respective devices Generally, it is a simulation cookie. From this perspective, today, when mobile apps are everywhere, we simply rely on sessions for security management. It seems that it is not particularly ideal.

At this time, stateless login such as JWT shows its advantages. The tokens that these login methods rely on can be passed through ordinary parameters or through the request header. Whatever works, it has strong flexibility.

But having said that, if your front-end and back-end separation is just a web page + server, there is no need to log in statelessly. You can do it based on session, which is convenient and convenient.

Okay, so much, let me talk to you about session-based authentication first. After logging in to JWT, I will elaborate with you. If your friends ca n’t wait, you can also take a look Of JWT's tutorial: Spring Security combined with Jwt to achieve stateless login .

2. Login interaction

In the last article , Songge and you all dealt with common login parameter configuration problems. For the success and failure of login, we have left a callback function not mentioned, this article will come to discuss with you in detail.

2.1 Separate data interaction between front and back end

Under the development architecture of front-end and back-end separation, the front-end and back-end interactions are performed through JSON. No matter whether the login succeeds or fails, there will be no server-side jump or client-side jump.

After the login is successful, the server returns a piece of JSON indicating that the login is successful to the front end. After the front end receives it, it is necessary to jump to the display, which is determined by the front end itself, and it has nothing to do with the back end.

If the login fails, the server will return a piece of login failure prompt JSON to the front-end. After the front-end receives it, the display of the jump should be decided by the front-end itself, and it has nothing to do with the back-end.

First of all, this idea is determined. Based on this idea, let's take a look at the login configuration.

2.2 Successful login

Previously, we configured the successful login processing through the following two methods:

  • defaultSuccessUrl
  • successForwardUrl

Both of these are configured with jump addresses, and are suitable for development regardless of front and back ends. In addition to these two methods, there is another nirvana, that is successHandler.

The function of successHandler is very powerful, and even includes the functions of defaultSuccessUrl and successForwardUrl. Let's take a look:

.successHandler((req, resp, authentication) -> {
    Object principal = authentication.getPrincipal();
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write(new ObjectMapper().writeValueAsString(principal));
    out.flush();
    out.close();
})

The parameter of the successHandler method is an AuthenticationSuccessHandler object. The method we want to implement in this object is onAuthenticationSuccess.

The onAuthenticationSuccess method has three parameters, which are:

  • HttpServletRequest
  • HttpServletResponse
  • Authentication

With the first two parameters, we can return the data as we like. Using HttpServletRequest we can do server-side jumps, using HttpServletResponse we can do client-side jumps, and of course, we can also return JSON data.

The third Authentication parameter saves the user information we just logged in successfully.

After the configuration is complete, we log in again, and we can see that the user information of the successful login is returned to the front end through JSON, as follows:

Of course, the user's password has been erased. The problem of erasing the password, Song brother has shared with you before, you can refer to this article: take you through the Spring Security login process

2.3 Login failed

There is a similar callback for failed login, as follows:

.failureHandler((req, resp, e) -> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write(e.getMessage());
    out.flush();
    out.close();
})

The callback for failure is also three parameters, the first two are needless to say, the third is an Exception, there are different reasons for the login failure, Exception saves the reason for the login failure, we can return it through JSON To the front end.

Of course, everyone also sees that in micro-personnel, I have also identified the types of exceptions one by one. According to different types of exceptions, we can give users a more clear reminder:

resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanceof LockedException) {
    respBean.setMsg("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
    respBean.setMsg("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
    respBean.setMsg("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
    respBean.setMsg("账户被禁用,请联系管理员!");
} else if (e instanceof BadCredentialsException) {
    respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();

There is one point to note here.

We know that when a user logs in, the user name or password is entered incorrectly, we generally only give a vague prompt that the user name or password is entered incorrectly, please re-enter , and will not give a clear such as "user name entered incorrectly" Or a precise prompt like "Password input error", but for many novice friends who do not know how to do it, he may give a clear error prompt, which will bring risks to the system.

But after using a security management framework like Spring Security, even if you are a novice, you will not make such a mistake.

In Spring Security, the exception corresponding to a user name lookup failure is:

  • UsernameNotFoundException

The exception corresponding to the password matching failure is:

  • BadCredentialsException

However, in the callback of the login failure, we always cannot see the UsernameNotFoundException exception. No matter the user name or password is entered incorrectly, the exception thrown is BadCredentialsException.

Why is this? Song Ge introduced in the previous article to take you through the Spring Security login process . There is a key step in the login, which is to load user data. Let's take a look at this method (part):

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	try {
		user = retrieveUser(username,
				(UsernamePasswordAuthenticationToken) authentication);
	}
	catch (UsernameNotFoundException notFound) {
		logger.debug("User '" + username + "' not found");
		if (hideUserNotFoundExceptions) {
			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
		else {
			throw notFound;
		}
	}
}

From this code, we can see that when searching for users, if UsernameNotFoundException is thrown, this exception will be caught. After the capture, if the value of the hideUserNotFoundExceptions property is true, a BadCredentialsException will be thrown. This is equivalent to hiding the UsernameNotFoundException exception, and by default, the value of hideUserNotFoundExceptions is true.

Everyone sees here why you are receiving BadCredentialsException no matter the user or password is wrong.

Generally speaking, this configuration does not need to be modified. If you must distinguish between UsernameNotFoundException and BadCredentialsException, here are three ideas for you:

  1. Define DaoAuthenticationProvider instead of the system default, and set the hideUserNotFoundExceptions property to false when defining it.
  2. When the user name search fails, the UsernameNotFoundException exception is not thrown, but a custom exception is thrown, so that the custom exception will not be hidden, and then the front-end user is prompted according to the custom exception information in the callback of the login failure.
  3. When the user name search fails, a BadCredentialsException is directly thrown, but the exception information is "the user name does not exist".

The three ideas are for reference only by friends. Unless the circumstances are special, you do not need to modify the default behavior of this piece.

What are the benefits of this? Obviously, developers can be forced to give a vague exception hint, so that even novices who do not know how to do so will not put the system at risk.

Well, after this configuration is complete, no matter whether the login is successful or failed, the backend will only return JSON to the frontend.

3. Uncertified treatment plan

What about uncertified?

Some friends say that it is not easy, just access the data without authentication, and just redirect to the login page. That's right, the system's default behavior is the same.

However, in the separation of front and back ends, this logic is obviously problematic. If the user does not log in and visits a page that requires authentication before accessing, at this time, we should not let the user redirect to the login page, but give the user a After the login prompt, the front-end receives the prompt, and then decides to jump to the page.

To solve this problem, it involves an interface in Spring Security AuthenticationEntryPoint, the interface has an implementation class:, LoginUrlAuthenticationEntryPointthere is a method in this class commence, as follows:

/**
 * Performs the redirect (or forward) to the login form URL.
 */
public void commence(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException authException) {
	String redirectUrl = null;
	if (useForward) {
		if (forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			if (logger.isDebugEnabled()) {
				logger.debug("Server side forward to: " + loginForm);
			}
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}
	else {
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

First of all, we can see from the comment of this method that this method is used to decide whether to redirect or forward. Through Debug tracking, we found that the value of useForward is false by default, so the request goes into the redirect .

Then our idea to solve the problem is very simple, rewrite this method directly, just return JSON in the method, no longer do the redirect operation, the specific configuration is as follows:

.csrf().disable().exceptionHandling()
.authenticationEntryPoint((req, resp, authException) -> {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            out.write("尚未登录,请先登录");
            out.flush();
            out.close();
        }
);

In Spring Security configuration plus custom AuthenticationEntryPointprocessing method, the method returns directly to the appropriate prompts to JSON. In this way, if the user directly accesses a request that can be accessed after authentication, the redirect operation will not occur, and the server will directly send a JSON prompt to the browser. After the browser receives the JSON, why should .

4. Log out

Finally, let's take a look at the processing solution for logout.

We have said before that, according to the previous configuration, after logging out, the system automatically jumps to the login page, which is also inappropriate. If it is a front-end and back-end separation project, you can return JSON after successful logout.

.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler((req, resp, authentication) -> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("注销成功");
    out.flush();
    out.close();
})
.permitAll()
.and()

In this way, after the logout is successful, the front end also receives JSON:

Well, this article will introduce the common JSON interaction problems in front-end and back-end separation with friends. If friends find the article helpful, please remember to click to read it.

Published 574 original articles · praised 6896 · visits 4.76 million +

Guess you like

Origin blog.csdn.net/u012702547/article/details/105261826