Introduction principle and practice of spring security

In web application development, security is undoubtedly very important. Choosing spring security to protect web applications is a very good choice. Spring security is a security module in the spring project, which can be seamlessly integrated with the spring project. In particular, it is very simple to add spring security to the spring boot project. In this article, we introduce spring security and its use in web applications.

Start with an example of spring security

Create an unprotected app

Suppose we have created a springboot web application. If not, download the code here https://github.com/xudeming/spring-security-demo (springboot2. X), there is a controller as follows:

@Controller
public class AppController {

    @RequestMapping("/hello")
    @ResponseBody
    String home() {
        return "Hello,spring security!";
    }
}    

We start the application, assuming that the port is 8080, then when we access it in the browser http://localhost:8080/hello You can see Hello, spring security! In the browser!.

Add spring security protection application

At this point, / Hello is freely accessible. Assuming that we need users with a certain role to access, we can introduce spring security for protection. Add the following dependencies and restart the application:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Visit / hello again to get an HTTP basic authentication pop-up window, as follows:

It indicates that spring security has worked. If we click Cancel, we will see an error message as follows:

There was an unexpected error (type=Unauthorized,status=401).

Close security Basic, log in using the form page

It is impossible for us to use the pop-up window in HTTP basic mode above in the actual project to let users complete login, but there will be a login page. Therefore, we need to close the HTTP basic mode. The configuration of the authentication pop-up window closing the HTTP basic mode is as follows:

security.basic.enabled=false

Spring security provides the function of form login by default. We create a new securityconfiguration class and add some code, as shown below:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
				.authorizeRequests()
				.anyRequest().authenticated()
				.and()
				.formLogin().and()
				.httpBasic();
	}
}

The above code is actually a configuration. Authorizerequests () defines which URLs need to be protected and which do not. Formlogin () defines the login page to go to when a user needs to log in. At this time, we do not write the login page, but spring security provides a login page and login controller by default.

After adding the above configuration classes, we restart the application. Then continue the visit http://localhost:8080/hello 。 You will find that you will automatically jump to a login page, as shown below:

This page is the default login page provided by spring security. Its HTML content is as follows:

<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
<table>
	<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
	<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
	<tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
	<input name="_csrf" type="hidden" value="635780a5-6853-4fcd-ba14-77db85dbd8bd" />
</table>
</form></body></html>

We can see that there is a form here. Action = "/ login". This / login is still provided by spring security. The form submits three data:

In order to log in to the system, we need to know the user name and password. The default user name of spring security is user. The default password will be generated when spring security is started (you can see it in the startup log). In this example, we specify a user name and password and add the following contents to the configuration file:

# security
security.basic.enabled=false
security.user.name=admin
security.user.password=admin

Restart the project and access the protected / Hello page. Automatically jump to the default login page of spring security. We enter the user name admin and password admin. Click the login button. You will find that the login is successful and jump to / Hello. In addition to login, spring security also provides the rememberme function, which will not be explained here.

Role resource access control

Usually, we need to implement the function of "specific resources can only be accessed by specific roles". Suppose our system has the following two roles:

Now we add "/ product" to the system to represent the resources of commodity information (user can access); add "/ Admin" code to the system to represent the resources of administrator (user cannot access). The code is as follows:

@Controller
@RequestMapping("/product")
public class ProductTestController {

	@RequestMapping("/info")
	@ResponseBody
	public String productInfo(){
		return " some product info ";
	}
}
-------------------------------------------
@Controller
@RequestMapping("/admin")
public class AdminTestController {

	@RequestMapping("/home")
	@ResponseBody
	public String productInfo(){
		return " admin home page ";
	}
}

In the formal application, our users and roles are saved in the database; In this example, to facilitate the demonstration, let's create two users and roles stored in memory. We added role users to the securityconfiguration created in the previous step, as shown in the following code:

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
				.inMemoryAuthentication()
				.withUser("admin1") // 管理员,同事具有 ADMIN,USER权限,可以访问所有资源
					.password("admin1")
					.roles("ADMIN","USER")
					.and()
				.withUser("user1").password("user1") // 普通用户,只能访问 /product/**
					.roles("USER");
	}

Here, we have added administrators (admin1, password admin1) and ordinary users (user1, password user1)

In springboot 2 In the version of X, the above code will adjust the car as follows:

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
				.inMemoryAuthentication()
				.withUser("admin1") // 管理员,同事具有 ADMIN,USER权限,可以访问所有资源
					.password("{noop}admin1")  // 
					.roles("ADMIN","USER")
					.and()
				.withUser("user1").password("{noop}user1") // 普通用户,只能访问 /product/**
					.roles("USER");
	}

The place of change is: Password ("{noop}admin1") here.

Continue to add the "link role" control configuration, and the code is as follows:

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
				.authorizeRequests()
				    .antMatchers("/product/**").hasRole("USER")
				    .antMatchers("/admin/**").hasRole("ADMIN")
				.anyRequest().authenticated()
				.and()
				.formLogin().and()
				.httpBasic();
	}

This configuration adds the role configuration corresponding to the link to the login configuration in the previous step. According to the above configuration, we can know:

Let's verify that ordinary users log in, restart the project, and enter in the browser: http://localhost:8080/admin/home 。 Similarly, we will arrive at the login page. We enter the user name user1 and the password is user1. The result is an error page and access is denied. The information is:

There was an unexpected error (type=Forbidden,status=403).
Access is denied

We changed the URI in the browser to: / product / info, and the access was successful. You can see some product info. It indicates that user1 can only access product / * *, which is consistent with our expectation.

Again, verify that the administrator user logs in, restart the browser, and enter http://localhost:8080/admin/home 。 Enter the user name admin1 and password admin1 in the login page. After submitting, you can see the admin home page, indicating that you have accessed the administrator resources. We modify the browser URI to / product / info. After refreshing, we can also see some product info, indicating that admin1 users can access all resources, which is also consistent with our expectations.

Get current login user information

Above, we have implemented the access control of "resource role", and the effect is consistent with our expectation, but it is not intuitive. We might as well try to obtain the information of "currently logged in user" in the controller and output it directly to see the effect. Take / product / info as an example, we modify its code as follows:

	@RequestMapping("/info")
	@ResponseBody
	public String productInfo(){
		String currentUser = "";
		Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		if(principl instanceof UserDetails) {
			currentUser = ((UserDetails)principl).getUsername();
		}else {
			currentUser = principl.toString();
		}
		return " some product info,currentUser is: "+currentUser;
	}

Here, we obtain the user information through the securitycontextholder and splice it into a string output. Restart the project and access it in the browser http://localhost:8080/product/info. Log in as admin1, and you can see that the browser displays some product info, currentuser is: admin1

Summary

So far, we have a basic understanding of spring security. Learned how to add spring security to the project and how to control the role access control of resources. Spring security is more than that. We have just started. In order to better use spring security in practice, we need to have a deeper understanding. Let's first understand some core concepts of spring security.

Spring security core components

The core components of spring security include securitycontext, securitycontextholder, authentication, userdetails and AuthenticationManager, which are described below.

SecurityContext

Security context. After the user passes the verification of spring security, the authentication information is stored in securitycontext. The interface of securitycontext is defined as follows:

public interface SecurityContext extends Serializable {
	/**
	 * Obtains the currently authenticated principal,or an authentication request token.
	 *
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal,or removes the authentication
	 * information.
	 *
	 * @param authentication the new <code>Authentication</code> token,or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);
}

You can see that the securitycontext interface defines only two methods. In fact, its main function is to obtain the authentication object.

SecurityContextHolder

The securitycontextholder is a holder that holds the securitycontext instance. In a typical web application, a user logs in once and is identified by his session ID. The server caches the principal information for the duration session. In spring security, the responsibility for storing the securitycontext between requests falls on the securitycontextpersistencefilter. By default, the context stores the context as the httpsession attribute between HTTP requests. It restores the securitycontextholder for each request and, most importantly, clears the securitycontextholder when the request completes. Securitycontextholder is a class whose functional methods are static.

Securitycontextholder can set the specified JVM policy (storage policy of securitycontext). There are three kinds of policies:

Securitycontextholder uses mode by default_ ThreadLocal mode, which is stored in the current thread. In the spring security application, we can usually see code similar to the following:

 SecurityContextHolder.getContext().setAuthentication(token);

Its function is to store the current authentication information.

Authentication

Authentication literally means "authentication". In spring security, authentication is used to indicate who the current user is. Generally speaking, you can understand that authentication is a group of user name and password information. Authentication is also an interface, which is defined as follows:

public interface Authentication extends Principal,Serializable {
 
	Collection<? extends GrantedAuthority> getAuthorities();
	Object getCredentials();
	Object getDetails();
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

The interface has four get methods to get

Therefore, it can be inferred that its implementation class has these four properties. The functions of these methods are as follows:

UserDetails

Userdetails means user information. It stores user information, which is defined as follows:

public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();
	String getpassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

The meaning of the method is as follows:

UserDetailsService

When you mention userdetails, you must mention userdetailsservice. Userdetailsservice is also an interface, and there is only one method loaduserbyusername, which can be used to obtain userdetails.

Generally, in spring security applications, we will customize a customuserdetailsservice to implement the userdetailsservice interface and implement its public userdetails loaduserbyusername (final string login); method. When we implement the loaduserbyusername method, we can query the database (or cache or other storage forms) to obtain user information, and then assemble it into a userdetails (usually an org.springframework.security.core.userdetails.user, which inherits from userdetails) and return it.

When implementing the loaduserbyusername method, if we fail to find relevant records through database search, we need to throw an exception to tell spring security to "deal with the aftermath". The exception is org springframework. security. core. userdetails. UsernameNotFoundException。

AuthenticationManager

AuthenticationManager is an interface with only one method. The receiving parameter is authentication, which is defined as follows:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

The function of AuthenticationManager is to verify authentication. If authentication fails, an authenticationexception will be thrown. Authenticationexception is an abstract class, so code logic cannot instantiate an authenticationexception and throw it. In fact, the exceptions thrown are usually its implementation classes, such as disabledexception, lockedexception, badcredentialsexception, etc. Badcredentialsexception may be common, that is, when the password is wrong.

Summary

Here, we just have a brief understanding of what is in spring security. Let's get familiar with it first. We don't need to remember all these nouns and concepts at once. Let's take a look first. I have an impression.

How spring security works

In the first section, by adding spring boot starter security dependency to POM file, our project is protected by spring security. By adding securityconfiguration, we realize some security configurations and realize personalized access control of linked resources. So how does this happen? Understanding its principle can make it easy for us to use.

Spring security is based on filter in web applications

In the official document of spring security, we can see the following sentence:

We can know that spring security is based on filter in web applications. Filter we are very familiar with it. Before struts or spring MVC, we can achieve business functions by servlet, filter, and we usually have multiple filter. They execute sequentially, and after one execution, they call the next doFilter in filterChain. Spring security creates an authentication object in the filter and calls the AuthenticationManager for verification

Spring security maintains a filter chain. Each filter in the chain has specific responsibilities and is added in the configuration according to the required services. The order of filters is important because there are dependencies between them. Spring security has the following filters (in order):

Here we list almost all spring security filters. It is these filters that complete various functions of spring security. At present, we only know these filters, but we don't know how they are integrated into the application. Before we go any further, we need to know about delegatingfilterproxy.

DelegatingFilterProxy

Delegatingfilterproxy is a special filter that exists in the spring web module. Delegatingfilterproxy makes itself a filter by inheriting genericfilterbean (because genericfilterbean implements filter). It is a filter, but its name ends with proxy. It is very interesting. In order to understand its functions, let's take a look at its usage configuration:

<filter>
    <filter-name>myFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

This configuration is that we use web XML is used to configure filter. However, unlike ordinary filters, delegatingfilterproxy does not have actual filtering logic. It will try to find the myfilter configured by the filter name node and delegate the filtering behavior to myfilter. This approach can take advantage of spring's rich dependency injection tools and lifecycle interfaces, so delegatingfilterproxy provides Web The link between XML and the application context. It's very interesting. You can experience it slowly.

Spring security entry - springsecurityfilterchain

The entry filter of spring security is springsecurityfilterchain. If we want to use spring security before spring boot, it is usually on the web Add the following configuration to XML:

   <filter>
       <filter-name>springSecurityFilterChain</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   </filter>

   <filter-mapping>
       <filter-name>springSecurityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

See, delegatingfilterproxy is configured here. After the above introduction, we know that it will actually find the filter in the filter name node - springsecurityfilterchain, and hand over the actual filtering work to springsecurityfilterchain.

After using spring boot, this XML configuration is replaced by Java class configuration. We used the @ enablewebsecurity annotation in the code category earlier. By tracing the source code, we can find that @ enablewebsecurity will load the websecurity configuration class, and the websecurity configuration class contains the code to create the filter springsecurityfilterchain:

 @Bean(name = {"springSecurityFilterChain"})
    public Filter springSecurityFilterChain() throws Exception {
        boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
        if (!hasConfigurers) {
            WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
            });
            this.webSecurity.apply(adapter);
        }

        return (Filter)this.webSecurity.build();
    }

Here, we introduce the springsecurityfilterchain, the entry of spring security, and its two configurations. However, we still don't know who springsecurityfilterchain is and how it works. Let's continue.

Filterchainproxy and securityfilterchain

In the official documents of spring, we can find the following sentence:

This sentence seems to reveal a signal. The entry springsecurityfilterchain mentioned above is actually filterchainproxy. If you don't believe it, you can debug the code and find that it is indeed filterchainproxy. Its full path name is org springframework. security. web. FilterChainProxy。 Open its source code, and the first line of comment is as follows:

So, yes. It is the person delegatingfilterproxy is looking for, and it is the person delegatingfilterproxy is looking for to delegate filtering tasks. Some codes are posted below:

public class FilterChainProxy extends GenericFilterBean {
   
   private List<SecurityFilterChain> filterChains;// 

   public FilterChainProxy(SecurityFilterChain chain) {
      this(Arrays.asList(chain));
   }

   public FilterChainProxy(List<SecurityFilterChain> filterChains) {
      this.filterChains = filterChains;
   }

   public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletException {
         doFilterInternal(request,response,chain);
   }

   private void doFilterInternal(ServletRequest request,ServletException {

      FirewalledRequest fwRequest = firewall
            .getFirewalledRequest((HttpServletRequest) request);
      HttpServletResponse fwResponse = firewall
            .getFirewalledResponse((HttpServletResponse) response);
		
      List<Filter> filters = getFilters(fwRequest);

      if (filters == null || filters.size() == 0) {
         fwRequest.reset();
         chain.doFilter(fwRequest,fwResponse);
         return;
      }

      VirtualFilterChain vfc = new VirtualFilterChain(fwRequest,chain,filters);
      vfc.doFilter(fwRequest,fwResponse);
   }

   private List<Filter> getFilters(HttpServletRequest request) {
      for (SecurityFilterChain chain : filterChains) {
         if (chain.matches(request)) {
            return chain.getFilters();
         }
      }
      return null;
   }

}

As you can see, there is a collection of securityfilterchain. This is where many security filters hide. When dofilter, the first matching filter set will be taken from the securityfilterchain and returned.

Summary

At this point, it may be a little vague. Here is a summary and comb.

At this point, the idea is much clearer. Now I don't know how the securityfilterchain came from. This is described below.

Besides, securityfilterchain

Earlier, we introduced springsecurityfilterchain, which is configured by XML or initialized by @ enablewebsecurity annotation (@ import ({websecurityconfiguration. Class)). Specifically, it is in the websecurityconfiguration class. We posted the code above. You can go back and see the deleted version here again:

   @Bean( name = {"springSecurityFilterChain"})
    public Filter springSecurityFilterChain() throws Exception {
        // 删除部分代码
        return (Filter)this.webSecurity.build();
    }

In the last line, we find websecurity Build() generated filterchainproxy. Therefore, it is inferred that securityfilterchain is made in websecurity. Post source code:

public final class WebSecurity extends
      AbstractConfiguredSecurityBuilder<Filter,WebSecurity> implements
      SecurityBuilder<Filter>,ApplicationContextAware {
    
    @Override
	protected Filter performBuild() throws Exception {
		int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
        // 我们要找的 securityFilterChains
		List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>(
				chainSize);
		for (RequestMatcher ignoredRequest : ignoredRequests) {
			securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
		}
		for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
			securityFilterChains.add(securityFilterChainBuilder.build());
		}
        // 创建 FilterChainProxy  ,传入securityFilterChains
		FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
		if (httpFirewall != null) {
			filterChainProxy.setFirewall(httpFirewall);
		}
		filterChainProxy.afterPropertiesSet();

		Filter result = filterChainProxy;
		postBuildAction.run();
		return result;
	}
}

So far, we know how spring security works in spring web applications. The specific details are to execute the code in the filter, which will not be further explored here. Our purpose is to find out how he works and what the general pulse is. At present, the content has achieved this purpose.

Some actual combat of spring security

Here are some examples of using spring security in practice. Still rely on the example at the beginning and adjust it on this basis.

Through database query, users and roles are stored to realize security authentication

In the opening example, we used the memory user role to demonstrate login authentication. But the actual project must be completed through the database. In the actual project, we may have three tables: user table, role table and user role association table. Of course, different systems have different designs, not necessarily such three tables. The significance of this example is that if we want to add spring security to an existing project, we need to adjust the login. It is mainly about customizing userdetailsservice. In addition, it may also need to deal with the problem of password, because spring does not know how to encrypt the user login password. At this time, we may need to customize passwordencoder, which will also be mentioned below.

Add spring data JPA, create a data table, and add data

Continue to improve the opening project. Now add spring data JPA to the project and use the MySQL database. Therefore, the following configuration is added to the POM file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>MysqL</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

In application Add database connection information to the properties file:

spring.datasource.url=jdbc:MysqL://localhost:3306/yourDB?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=dbuser
spring.datasource.password=******
spring.datasource.driver-class-name=com.MysqL.jdbc.Driver

Here, for simplicity and convenience, we only create a table with the following fields:

@Entity
public class User implements java.io.Serializable{

	@Id
	@Column
	private Long id;
	@Column
	private String login;
	@Column
	private String password;
	@Column
	private String role;
    // 省略get set 等
}

Then we add 2 pieces of data as follows:

Bcryptpasswordencoder is used for passwords here. You need to add configuration in securityconfiguration, which will be posted later.

Customize userdetailsservice

As mentioned earlier, userdetailsservice and spring security need to find users in the authentication process. They will call the loaduserbyusername method of userdetailsservice to get a userdetails. Let's implement it. The code is as follows:

@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
         // 1. 查询用户
		User userFromDatabase = userRepository.findOneByLogin(login);
		if (userFromDatabase == null) {
			//log.warn("User: {} not found",login);
		 throw new UsernameNotFoundException("User " + login + " was not found in db");
            //这里找不到必须抛异常
		}
	    // 2. 设置角色
		Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
		GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole());
		grantedAuthorities.add(grantedAuthority);
         
		return new org.springframework.security.core.userdetails.User(login,userFromDatabase.getpassword(),grantedAuthorities);
	}
}

This method does two things: querying users and setting roles. Usually, a user will have multiple roles, that is, userfromdatabase Getrole () is usually a list, so when setting the role, it is a for loop to new multiple simplegrantedauthorities and set. (for simplicity, this example does not set the role table and user role association table. Only one role field is added to the user, so there is only one grandedauthorities.)

At the same time, modify the previous securityconfiguration and add the customuserdetailsservice bean configuration, as follows:

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService)// 设置自定义的userDetailsService
				.passwordEncoder(passwordEncoder());
		/*auth
			.inMemoryAuthentication()
			.withUser("admin1")
				.password("admin1")
				.roles("ADMIN","USER")
				.and()
			.withUser("user1").password("user1")
				.roles("USER");*/
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

Verification effect

We customized the userdetailsservice above. At this time, spring security will be called in its action process. If nothing unexpected happens, restart the system. We can log in with user1 to see / product / info, but not / admin / home. Let's restart the project and verify it.

First enter user1 and the wrong password. The results are as follows:

Then enter user1 and the correct password. The results are as follows:

Then change the browser link to / admin / home, and the result shows:

There was an unexpected error (type=Forbidden,status=403).
Access is denied

This is completely consistent with our expectations. So far, we have added spring security to the project, and can complete the authentication and authorization by querying the database user and giving the role information to spring security.

Spring security session stateless

Remember the example we gave at the beginning? After logging in with the administrator account and password, we can access / admin / home. At this time, change the browser address bar to / product / info and refresh the page. It still can be accessed, indicating that the authentication status has been maintained; If we close the browser and re-enter / admin / home, we will be prompted to log in again, which feels like a session. If we disable the browser cookie at this time, you will find that the automatic jump after login will only get 403403, which means access is denied, which means no permission, indicating that the authorization status is linked to the session in this case. That is, spring security uses session. But not all systems need sessions. Can we make spring security not applicable to sessions? The answer is yes!

Using spring security, we can accurately control when a session is created and how spring security interacts with it:

Here, we want to focus on stateless, commonly known as stateless. Why pay attention to this stateless situation? At present, our applications are basically front-end and back-end separated applications. For example, your set of Java APIs is called for react front end, Android end and IOS end. What session do you mention at this time? What we need at this time is stateless. We usually interact in a token way.

The spring security configures stateless in the following way: modify the securityconfiguration we defined earlier:

http
    .sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

Integrating spring security with custom token in front and back end separation applications

We mentioned stateless above. In fact, our front and back-end separation projects are stateless and do not maintain the login state. The server identifies the caller through the token passed by the client call.

Generally, our system process is as follows:

If we want to use custom tokens in the spring security project, we need to consider the following questions:

The following starts with the login token. The usernamepasswordauthenticationtoken and securitycontextholder need to be used here. The code is as follows:

    @RequestMapping(value = "/authenticate",method = RequestMethod.POST)
    public Token authorize(@RequestParam String username,@RequestParam String password) {
        // 1 创建UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken token 
                           = new UsernamePasswordAuthenticationToken(username,password);
        // 2 认证
        Authentication authentication = this.authenticationManager.authenticate(token);
        // 3 保存认证信息
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 4 加载UserDetails
        UserDetails details = this.userDetailsService.loadUserByUsername(username);
        // 5 生成自定义token
        return tokenProvider.createToken(details);
    }
    @Inject
    private AuthenticationManager authenticationManager;

Steps 1, 2, 3 and 4 in the above code interact with spring security. Only step 5 is defined by ourselves. Here, the tokenprovider is the generation method of tokens in our system (this is completely personalized, usually an encryption string, which may contain user information, expiration time, etc.). The tokens are also our custom return objects, which contain token information, similar to {"token": "ABCD", "expires": 1234567890}

Our tokenprovider usually has at least two methods: generate a token and verify a token. Roughly as follows:

public class TokenProvider {

    private final String secretKey;
    private final int tokenValidity;

    public TokenProvider(String secretKey,int tokenValidity) {
        this.secretKey = secretKey;
        this.tokenValidity = tokenValidity;
    }
   // 生成token
    public Token createToken(UserDetails userDetails) {
        long expires = System.currentTimeMillis() + 1000L * tokenValidity;
        String token =  computeSignature(userDetails,expires);
        return new Token(token,expires);
    }
    // 验证token
   public boolean validateToken(String authToken,UserDetails userDetails) {
        check token
        return true or false;
    }
     // 从token中识别用户
    public String getUserNameFromToken(String authToken) {
        // ……
        return login;
    }
    public String computeSignature(UserDetails userDetails,long expires) {
        // 一些特有的信息组装,并结合某种加密活摘要算法
        return 例如 something+"|"+something2+MD5(s);
    }

}

At this point, our client can call http://host/context/authenticate To get a token, like this: {"token": "ABCD", "expires": 1234567890}. Then, the next time we make a request, we will take the parameter token = ABCD (or in the custom request header). How can we restore the "session" in spring security? We need a filter:

public class MyTokenFilter extends GenericFilterBean {

    private final Logger log = LoggerFactory.getLogger(XAuthTokenFilter.class);

    private final static String XAUTH_TOKEN_HEADER_NAME = "my-auth-token";

    private UserDetailsService detailsService;

    private TokenProvider tokenProvider;
    public XAuthTokenFilter(UserDetailsService detailsService,TokenProvider tokenProvider) {
        this.detailsService = detailsService;
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse,FilterChain filterChain) throws IOException,ServletException {
        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String authToken = httpServletRequest.getHeader(XAUTH_TOKEN_HEADER_NAME);
            if (StringUtils.hasText(authToken)) {
               // 从自定义tokenProvider中解析用户
                String username = this.tokenProvider.getUserNameFromToken(authToken);
                // 这里仍然是调用我们自定义的UserDetailsService,查库,检查用户名是否存在,
                // 如果是伪造的token,可能DB中就找不到username这个人了,抛出异常,认证失败
                UserDetails details = this.detailsService.loadUserByUsername(username);
                if (this.tokenProvider.validateToken(authToken,details)) {
                    log.debug(" validateToken ok...");
                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details,details.getpassword(),details.getAuthorities());
                    // 这里还是上面见过的,存放认证信息,如果没有走这一步,下面的doFilter就会提示登录了
                    SecurityContextHolder.getContext().setAuthentication(token);
                }
            }
            // 调用后续的Filter,如果上面的代码逻辑未能复原“session”,SecurityContext中没有想过信息,后面的流程会检测出"需要登录"
            filterChain.doFilter(servletRequest,servletResponse);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

So far, we have implemented a custom token generation class, and intercepted the client request through a filter, parsed the token, restored the stateless "session", so that the current request processing thread has authentication and authorization data, and the subsequent business logic can be executed. Next, we need to integrate the customized content into spring security.

First, write a class that inherits securityconfigureradapter:

public class MyAuthTokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {

    private TokenProvider tokenProvider;  // 我们之前自定义的 token功能类
    private UserDetailsService detailsService;// 也是我实现的UserDetailsService
    
    public MyAuthTokenConfigurer(UserDetailsService detailsService,TokenProvider tokenProvider) {
        this.detailsService = detailsService;
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MyAuthTokenFilter customFilter = new MyAuthTokenFilter(detailsService,tokenProvider);
        http.addFilterBefore(customFilter,UsernamePasswordAuthenticationFilter.class);
    }
}

Add the following content to the securityconfiguration configuration class:

    // 增加方法
    private MyAuthTokenConfigurer securityConfigurerAdapter() {
      return new MyAuthTokenConfigurer(userDetailsService,tokenProvider);
    }
    // 依赖注入
    @Inject
    private UserDetailsService userDetailsService;

    @Inject
    private TokenProvider tokenProvider;

     //方法修改 , 增加securityConfigurerAdapter
     @Override
	protected void configure(HttpSecurity http) throws Exception {
		http
				.authorizeRequests()
				.anyRequest().authenticated()
                  // .... 其他配置
				.and()
                 .apply(securityConfigurerAdapter());// 这里增加securityConfigurerAdapter
				
	}

So far, we have completed the combination of token authentication and spring security in stateless applications.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>