Minimalist Spring Security 6 + Front-Back Separation Implementation: Focus on Understanding Workflow
Minimalist Spring Security 6 + Front-Back Separation Implementation: Focus on Understanding Workflow
Spring Security is relatively simple to use in Spring MVC, with built-in login, logout pages, session management, etc. However, how to implement a front-back separated project with Spring Security is not very familiar. Many resources are detailed, but the key points of combining Spring Security with front-back separation are not easy to understand. Therefore, this article removes all irrelevant content and only keeps a skeleton of integrating Spring Security with front-back separation. You can enrich other code based on your own business logic.
This article is based on JWT. After login, a JWT string is returned. If you are not familiar with JWT, please search for information. This article does not introduce the generation and parsing of JWT; it only uses the simplest string representation. The focus is on understanding the entire workflow, hoping to help everyone.
- Customize a Service that implements the UserDetailsService interface: Spring Security needs to look up user information by username. This interface has only one method,
loadUserByUsername. It can look up users via database or use an in-memory model. This article only creates a virtual user with fixed username and password.
@Component
public class SecurityService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("Executing loadUserByUsername");
UserDetails userDetails = new User("admin", "123", AuthorityUtils.NO_AUTHORITIES);
return userDetails;
}
}
- PasswordEncoder: After finding the user, Spring Security compares the submitted password. It must specify a PasswordEncoder for password encryption. You can use
DelegatingPasswordEncoder, etc. This article implements a custom encryption class that returns the password as-is without encryption; otherwise, you would need to use encrypted ciphertext when looking up the user in the first step. Simply inject the bean without any other configuration.
@Bean
public PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
};
}
- Configuration in the security configuration class:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityFilter securityFilter) throws Exception {
return http
.formLogin(form -> form.disable()) // Disable default login page
.logout(config -> config.disable()) // Disable default logout page
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Disable session, not needed for front-back separation
.httpBasic(httpBasic -> httpBasic.disable())
.authorizeHttpRequests(
auth -> auth.requestMatchers("/login", "/logout").permitAll()
.anyRequest().authenticated()
) // Set permissions: only login and logout do not require authentication, all others require authentication
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class) // Add JWT processing filter to parse user info from JWT
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint())) // Custom handler for unauthenticated users; otherwise, the browser returns 403 error. We need a JSON prompt for the frontend. The effect is shown in the images below.
.build();
}
Before customizing AuthenticationEntryPoint:
After customizing AuthenticationEntryPoint:
Custom AuthenticationEntryPoint for unauthenticated users:
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return (request, response, authException) -> {
response.setContentType("text/json;charset=utf-8");
response.getWriter().write("Login required");
};
}
- JWT validation filter: After login, a JWT string is returned. On subsequent requests, the client includes this string. This filter validates whether the JWT is correct. If valid, it saves the user's authentication state, preventing Spring Security from treating the request as unauthenticated. Code:
@Component
public class SecurityFilter extends GenericFilterBean {
@Resource
SecurityService security;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getParameter("jwt"); // Normally, JWT should be placed in the header. For simplicity, it is placed in request parameters here.
if ("jwtXXX".equals(jwt)){ // Normally, should verify JWT validity and expiration. For simplicity, a fixed string is used.
UserDetails userDetails = security.loadUserByUsername("admin");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// This step is crucial: put the authentication info into SecurityContext, indicating successful authentication.
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
System.out.println("JWT correct, authentication successful");
} else {
System.out.println("JWT incorrect, authentication failed");
}
chain.doFilter(request, response);
}
}
- Login and resource request controller:
@RestController
public class LoginController {
@Resource
AuthenticationConfiguration configuration;
@PostMapping("/login")
public String login(String username, String password){
AuthenticationManager authenticationManager = null;
try {
authenticationManager = configuration.getAuthenticationManager();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
Authentication authenticate = authenticationManager.authenticate(authRequest);
// This step is similar to the above: set authentication in SecurityContext.
SecurityContextHolder.getContext().setAuthentication(authenticate);
return "Login successful"; // Should return JSON containing JWT, etc. For simplicity, omitted.
} catch (Exception e) {
e.printStackTrace();
return "Login failed";
}
}
@GetMapping("/he")
public String he(){
return "Requested resource"; // Represents a protected resource
}
}
Demonstration (using Postman to simulate requests)
Result after successful login:
After successful login, subsequent requests include the JWT string, allowing access to protected resources:
If not logged in (i.e., request does not contain a JWT or the JWT is incorrect), an error message is returned:
Summary
Integrating Spring Security 6 with front-back separation is relatively simple. Many things in the above example are simplified, such as how to generate and validate JWT, the JSON strings returned by the controller, and reading user information from the database. These are considered specific business content. Once this integration skeleton is set up, you can modify other parts according to your needs.