00:00

JWT Explained: How JSON Web Token Authentication Works

Every secure web application must answer two questions on each request: authentication ("Who are you?") and authorization ("What are you allowed to do?").

A JSON Web Token (JWT) is one of the most popular ways to carry the answers to those questions from the client to the server. In this tutorial you will learn what a JWT is, how it is built, how it is checked, and how to create and validate JWTs in Java and Spring Boot 3, with simple examples.

1. Why Token-Based Authentication?

Traditional web applications use session-based authentication. After login, the server creates a session, stores it in memory or a database, and gives the browser a session ID cookie. On every request, the server looks up that session to find the user.

This works well for one server, but becomes harder as an application grows:

  • Every server must be able to find every session, so sessions must be shared or kept "sticky" to one server.
  • Millions of users mean millions of stored sessions.
  • Each request may need an extra lookup in a session store or database.
  • Mobile apps and other services do not handle browser cookies as naturally as websites do.

Token-based authentication takes a different approach. After login, the server gives the client a signed token that already contains the user's details. The client sends it with each request, and any server can check it on its own, with no shared session store. JWT is the most widely used format for such tokens.

2. What Is JWT?

A JWT (JSON Web Token) is a compact, URL-safe string that carries a small JSON object between two parties. It is defined by the open standard RFC 7519.

The JSON inside a JWT holds claims, which are statements such as "the user is riya", "she has the USER role", and "this token expires at 10:15". The token is digitally signed, so the server can detect if anyone changes even a single character.

JWTs are mainly used to:

  • Authenticate users after login, without server-side sessions.
  • Authorize access to APIs, based on the roles or scopes inside the token.
  • Exchange information safely between services that trust the same signing key.

3. Key Features of JWT

FeatureWhat It Means
CompactA short string that fits easily in an HTTP header or URL.
Self-containedCarries the user's ID, roles and expiry time inside the token itself.
Tamper-proofA digital signature shows whether the content was changed.
StatelessThe server does not have to store a session for each user.
JSON-basedEasy to create and read in almost every programming language.
Fast to verifyChecking a signature is usually quicker than a database lookup.
Works across servicesAny microservice with the right key can verify the same token.

4. Structure of a JWT

A JWT has three parts, separated by dots:

JWT format
Header.Payload.Signature

Each part is Base64URL encoded, which turns it into text that is safe to put in URLs and headers. A real token looks like this (split over three lines to make the parts easy to see):

Example JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJyaXlhIiwibmFtZSI6IlJpeWEgU2hhcm1hIiwicm9sZXMiOlsiVVNFUiJdLCJpc3MiOiJqYXZhYnl0ZWNoaWUtZGVtbyIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI2NTAwfQ.
ds-J2QIlVHzUy3LvBInnBZz7TwPoyNr8Fd8FvfIGecY
JWT structure diagram showing the header, payload and signature parts

4.1 Header

The header says what kind of token this is and which algorithm was used to sign it:

Header (decoded)
{
  "alg": "HS256",
  "typ": "JWT"
}

4.2 Payload

The payload holds the claims: information about the user and the token itself.

Payload (decoded)
{
  "sub": "riya",
  "name": "Riya Sharma",
  "roles": ["USER"],
  "iss": "javabytechie-demo",
  "iat": 1767225600,
  "exp": 1767226500
}

The times are in seconds since 1 January 1970 (UTC). Here, iat is 1 January 2026 at 00:00 UTC, and exp is 15 minutes later.

4.3 Signature

The signature proves that the token was created by a trusted server and has not been changed. For the HS256 algorithm it is calculated like this:

Signature formula
signature = HMAC-SHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    secretKey
)

If an attacker changes the payload, for example from "USER" to "ADMIN", the signature no longer matches and the server rejects the token. The attacker cannot create a new valid signature without the secret key.

5. Common JWT Claims

The JWT standard defines a set of registered claims with short, three-letter names. They are all optional, but you should use the ones that fit your needs:

ClaimNameMeaning
issIssuerWho created the token
subSubjectWho the token is about, usually the user ID
audAudienceWhich application or API the token is meant for
expExpiration TimeAfter this time the token must be rejected
nbfNot BeforeBefore this time the token must be rejected
iatIssued AtWhen the token was created
jtiJWT IDA unique ID for this token, useful for blocking or tracking it

You can also add your own custom claims, such as roles, name or tenantId. Keep them small and never put secrets in them.

6. How the Signature Works

JWTs can be signed in two main ways:

HS256 (Symmetric)RS256 / ES256 (Asymmetric)
KeysOne shared secret keyA private key and a public key
Who signsAnyone with the secretOnly the holder of the private key
Who verifiesAnyone with the same secretAnyone with the public key
Best forOne application that both creates and checks tokensMany services, or tokens from an external identity provider
RiskEvery verifier could also create tokensVerifiers cannot create tokens

A simple rule: use HS256 when a single application issues and checks its own tokens. Use RS256 or ES256 when many services must verify tokens, so that only one place can ever create them.

7. How JWT Authentication Works

Here is the complete journey of a JWT, from login to expiry:

  1. User login: the user enters a username and password, and the client sends them to the server over HTTPS.
  2. Token creation: the server checks the credentials. If they are correct, it creates a JWT with claims such as the user ID, roles and expiry time, signs it, and returns it.
  3. Token storage: the client keeps the token, for example in memory or in a secure cookie (see section 13).
  4. Token sent with requests: for each protected request, the client adds the token to the HTTP header: Authorization: Bearer <token>.
  5. Token verification: the server checks the signature, the expiry time and other claims such as the issuer. It does not need to look anything up in a database.
  6. Server response: if the token is valid and the user has permission, the server returns the data. If the token is invalid or expired, it returns 401 Unauthorized. If the user lacks permission, it returns 403 Forbidden.
  7. Token expiry: when the token expires, the client uses a refresh token to get a new one, or asks the user to log in again.

8. Encoded, Not Encrypted

This is the most important thing to remember about JWTs: Base64URL encoding is not encryption. Anyone who gets the token can read the header and payload. The signature only stops people from changing the content, not from reading it.

You can see this for yourself with a few lines of plain Java:

JwtDecoderDemo.java
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class JwtDecoderDemo {

    public static void main(String[] args) {
        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
                + "eyJzdWIiOiJyaXlhIiwibmFtZSI6IlJpeWEgU2hhcm1hIiwicm9sZXMiOlsiVVNFUiJdLCJpc3MiOiJqYXZhYnl0ZWNoaWUtZGVtbyIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI2NTAwfQ."
                + "ds-J2QIlVHzUy3LvBInnBZz7TwPoyNr8Fd8FvfIGecY";

        String[] parts = token.split("\\.");
        Base64.Decoder decoder = Base64.getUrlDecoder();

        String header = new String(decoder.decode(parts[0]), StandardCharsets.UTF_8);
        String payload = new String(decoder.decode(parts[1]), StandardCharsets.UTF_8);

        System.out.println("Header : " + header);
        System.out.println("Payload: " + payload);
    }
}
Output
Header : {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"riya","name":"Riya Sharma","roles":["USER"],"iss":"javabytechie-demo","iat":1767225600,"exp":1767226500}

So never put passwords, card numbers or other secrets in a JWT. If the content itself must be hidden, use an encrypted token (JWE) instead, or keep the sensitive data on the server.

9. Creating and Validating JWTs in Java

Writing signature code by hand is risky, so use a well-tested library. JJWT is a popular choice for plain Java. Add these three dependencies:

pom.xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>

The class below creates a token and then validates it. Validation checks the signature, the expiry time and the issuer in one call:

JwtExample.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;

import javax.crypto.SecretKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;

public class JwtExample {

    private static final String ISSUER = "javabytechie-demo";

    // Creates a random 256-bit key. In a real app, load the key from a secure place instead.
    private static final SecretKey KEY = Jwts.SIG.HS256.key().build();

    public static String createToken(String username, List<String> roles) {
        Instant now = Instant.now();

        return Jwts.builder()
                .issuer(ISSUER)
                .subject(username)
                .claim("roles", roles)
                .issuedAt(Date.from(now))
                .expiration(Date.from(now.plus(15, ChronoUnit.MINUTES)))
                .signWith(KEY)
                .compact();
    }

    public static Claims validateToken(String token) {
        return Jwts.parser()
                .verifyWith(KEY)
                .requireIssuer(ISSUER)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    public static void main(String[] args) {
        String token = createToken("riya", List.of("USER"));
        System.out.println("Token: " + token);

        try {
            Claims claims = validateToken(token);
            System.out.println("User : " + claims.getSubject());
            System.out.println("Roles: " + claims.get("roles"));
            System.out.println("Expires at: " + claims.getExpiration());
        } catch (ExpiredJwtException e) {
            System.out.println("Token has expired");
        } catch (JwtException e) {
            System.out.println("Invalid token: " + e.getMessage());
        }
    }
}

If the token was changed, signed with a different key, issued by someone else or has expired, validateToken throws an exception, and you must reject the request.

10. JWT Authentication in Spring Boot

Spring Security already has full JWT support built in, through the OAuth2 Resource Server module. You do not need to write your own filter. In this example, one Spring Boot application will:

  1. Check the username and password at /api/auth/login and return a signed JWT.
  2. Accept that JWT on every other endpoint and turn its roles claim into Spring Security roles.

10.1 Dependencies and Secret Key

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Create a strong random secret of at least 32 bytes and pass it in through an environment variable:

Terminal
export JWT_SECRET=$(openssl rand -base64 32)
application.properties
jwt.secret=${JWT_SECRET}
jwt.issuer=javabytechie-demo
jwt.expiry-minutes=15

10.2 Security Configuration

This class creates the JWT encoder and decoder, defines which URLs are open, and maps the roles claim to ROLE_ authorities:

SecurityConfig.java
package com.javabytechie.jwtdemo.config;

import com.nimbusds.jose.jwk.source.ImmutableSecret;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.issuer}")
    private String issuer;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // No cookies or sessions are used, so CSRF protection is not needed for this API
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/login").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));

        return http.build();
    }

    @Bean
    public SecretKey jwtSecretKey() {
        byte[] keyBytes = Base64.getDecoder().decode(secret);
        return new SecretKeySpec(keyBytes, "HmacSHA256");
    }

    @Bean
    public JwtEncoder jwtEncoder(SecretKey jwtSecretKey) {
        return new NimbusJwtEncoder(new ImmutableSecret<>(jwtSecretKey));
    }

    @Bean
    public JwtDecoder jwtDecoder(SecretKey jwtSecretKey) {
        NimbusJwtDecoder decoder = NimbusJwtDecoder.withSecretKey(jwtSecretKey)
                .macAlgorithm(MacAlgorithm.HS256)
                .build();
        // Checks the expiry time and the issuer of every token
        decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
        return decoder;
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter rolesConverter = new JwtGrantedAuthoritiesConverter();
        rolesConverter.setAuthoritiesClaimName("roles");
        rolesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(rolesConverter);
        return converter;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        return new InMemoryUserDetailsManager(
            User.withUsername("riya").password(encoder.encode("riya123")).roles("USER").build(),
            User.withUsername("admin").password(encoder.encode("admin123")).roles("USER", "ADMIN").build()
        );
    }

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

10.3 Token Service

The token service builds the claims and signs them with the secret key:

TokenService.java
package com.javabytechie.jwtdemo.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;

@Service
public class TokenService {

    private final JwtEncoder jwtEncoder;
    private final String issuer;
    private final long expiryMinutes;

    public TokenService(JwtEncoder jwtEncoder,
                        @Value("${jwt.issuer}") String issuer,
                        @Value("${jwt.expiry-minutes}") long expiryMinutes) {
        this.jwtEncoder = jwtEncoder;
        this.issuer = issuer;
        this.expiryMinutes = expiryMinutes;
    }

    public String generateToken(Authentication authentication) {
        Instant now = Instant.now();

        // "ROLE_ADMIN" becomes "ADMIN" inside the token
        List<String> roles = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .map(authority -> authority.replace("ROLE_", ""))
                .toList();

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer(issuer)
                .subject(authentication.getName())
                .issuedAt(now)
                .expiresAt(now.plus(expiryMinutes, ChronoUnit.MINUTES))
                .claim("roles", roles)
                .build();

        JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build();

        return jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
    }

    public long getExpirySeconds() {
        return expiryMinutes * 60;
    }
}

10.4 Login and Protected Endpoints

AuthController.java
package com.javabytechie.jwtdemo.controller;

import com.javabytechie.jwtdemo.service.TokenService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final TokenService tokenService;

    public AuthController(AuthenticationManager authenticationManager, TokenService tokenService) {
        this.authenticationManager = authenticationManager;
        this.tokenService = tokenService;
    }

    @PostMapping("/login")
    public TokenResponse login(@RequestBody LoginRequest request) {
        // Throws an exception (and returns 401) if the username or password is wrong
        Authentication authentication = authenticationManager.authenticate(
                UsernamePasswordAuthenticationToken.unauthenticated(request.username(), request.password()));

        String token = tokenService.generateToken(authentication);
        return new TokenResponse(token, "Bearer", tokenService.getExpirySeconds());
    }

    public record LoginRequest(String username, String password) {
    }

    public record TokenResponse(String accessToken, String tokenType, long expiresIn) {
    }
}
ProfileController.java
package com.javabytechie.jwtdemo.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class ProfileController {

    // Any logged-in user
    @GetMapping("/profile")
    public Map<String, Object> profile(@AuthenticationPrincipal Jwt jwt) {
        return Map.of(
                "username", jwt.getSubject(),
                "roles", jwt.getClaimAsStringList("roles"),
                "expiresAt", jwt.getExpiresAt()
        );
    }

    // Only users with the ADMIN role
    @GetMapping("/admin/reports")
    public Map<String, String> reports() {
        return Map.of("report", "Monthly sales report");
    }
}

11. Testing with curl

Start the application and log in to get a token:

Terminal
curl -X POST http://localhost:8080/api/auth/login \
     -H "Content-Type: application/json" \
     -d '{"username": "riya", "password": "riya123"}'
Response
{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqYXZhYnl0ZWNoaWUtZGVtbyIs...",
  "tokenType": "Bearer",
  "expiresIn": 900
}

Use the token to call protected endpoints:

Terminal
TOKEN="paste-the-accessToken-here"

# 200 OK: any logged-in user can see their profile
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/profile

# 403 Forbidden: riya does not have the ADMIN role
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/admin/reports

# 401 Unauthorized: no token, or an expired or changed token
curl http://localhost:8080/api/profile

12. Access Tokens and Refresh Tokens

Short-lived tokens are safer, but nobody wants to log in every 15 minutes. That is why many systems use two tokens:

Access Token (JWT)Refresh Token
Used forCalling APIsGetting a new access token
LifetimeShort (for example 5 to 15 minutes)Long (for example days)
Sent toEvery protected APIOnly the login or token endpoint
Stored on the server?NoUsually yes, so it can be revoked

When the access token expires, the client sends the refresh token to the server and receives a fresh access token. If a user logs out or an account is blocked, the server deletes the refresh token, and access ends as soon as the current access token expires. For extra safety, issue a new refresh token each time one is used (called refresh token rotation).

13. Where to Store a JWT

StorageRiskAdvice
localStorage / sessionStorageAny script on the page can read it, so an XSS bug can steal the tokenAvoid for sensitive apps
JavaScript memoryLost on page refresh, but hard to stealGood for short-lived access tokens
HttpOnly, Secure, SameSite cookieScripts cannot read it; needs CSRF protectionA good choice for browser apps
Mobile secure storageProtected by the operating systemUse Keychain (iOS) or Keystore (Android)

14. JWT vs Session Authentication

SessionJWT
Where user data livesOn the serverInside the token
What the client holdsA random session IDThe signed token
Scaling to many serversNeeds a shared session storeAny server with the key can verify
Logout or revokeEasy: delete the sessionHarder: the token stays valid until it expires
Best forTraditional server-rendered websitesAPIs, mobile apps and microservices

Neither option is always better. Many browser-based websites are still simpler and safer with sessions, while APIs and distributed systems benefit most from JWTs.

15. Advantages and Disadvantages

15.1 Advantages

  • No server-side session storage is needed.
  • Works across many servers and microservices.
  • Fast to verify without a database lookup.
  • Works with web, mobile and service-to-service calls.
  • Based on an open standard with libraries for every language.

15.2 Disadvantages

  • Hard to revoke: a stolen token works until it expires, unless you keep a block list.
  • Readable content: the payload is only encoded, so it must not hold secrets.
  • Stale data: if a user's role changes, old tokens still carry the old role until they expire.
  • Larger than a session ID: sending many claims on every request adds overhead.

16. Best Practices

  • Always use HTTPS so tokens cannot be read on the network.
  • Keep access tokens short-lived, for example 5 to 15 minutes.
  • Validate everything: signature, exp, iss and, when used, aud.
  • Allow only the algorithms you expect, and never accept alg: none.
  • Use a strong secret (at least 256 bits for HS256) and keep it out of source code.
  • Prefer RS256 or ES256 when many services verify tokens.
  • Keep the payload small and free of sensitive data.
  • Use refresh tokens with rotation, and store them so they can be revoked.
  • Use a trusted library instead of writing your own signing or parsing code.

17. Common Mistakes to Avoid

  • Decoding without verifying. Reading the payload without checking the signature means trusting anything a user sends.
  • Putting passwords or personal data in the payload. Anyone with the token can read it.
  • Very long expiry times, such as 30 days for an access token.
  • Hard-coding the secret key in the code or committing it to Git.
  • Using a short, guessable secret like "secret", which can be cracked quickly.
  • Sending tokens in URLs. URLs end up in logs and browser history; use the Authorization header.
  • Expecting logout to cancel a JWT automatically. Deleting it on the client does not make it invalid on the server.

18. Conclusion

A JWT is a compact, signed token that carries a user's identity and permissions from the client to the server. Its three parts, the header, payload and signature, make it easy to read and impossible to change without being noticed. Because servers can verify JWTs on their own, they fit naturally into APIs, mobile apps and microservices.

Use JWTs wisely: keep them short-lived, never store secrets in them, always verify the signature and claims, and let a trusted library or Spring Security's built-in support do the hard work.