404 Not Found

404 Not Found


nginx

OAuth2 and JWT Authentication

JWT is the passport for microservices—stateless, self-contained, and enabling cross-service authentication—putting an end to the challenges of session sharing.

1. What You'll Learn


2. A True Story of an Architect

(1) Pain Point: Sessions Expire in Microservices

After Alice scaled OrderFlow from a single instance to three instances, a session issue arose: when a user logged in on Instance A and the request was load-balanced to Instance B, the session did not exist, and the user was logged out. Bob tried using Redis to share sessions, but this introduced new dependencies and complexity. Charlie requested support for third-party login (Google/GitHub), which the traditional session solution was completely unable to accommodate.

(2) JWT Solution

JWT tokens contain all authentication information, so the server does not need to store sessions:

JAVA
// Issue JWT on login
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();

// Verify JWT on each request (stateless, no session needed)
// Spring Security auto-extracts roles from JWT claims

(3) Revenue

After Alice replaced sessions with JWTs: the three instances no longer need to share a session, enabling zero-cost horizontal scaling; tokens are stored on the client side, making the server stateless; and third-party OAuth 2.0 login is supported.


3. OAuth 2.0 and JWT Concepts

(1) OAuth 2.0 Authorization Flow

100%
sequenceDiagram
    participant U as User Agent<br/>(Browser)
    participant C as Client<br/>(OrderFlow SPA)
    participant AS as Authorization<br/>Server
    participant RS as Resource<br/>Server (API)

    U->>C: Click "Login with GitHub"
    C->>AS: Redirect to /authorize
    AS->>U: Login + Consent page
    U->>AS: Approve
    AS->>C: Redirect back with auth code
    C->>AS: POST /token (auth code + client_secret)
    AS->>C: Access Token (JWT)
    C->>RS: GET /api/orders (Bearer token)
    RS->>RS: Verify JWT signature
    RS->>C: 200 OK + Data
Authorization Model Use Cases Client Types
Authorization Code Flow Web Applications / SPAs Backend Services + Frontend
Client Credentials Inter-service Calls Machine-to-Machine
Resource Owner Password Credentials (ROPC) Deprecated, for testing only First-party apps
Implicit Mode Deprecated Legacy SPA

(2) JWT Token Structure

A JWT consists of three parts, separated by .:

TEXT
Header.Payload.Signature

eyJhbGciOiJS256.eyJzdWIiOiJib2IiLCJyb2xlIjoiQ1VTVU9NRVIifQ.signature
Section Content Example
Header Algorithm + Token Type {"alg":"RS256","typ":"JWT"}
Payload Claims {"sub":"bob","role":"CUSTOMER","exp":1705312000}
Signature Signature of Header+Payload RS256 Signature Integrity Verification
Standard Claim Meaning
sub Subject (User ID)
iss Issuer
aud Audience
exp Expiration
iat Issued At (Issuance Date)
scope Scope of Permissions

4. OAuth 2.0 Resource Server Configuration

(1) Dependencies and Configuration

(1) ▶ Example: pom.xml dependencies

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Output:

TEXT
// Execution Successful

(2) ▶ Example: SecurityFilterChain JWT Configuration

JAVA
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s ->
                s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/login").permitAll()
                .requestMatchers("/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/orders/**").authenticated()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 ->
                oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            String role = jwt.getClaimAsString("role");
            if (role == null) return List.of();
            return List.of(new SimpleGrantedAuthority("ROLE_" + role));
        });
        return converter;
    }
}

Output:

TEXT
// Execution Successful

5. JWT Issuance and Verification

(1) RSA Key Pair Configuration

(1) ▶ Example: Generating an RSA key pair

JAVA
@Configuration
public class JwtConfig {

    @Bean
    public KeyPair keyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048);
        return generator.generateKeyPair();
    }

    @Bean
    public JwtEncoder jwtEncoder(KeyPair keyPair) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
        JWKSource<SecurityContext> jwkSource =
            new ImmutableJWKSet<>(new JWKSet(rsaKey));
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtDecoder jwtDecoder(KeyPair keyPair) {
        return JwtDecoders.withPublicKey((RSAPublicKey) keyPair.getPublic());
    }
}

Output:

TEXT
// Execution Successful

(2) ▶ Example: Issuing a JWT upon login

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

    private final JwtEncoder jwtEncoder;
    private final AuthenticationManager authenticationManager;

    public AuthController(JwtEncoder jwtEncoder,
                          AuthenticationManager authenticationManager) {
        this.jwtEncoder = jwtEncoder;
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) {
        Authentication auth = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.username(), request.password()));

        Instant now = Instant.now();
        Instant expiry = now.plus(1, ChronoUnit.HOURS);

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("orderflow-service")
            .subject(auth.getName())
            .issuedAt(now)
            .expiresAt(expiry)
            .claim("role", auth.getAuthorities().stream()
                .filter(a -> a.getAuthority().startsWith("ROLE_"))
                .map(a -> a.getAuthority().replace("ROLE_", ""))
                .findFirst().orElse("CUSTOMER"))
            .build();

        String token = jwtEncoder
            .encode(JwtEncoderParameters.from(claims))
            .getTokenValue();

        return Map.of("accessToken", token);
    }

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

Output:

TEXT
// Execution Successful

6. Custom JWT Claims and Role Mappings

(1) Role Mapping Strategy

Claim Field Value Mapped to Authority
role: "ADMIN" Single-character string ROLE_ADMIN
roles: ["ADMIN","CUSTOMER"] Character Array ROLE_ADMIN, ROLE_CUSTOMER
scope: "read write" OAuth2 scope SCOPE_read, SCOPE_write

(1) ▶ Example: Custom JwtAuthenticationConverter

JAVA
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        // Support both "role" (single) and "roles" (array) claims
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        String role = jwt.getClaimAsString("role");
        if (role != null) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }

        List<String> roles = jwt.getClaimAsStringList("roles");
        if (roles != null) {
            roles.forEach(r -> authorities.add(
                new SimpleGrantedAuthority("ROLE_" + r)));
        }

        return authorities;
    });
    return converter;
}

Output:

TEXT
// Execution Successful

(2) ▶ Example: Using JWT Information in a Controller

JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @GetMapping("/my")
    public List<OrderResponse> getMyOrders(
            @AuthenticationPrincipal Jwt jwt) {
        String username = jwt.getClaimAsString("sub");
        String role = jwt.getClaimAsString("role");
        return orderService.getOrdersByUsername(username);
    }
}

Output:

TEXT
// Execution Successful
Method Obtaining User Information Applicable Scenarios
@AuthenticationPrincipal Jwt jwt Complete JWT Claims Custom Claim Required
@AuthenticationPrincipal UserDetails user UserDetails Standard user information required
Principal principal getName() Username only

7. Comprehensive Example: Complete Implementation of OrderFlow JWT Authentication

JAVA
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/orders/**").authenticated()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt ->
            Optional.ofNullable(jwt.getClaimAsString("role"))
                .map(role -> List.of(new SimpleGrantedAuthority("ROLE_" + role)))
                .orElse(List.of()));
        return converter;
    }

    @Bean
    public AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService, PasswordEncoder encoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(encoder);
        return new ProviderManager(provider);
    }
}

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

    private final JwtEncoder jwtEncoder;
    private final AuthenticationManager authManager;

    public AuthController(JwtEncoder jwtEncoder, AuthenticationManager authManager) {
        this.jwtEncoder = jwtEncoder;
        this.authManager = authManager;
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest req) {
        Authentication auth = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(req.username(), req.password()));
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("orderflow").subject(auth.getName())
            .issuedAt(now).expiresAt(now.plus(1, ChronoUnit.HOURS))
            .claim("role", auth.getAuthorities().stream()
                .map(a -> a.getAuthority().replace("ROLE_", ""))
                .findFirst().orElse("CUSTOMER"))
            .build();
        String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
        return Map.of("accessToken", token);
    }

    public record LoginRequest(String username, String password) {}
}
💻 Testing Process:

BASH
# 1. Login and get JWT
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"bob","password":"pass123"}' | jq -r '.accessToken')

# 2. Access protected API with JWT
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/api/v1/orders/my

# 3. Admin endpoint with admin JWT
ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"admin123"}' | jq -r '.accessToken')

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://localhost:8080/api/v1/admin/dashboard

❓ FAQ

Q What is the key difference between JWT and sessions?
A Sessions are stateful (stored on the server), while JWTs are stateless (self-contained tokens). JWTs are suitable for microservices and horizontal scaling, while sessions are suitable for monolithic applications. JWTs cannot be actively revoked (unless a blacklist is implemented), whereas sessions can be destroyed immediately.
Q Does JWT use symmetric or asymmetric encryption?
A Asymmetric encryption (RSA/ECDSA) is recommended for production environments. The Resource Server only needs to verify the public key; the private key is kept solely by the Authorization Server. Symmetric encryption (HMAC) carries a high risk of key compromise.
Q What should be done when a JWT expires?
A Common approaches: 1) The refresh token mechanism, which uses a short-term access token and a long-term refresh token; 2) The client detects the expiration and automatically redirects to the login page; 3) Set a reasonable expiration time (1-4 hours).
Q How do I revoke an issued JWT?
A A JWT cannot be revoked on its own. Common approaches include: 1) a short expiration time + a refresh token; 2) a token blacklist (stored in Redis); 3) changing the signing key to invalidate all old tokens.
Q What is the relationship between the OAuth 2.0 Resource Server and the Authorization Server?
A The Authorization Server is responsible for issuing tokens, while the Resource Server is responsible for verifying tokens to protect resources. In this lesson, OrderFlow serves as both the Resource Server and a simplified implementation of the Authorization Server. For production environments, we recommend using professional solutions such as Keycloak.
Q How much information should be included in JWT claims?
A Follow the principle of minimalism. Include only the user identifier (sub) and role; do not include sensitive information (such as passwords or phone numbers). JWTs are Base64-encoded, so anyone can decode them; integrity is guaranteed solely by the signature.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Implement JWT login and API authentication for OrderFlow, replacing httpBasic. Use curl to test the login process and obtain a token → Use the token to access the protected API.

  2. Advanced Exercise (Difficulty: ⭐⭐): Implement the Refresh Token mechanism—the Access Token is valid for 15 minutes, the Refresh Token is valid for 7 days, and the Refresh Token is used to obtain a new Access Token.

  3. Challenge (Difficulty: ⭐⭐⭐): Integrate Keycloak as an external authorization server, with OrderFlow acting solely as a resource server to validate JWTs issued by Keycloak. Consider how to establish trust relationships between the services.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏