Spring Boot + Vue front-end split project, how to kick the logged-in user?

Last article In Spring Security, we talked about how to kick off the previous login user or prohibit the user from logging on again. We achieved the desired effect through a simple case.

But one less perfect thing is that our users are configured in memory and we don't put them in the database.Normally, other configurations that Songo talks about in the Spring Security series are just references Spring Security+Spring Data Jpa join forces, so security management is only simpler! In this article, switch the data into data in the database.

However, when doing concurrent session processing for Spring Security, it is problematic to switch the in-memory user directly to the user in the database. Let's talk about this today and apply this function to micro-personnel by the way. https://github.com/lenve/vhr).

This is the 14th article in Songo's recently serialized Spring Security series. Reading the previous articles in this series will help to better understand this article:

  1. Dig a big hole and start Spring Security!
  2. Pingo takes you to Spring Security by hand. Don't ask me how to decrypt the password anymore
  3. Hand-on instructions for customizing form logins in Spring Security
  4. Spring Security does front-end separation, let's not do page jumps!Unified JSON Interaction
  5. Authorization in Spring Security was so simple
  6. How does Spring Security store user data in the database?
  7. Spring Security+Spring Data Jpa join forces, so security management is only simpler!
  8. Spring Boot + Spring Security for automatic login
  9. Spring Boot automatic login, how to control security risk?
  10. Where is Spring Security better than Shiro in a microservice project?
  11. Two ways SpringSecurity customizes authentication logic (advanced gameplay)
  12. How can I quickly view information like login user IP address in Spring Security?
  13. Spring Security automatically kicks off the previous logged-in user, a configuration is complete!

This article's case will be based on Spring Security+Spring Data Jpa join forces, so security management is only simpler! It's built in one article, so I don't have to write duplicate code. If you're unfamiliar with it, your buddies can refer to it.

1. Environmental preparation

First, let's open the Spring Security+Spring Data Jpa join forces, so security management is only simpler! The case in the article, which combines Spring Data Jpa to store user data in a database.

Then we copy the login page mentioned in the previous article into the project (you can download the complete case at the end):

[External chain picture transfer failed, source station may have anti-theft chain mechanism, it is recommended to save the picture and upload it directly (img-7XB0viq6-1588898082940).) http://img.itboyhub.com/2020/04/20200506204420.png)]

And configure the login page slightly in SecurityConfig:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            ...
            .and()
            .formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/doLogin")
            ...
            .and()
            .sessionManagement()
            .maximumSessions(1);
}

This is a general configuration, so I won't talk more about it.Notice at the end that we set the number of session s to 1.

Okay, when the configuration is complete, we start the project, parallel multiend login test.

Open multiple browsers and do multiple login tests separately. We are surprised to find that each browser can login successfully and won't kick off users who have already logged in each time!

What's wrong?

2. Problem Analysis

To understand this, we need to understand how Spring Security saves user objects and session s.

The SessionRegistryImpl class is used in Spring Security to unify session information management. Let's look at the source code for this class (in part):

public class SessionRegistryImpl implements SessionRegistry,
		ApplicationListener<sessiondestroyedevent> {
	/** <principal:object,sessionidset> */
	private final ConcurrentMap<object, set<string>&gt; principals;
	/** <sessionid:object,sessioninformation> */
	private final Map<string, sessioninformation> sessionIds;
	public void registerNewSession(String sessionId, Object principal) {
		if (getSessionInformation(sessionId) != null) {
			removeSessionInformation(sessionId);
		}
		sessionIds.put(sessionId,
				new SessionInformation(principal, sessionId, new Date()));

		principals.compute(principal, (key, sessionsUsedByPrincipal) -&gt; {
			if (sessionsUsedByPrincipal == null) {
				sessionsUsedByPrincipal = new CopyOnWriteArraySet&lt;&gt;();
			}
			sessionsUsedByPrincipal.add(sessionId);
			return sessionsUsedByPrincipal;
		});
	}
	public void removeSessionInformation(String sessionId) {
		SessionInformation info = getSessionInformation(sessionId);
		if (info == null) {
			return;
		}
		sessionIds.remove(sessionId);
		principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -&gt; {
			sessionsUsedByPrincipal.remove(sessionId);
			if (sessionsUsedByPrincipal.isEmpty()) {
				sessionsUsedByPrincipal = null;
			}
			return sessionsUsedByPrincipal;
		});
	}

}

The source code for this class is still long. Here I extract some of the more critical parts:

  1. First of all, you can see that, first of all, a principals object is declared. This is a map collection that supports concurrent access. The key of the collection is the principal of the user. Normally, the principal of the user is actually the user object. Songgo also talked about how principals are stored in Authentication in previous articles (see: Songge takes you through the Spring Security login process ), and the value of the collection is a set collection in which the sessionid corresponding to the user is saved.
  2. If a new session needs to be added, add it in the registerNewSession method by calling the principals.compute method, where key is principal.
  3. If the user logs out of the login, the sessionid needs to be removed, and the operation is done in the removeSessionInformation method, specifically by calling the principals.computeIfPresent method, these basic operations on collections are no longer discussed.

Seeing this, you have found a problem. The key of the ConcurrentMap collection is a principal object. Make the key from the object. Make sure to override the equals method and hashCode method. Otherwise, if you save the data for the first time, you will not find it next time. This is JavaSE knowledge, I will not say more.

If we're using memory-based users, let's look at the definition in Spring Security:

public class User implements UserDetails, CredentialsContainer {
	private String password;
	private final String username;
	private final Set<grantedauthority> authorities;
	private final boolean accountNonExpired;
	private final boolean accountNonLocked;
	private final boolean credentialsNonExpired;
	private final boolean enabled;
	@Override
	public boolean equals(Object rhs) {
		if (rhs instanceof User) {
			return username.equals(((User) rhs).username);
		}
		return false;
	}
	@Override
	public int hashCode() {
		return username.hashCode();
	}
}

You can see that he actually overrides the equals and hashCode methods himself.

So we have no problem using memory-based users, and we have problems using custom users.

If you find the problem, it's easy to solve it. Rewrite the equals and hashCode methods of the User class:

@Entity(name = "t_user")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
    private List<role> roles;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(username, user.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(username);
    }
    ...
    ...
}

After the configuration is complete, restart the project, and then go to the multi-end login test, you will find that you can successfully kick off the logged-in users.

If you use MyBatis instead of Jpa, the same is true, simply rewrite the equals and hashCode methods of the logged-in user.

3. Micro Personnel Application

3.1 Problems

Since Micro Personnel is currently logged on in JSON format, if the project controls the concurrency of session s, there will be some additional issues to address.

The biggest problem is that we have replaced the UsernamePasswordAuthenticationFilter with a custom filter, which in turn invalidates the previous session configuration.All related configurations are to be configured in the new filter LoginFilter, including SessionAuthentication Strategy, which also needs to be manually configured by ourselves.

This has resulted in some workload, but when you're done, you can be sure that your understanding of Spring Security will improve.

3.2 Specific applications

Let's see how this works. I'm going to highlight some key codes that you can download from GitHub: https://github.com/lenve/vhr.

First, we override the equals and hashCode methods of the Hr class as follows:

public class Hr implements UserDetails {
    ...
    ...
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Hr hr = (Hr) o;
        return Objects.equals(username, hr.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(username);
    }
    ...
    ...
}

Next, configure it in SecurityConfig.

Here we want to provide the SessionAuthentication Strategy ourselves, and the ConcurrentSessionControlAuthentication Strategy that concurrently handled the session before, that is, we need to provide an instance of ConcurrentSessionControlAuthentication Strategy ourselves, then configure it to LoginFilter, but create the ConcurrentSessionControlAuthentication StrategyA SessionRegistryImpl object is also required during the instance process.

As we have said before, the SessionRegistryImpl object is used to maintain session information, and now we have to provide it ourselves. The SessionRegistryImpl instance is well created, as follows:

@Bean
SessionRegistryImpl sessionRegistry() {
    return new SessionRegistryImpl();
}

Then configure SessionAuthenticationStrategy in LoginFilter as follows:

@Bean
LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -&gt; {
                //ellipsis
            }
    );
    loginFilter.setAuthenticationFailureHandler((request, response, exception) -&gt; {
                //ellipsis
            }
    );
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    loginFilter.setFilterProcessesUrl("/doLogin");
    ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
    sessionStrategy.setMaximumSessions(1);
    loginFilter.setSessionAuthenticationStrategy(sessionStrategy);
    return loginFilter;
}

Here we manually build the ConcurrentSessionControlAuthenticationStrategy instance ourselves, pass the SessionRegistryImpl parameter during the build, set the concurrency number of sessions to 1, and configure session Strategy to LoginFilter.

>Actually Last article Ultimately, our configuration is the same as above, but now we write it out by ourselves.

Is this configured?No,There is also a key filter for session processing called ConcurrentSessionFilter, which was originally unmanaged by us, but SessionRegistryImpl is also used in this filter, and SessionRegistryImpl is now defined by us, so we need to reconfigure the filter as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            ...
    http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -&gt; {
        HttpServletResponse resp = event.getResponse();
        resp.setContentType("application/json;charset=utf-8");
        resp.setStatus(401);
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(RespBean.error("You are already logged on to another device and this logon is offline!")));
        out.flush();
        out.close();
    }), ConcurrentSessionFilter.class);
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

Here, we re-create an instance of ConcurrentSessionFilter instead of the system default.When creating a new ConcurrentSessionFilter instance, two parameters are required:

  1. sessionRegistry is the SessionRegistryImpl instance we provided earlier.
  2. The second parameter is a callback function that handles when the session expires, that is, when the user is kicked off by another login, what kind of offline prompt you want to give is done here.

Finally, we need to manually add a record to SessionRegistryImpl after processing the login data:

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Autowired
    SessionRegistry sessionRegistry;
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //ellipsis
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            Hr principal = new Hr();
            principal.setUsername(username);
            sessionRegistry.registerNewSession(request.getSession(true).getId(), principal);
            return this.getAuthenticationManager().authenticate(authRequest);
        } 
        ...
        ...
    }
}

Here, we manually call the sessionRegistry.registerNewSession method to add a session record to SessionRegistryImpl.

OK, and then our project is configured.

Next, restart the vhr project and do multiple login tests. If you are kicked offline, you will see the following tips:

Complete code, I have updated to vhr, you can download to learn.

If your little buddies are interested in videos of the vhr project recorded by Songo, check out here: Video tutorial for micro projects

4. Summary

Okay, this article focuses on a pit you might encounter when dealing with session concurrency in Spring Security and how to deal with session concurrency in the case of front-end and back-end separation.Don't know if they have GET?

The cases in the second section of this article can be downloaded from GitHub: https://github.com/lenve/spring-security-samples

If you feel there is something to gain, remember to click one after another to encourage Panasonic Oh to </role></grandauthority></string, ></sessionid:object, sessioninformation></object, ></principal:object, sessionidset></sessiondestroyedevent>

Tags: Programming Spring Session Database github

Posted on Thu, 07 May 2020 21:05:56 -0400 by olanjouw