Implementing JWT-Based Authentication and Authorization with Spring Security in a Frontend‑Backend Separated Architecture

This article explains how to use Spring Security together with JWT to build a stateless authentication system for front‑end/back‑end separated applications, covering token issuance, refresh logic, custom filters, handlers, UserDetailsService implementation, global security configuration, and testing procedures.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing JWT-Based Authentication and Authorization with Spring Security in a Frontend‑Backend Separated Architecture

The author introduces authentication and authorization as essential parts of real‑world projects, selects Spring Security as the primary security component, and launches the "Spring Security Advanced" series to discuss the transition from monolithic to OAuth2‑based distributed architectures.

In a front‑end/back‑end separated scenario, sessions cannot be used, so JWT is adopted as a stateless token mechanism. The workflow is: the client calls the login API with username and password, receives an accessToken for resource access and a refreshToken for renewing the access token when it expires.

The demo project is built with Spring Boot and contains two modules: common-base for shared utilities and security-authentication-jwt for security‑related classes such as custom filters, handlers, and configuration. The author also designs five RBAC tables (user, role, permission, user‑role, role‑permission) to store authorization data.

Because the default UsernamePasswordAuthenticationFilter is unsuitable for stateless APIs, a custom JwtAuthenticationLoginFilter is created. The filter processes login requests, authenticates credentials, and on success returns both tokens. The core code is shown below:

public class JwtAuthenticationLoginFilter extends UsernamePasswordAuthenticationFilter {
    // implementation details ...
}

Custom AuthenticationSuccessHandler and AuthenticationFailureHandler are implemented to return JSON containing the tokens or error information respectively.

Additional handlers such as AuthenticationEntryPoint (for unauthenticated access) and AccessDeniedHandler (for insufficient permissions) are also provided, with example implementations included in the article.

The UserDetailsService loads user data from the database, performing password matching and assembling the user's roles and permissions. The key method signature is:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

Implementation details, including the LoginService that queries the database and the custom SecurityUser class that extends UserDetails, are illustrated with code snippets.

A TokenAuthenticationFilter extracts the accessToken from the request header, validates its signature and expiration, and stores the resulting Authentication in a thread‑local context for downstream use.

The refresh‑token endpoint validates the received refreshToken and issues a new pair of accessToken and refreshToken. The author notes that production systems may use different encryption algorithms for the two tokens.

Global security configuration disables form login and sessions, permits the login and refresh endpoints, applies the custom JWT filter chain, and registers the custom handlers. The essential configuration class is:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
    @Autowired
    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
    @Autowired
    private RequestAccessDeniedHandler requestAccessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().disable()
            .apply(jwtAuthenticationSecurityConfig)
            .and()
            .authorizeRequests()
            .antMatchers("/login", "/refreshToken").permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(entryPointUnauthorizedHandler)
            .accessDeniedHandler(requestAccessDeniedHandler)
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable();
    }

    @Bean
    public TokenAuthenticationFilter authenticationTokenFilterBean() {
        return new TokenAuthenticationFilter();
    }

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

Testing steps using Postman demonstrate successful login (returning two tokens), accessing a protected endpoint with and without a token, and refreshing an expired token.

In conclusion, although Spring Security is heavyweight, it provides a robust foundation for implementing OAuth2‑style authentication and authorization in modern Java applications.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaAuthenticationJWTAuthorizationspring-security
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.