An article takes you to use Spring Security to complete the front-end separation and use JSON for interaction

1. Stateless login

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 like 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 drawbacks, as follows:

The server saves a large amount of data, which increases the pressure on the
server. The server saves the user state and does not support clustered deployment

2. What is stateless

Each service in the microservice cluster uses a RESTful style interface for external provision. And one of the most important specifications of the RESTful style is the statelessness of services, namely:

The server does not store any client requester information
. Each client request must have self-descriptive information, and the client's identity can be identified through this information

So what are the benefits of this statelessness?

Client requests do not rely on server information, and multiple requests do not need to access the same server
. The cluster and state of the server are transparent to the
client. The server can be migrated and scaled arbitrarily (cluster deployment is convenient) to
reduce the server Storage pressure

3. How to achieve statelessness

The process of stateless login:

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, which is returned to the client.
After the client sends a request, it needs to carry the authentication token
server pair. The token sent by the client is decrypted, judged whether it is valid, and the user login information is obtained

4. Their advantages and disadvantages

(1) The biggest advantage of using session is convenience. You don't need to do too much processing, everything is the default.

(2) But there is another fatal problem when using session is that if your front end is Android, iOS, applet, etc., these apps naturally do not have cookies. If you have to use session, you need these engineers to do it on their devices. Adaptation is generally to simulate cookies. From this perspective, today when mobile apps are blooming everywhere, our pure reliance on sessions for security management does not seem to be particularly ideal.

(3) At this time, stateless logins such as JWT show their advantages. The tokens that these login methods rely on can be passed through ordinary parameters or through request headers. Anything is fine, and it is very flexible. Sex.

(4) But having said that, if your front-end and back-end separation is only web + server, there is no need to log in statelessly, just do it based on session, which saves trouble and is convenient.

Two, login interaction

1. Data interaction between front and back ends

Under the development architecture where the front and back ends are separated, the interaction between the front and back ends is carried out through JSON. No matter whether the login is successful or failed, there will be no server-side jump or client-side jump.

After the login is successful, the server will return a JSON indicating that the login is successful to the front-end. After the front-end receives it, the display should be jumped to.

If the login fails, the server will return a JSON indicating the login failure to the front-end. After the front-end receives it, the display that should be redirected is determined by the front-end and 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. Successful login

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

defaultSuccessUrl
successForwardUrl

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

successHandlerFunction is very powerful, and even have included defaultSuccessUrland successForwardUrlfeatures. 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();
})

successHandlerParameter method is an AuthenticationSuccessHandlerobject that the method we want to achieve is onAuthenticationSuccess.

The onAuthenticationSuccess method has three parameters, namely:

HttpServletRequest
HttpServletResponse
Authentication

With the first two parameters, we can return data here as we wish. Use HttpServletRequestwe can do server-side jumps, use HttpServletResponseour client can do the jump, of course, can also return JSON data.

The third Authenticationargument is that we have just saved the user information login success.

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

Insert picture description here
The user’s password erasure problem can be referred to: An article takes you through the login process of Spring Security

3. Login Wipe

There is a similar callback for login failure, 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 failed callback is also three parameters. Needless to say, the first two are. The third is an Exception. For login failures, there will be different reasons. The Exception contains the reason for the login failure. We can return it through JSON. To the front end.

Of course, you have also seen that in the micro-personnel, I have also identified the abnormal types one by one. According to different abnormal types, 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("账户被锁定,请联系管理员!");
} elseif (e instanceof CredentialsExpiredException) {
    
    
    respBean.setMsg("密码过期,请联系管理员!");
} elseif (e instanceof AccountExpiredException) {
    
    
    respBean.setMsg("账户过期,请联系管理员!");
} elseif (e instanceof DisabledException) {
    
    
    respBean.setMsg("账户被禁用,请联系管理员!");
} elseif (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 the user logs in, the user name or password is entered incorrectly, we generally only give a vague prompt, that is, "user name or password entered incorrectly, please re-enter", instead of giving a clear such as "user name input Accurate prompts such as "error" or "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 the user name lookup failure is:

UsernameNotFoundException

The exception corresponding to the password matching failure is:

BadCredentialsException

But we failed login callback, but always see UsernameNotFoundExceptionabnormal, regardless of the user name or password is entered incorrectly, an exception is thrown BadCredentialsException.

Why is this? In an article with Spring Security you get the login process introduced, there is a critical step in the logon, the user data is to be loaded, let's look at this method brought out (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) {
    
    
			thrownew BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
		else {
    
    
			throw notFound;
		}
	}
}

From this code, we see that when looking for a user, if thrown UsernameNotFoundException, the exception is captured, after capture, if the hideUserNotFoundExceptionsproperty is true, it throws a BadCredentialsException. Equivalent to UsernameNotFoundExceptionabnormal hide, and by default, hideUserNotFoundExceptionsthe value will be true.

We see here to understand why regardless of the user or wrong password, you receive are BadCredentialsExceptionabnormal.

Insert picture description here

In general this configuration is not required to modify, if you must distinguish out UsernameNotFoundExceptionand BadCredentialsExceptionI are here to offer three ideas:

(1) their definition DaoAuthenticationProviderinstead of the default, when defining the hideUserNotFoundExceptionsproperty is set to false.
(2) When the user name lookup fails, do not throw UsernameNotFoundExceptionan exception, but throw a custom exception, this custom exception will not be hidden, then the login failed callback information in accordance with custom exception to a front-end user prompt.
(3) When the user name search fails, it will be thrown directly BadCredentialsException, but the exception message is "the user name does not exist".

The three ideas are for your reference only, unless the situation is special, you generally don't need to modify the default behavior of this section.

What are the advantages of the official doing this? Obviously, the developer can be forced to give a vague exception prompt, so that even an inexperienced novice will not put the system in danger.

Okay, after the configuration is complete, the backend will only return JSON to the frontend regardless of whether the login succeeds or fails.

3. Uncertified treatment plan

What if it is not certified?

A small partner said that it is not easy. You can access data without authentication and just redirect to the login page. That's right, the default behavior of the system is also the same.

But in the separation of front and back ends, this logic is obviously problematic. If the user accesses a page that needs to be authenticated without logging in, at this time, we should not redirect the user to the login page, but give the user a The login prompt, after the front end receives the prompt, it decides the page jump by itself.

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, we can see from the comments of this method, this method is used to determine in the end is still to be redirected to forward, through the Debug tracing, we found that default useForwardvalue is false, so the request into the redirection.

So our idea of ​​solving the problem is very simple. Just rewrite this method and return JSON in the method instead of redirecting. 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 needs to be authenticated before they can access, there will be no redirection. The server will directly give the browser a JSON prompt. After the browser receives the JSON, what should it do? .

Four, log out

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

Logout We said earlier that according to the previous configuration, after logging out, the system will automatically jump to the login page. This is also inappropriate. If it is a front-end and back-end separation project, logout and login are successful and return JSON. The configuration is as follows:

.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 received is also JSON:

Insert picture description here

Five, the code

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.inMemoryAuthentication()
                .withUser("yolo")
                .password("123").roles("admin");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
    
    
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("name")
                .passwordParameter("passwd")
                .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();
                })
                .failureHandler((req, resp, e) -> {
    
    
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(e.getMessage());
                    out.flush();
                    out.close();
                })
                .permitAll()
                .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()
                .csrf().disable().exceptionHandling()
                .authenticationEntryPoint((req, resp, authException) -> {
    
    
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            out.write("尚未登录,请先登录");
                            out.flush();
                            out.close();
                        }
                );
    }
}

Guess you like

Origin blog.csdn.net/nanhuaibeian/article/details/108585599