Replace HTTP Basic authentication with JWT tokens and introduce Spring Cloud Config for centralized configuration.
JWT-secured Task Management API with:
Task API (pom.xml):
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Spring Cloud Config Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
Config Server (separate project):
<!-- Spring Cloud Config Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
Add to both projects:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Learn how JSON Web Tokens work for stateless authentication.
A JWT has three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huIiwiaWF0IjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
[Header].[Payload].[Signature]
{
"alg": "HS256",
"typ": "JWT"
}
Base64URL encoded → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{
"sub": "john", // Subject (username)
"iat": 1616239022, // Issued at
"exp": 1616242622, // Expiration (1 hour)
"roles": ["ROLE_USER"]
}
Base64URL encoded → eyJzdWIiOiJqb2huIiwiaWF0IjoxNjE2MjM5MDIyfQ
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
);
Result → SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Client Server
| |
| POST /auth/login |
| {username, password} |
|----------------------------->|
| | 1. Validate credentials
| | 2. Generate JWT token
| 200 OK |
| {token: "eyJ..."} |
|<-----------------------------|
| |
| GET /api/tasks |
| Authorization: Bearer eyJ...|
|----------------------------->|
| | 3. Extract token
| | 4. Validate signature
| | 5. Extract username & roles
| | 6. Process request
| 200 OK |
| [tasks...] |
|<-----------------------------|
Aspect | JWT | Session |
Storage | Client-side (localStorage) | Server-side (memory/DB) |
Scalability | Excellent (stateless) | Limited (stateful) |
Performance | No DB lookup per request | DB lookup needed |
Revocation | Hard (until expiry) | Easy (delete session) |
Size | Larger (sent in every request) | Smaller (session ID only) |
Best for | Microservices, APIs | Traditional web apps |
✅ Best Practices:
❌ Don't:
Build JWT token generation and validation utility.
package com.example.taskmanager.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtProperties {
private String secret = "myDefaultSecretKeyThatShouldBeChangedInProduction12345678";
private long expiration = 3600000; // 1 hour in milliseconds
private long refreshExpiration = 86400000; // 24 hours
private String tokenPrefix = "Bearer ";
private String headerString = "Authorization";
}
package com.example.taskmanager.security.jwt;
import com.example.taskmanager.config.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
@Slf4j
public class JwtTokenUtil {
private final JwtProperties jwtProperties;
private final SecretKey secretKey;
public JwtTokenUtil(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
this.secretKey = Keys.hmacShaKeyFor(
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
);
}
// Generate token with user details
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// Add roles to claims
String roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
claims.put("roles", roles);
return createToken(claims, userDetails.getUsername(), jwtProperties.getExpiration());
}
// Generate refresh token
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername(), jwtProperties.getRefreshExpiration());
}
// Create token with claims
private String createToken(Map<String, Object> claims, String subject, long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
// Extract username from token
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// Extract expiration date
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// Extract specific claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// Extract all claims
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
}
// Check if token is expired
public boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// Validate token
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// Extract roles from token
public String extractRoles(String token) {
Claims claims = extractAllClaims(token);
return claims.get("roles", String.class);
}
}
package com.example.taskmanager.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
}
package com.example.taskmanager.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String token;
private String refreshToken;
private String tokenType = "Bearer";
private Long expiresIn;
private String username;
private Set<String> roles;
}
package com.example.taskmanager.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignupRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Full name is required")
private String fullName;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
}
# JWT Configuration
jwt:
secret: ${JWT_SECRET:mySecretKeyForDevelopmentOnlyChangeInProduction1234567890}
expiration: 3600000 # 1 hour
refresh-expiration: 86400000 # 24 hours
token-prefix: "Bearer "
header-string: "Authorization"
Create custom filter to validate JWT tokens on each request.
package com.example.taskmanager.security.jwt;
import com.example.taskmanager.config.JwtProperties;
import com.example.taskmanager.security.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final CustomUserDetailsService userDetailsService;
private final JwtProperties jwtProperties;
public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil,
CustomUserDetailsService userDetailsService,
JwtProperties jwtProperties) {
this.jwtTokenUtil = jwtTokenUtil;
this.userDetailsService = userDetailsService;
this.jwtProperties = jwtProperties;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = extractJwtFromRequest(request);
if (jwt != null && SecurityContextHolder.getContext().getAuthentication() == null) {
String username = jwtTokenUtil.extractUsername(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Set authentication for user: {}", username);
}
}
} catch (Exception e) {
log.error("Cannot set user authentication: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String extractJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(jwtProperties.getHeaderString());
if (bearerToken != null && bearerToken.startsWith(jwtProperties.getTokenPrefix())) {
return bearerToken.substring(jwtProperties.getTokenPrefix().length());
}
return null;
}
}
package com.example.taskmanager.security.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.error("Unauthorized error: {}", authException.getMessage());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("timestamp", LocalDateTime.now().toString());
errorDetails.put("status", HttpServletResponse.SC_UNAUTHORIZED);
errorDetails.put("error", "Unauthorized");
errorDetails.put("message", authException.getMessage());
errorDetails.put("path", request.getRequestURI());
response.getWriter().write(objectMapper.writeValueAsString(errorDetails));
}
}
package com.example.taskmanager.config;
import com.example.taskmanager.security.CustomUserDetailsService;
import com.example.taskmanager.security.jwt.JwtAuthenticationEntryPoint;
import com.example.taskmanager.security.jwt.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(CustomUserDetailsService userDetailsService,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.exceptionHandling(exception ->
exception.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll()
.requestMatchers("/api/auth/**").permitAll()
// Admin only endpoints
.requestMatchers("/api/users/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/tasks/**").hasRole("ADMIN")
// Authenticated endpoints
.requestMatchers("/api/tasks/**").authenticated()
.requestMatchers("/api/categories/**").authenticated()
.anyRequest().authenticated()
)
.headers(headers -> headers.frameOptions(frame -> frame.disable()));
// Add JWT filter
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
Create login, signup, and token refresh endpoints.
package com.example.taskmanager.service;
import com.example.taskmanager.dto.AuthResponse;
import com.example.taskmanager.dto.LoginRequest;
import com.example.taskmanager.dto.SignupRequest;
import com.example.taskmanager.model.User;
import com.example.taskmanager.repository.UserRepository;
import com.example.taskmanager.security.jwt.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AuthService {
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenUtil jwtTokenUtil;
public AuthService(AuthenticationManager authenticationManager,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtTokenUtil jwtTokenUtil) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenUtil = jwtTokenUtil;
}
public AuthResponse login(LoginRequest loginRequest) {
log.debug("Authenticating user: {}", loginRequest.getUsername());
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);
User user = userRepository.findByUsername(loginRequest.getUsername())
.orElseThrow(() -> new RuntimeException("User not found"));
log.info("User logged in: {}", loginRequest.getUsername());
return new AuthResponse(
token,
refreshToken,
"Bearer",
3600L, // 1 hour
user.getUsername(),
user.getRoles()
);
}
@Transactional
public AuthResponse signup(SignupRequest signupRequest) {
log.debug("Registering new user: {}", signupRequest.getUsername());
if (userRepository.existsByUsername(signupRequest.getUsername())) {
throw new RuntimeException("Username already exists");
}
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new RuntimeException("Email already exists");
}
User user = new User();
user.setUsername(signupRequest.getUsername());
user.setEmail(signupRequest.getEmail());
user.setFullName(signupRequest.getFullName());
user.setPassword(passwordEncoder.encode(signupRequest.getPassword()));
user.getRoles().add("ROLE_USER");
userRepository.save(user);
log.info("User registered: {}", signupRequest.getUsername());
// Auto-login after signup
LoginRequest loginRequest = new LoginRequest(
signupRequest.getUsername(),
signupRequest.getPassword()
);
return login(loginRequest);
}
public AuthResponse refreshToken(String refreshToken) {
String username = jwtTokenUtil.extractUsername(refreshToken);
UserDetails userDetails = userRepository.findByUsername(username)
.map(user -> org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().toArray(new String[0]))
.build())
.orElseThrow(() -> new RuntimeException("User not found"));
if (jwtTokenUtil.validateToken(refreshToken, userDetails)) {
String newToken = jwtTokenUtil.generateToken(userDetails);
User user = userRepository.findByUsername(username).get();
return new AuthResponse(
newToken,
refreshToken,
"Bearer",
3600L,
user.getUsername(),
user.getRoles()
);
}
throw new RuntimeException("Invalid refresh token");
}
}
package com.example.taskmanager.controller;
import com.example.taskmanager.dto.AuthResponse;
import com.example.taskmanager.dto.LoginRequest;
import com.example.taskmanager.dto.SignupRequest;
import com.example.taskmanager.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
@Tag(name = "Authentication")
@Slf4j
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@Operation(summary = "User login - Get JWT token")
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest loginRequest) {
log.info("POST /api/auth/login - User: {}", loginRequest.getUsername());
AuthResponse response = authService.login(loginRequest);
return ResponseEntity.ok(response);
}
@Operation(summary = "User signup - Register and get JWT token")
@PostMapping("/signup")
public ResponseEntity<AuthResponse> signup(@Valid @RequestBody SignupRequest signupRequest) {
log.info("POST /api/auth/signup - User: {}", signupRequest.getUsername());
AuthResponse response = authService.signup(signupRequest);
return ResponseEntity.ok(response);
}
@Operation(summary = "Refresh JWT token")
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refreshToken(@RequestBody Map<String, String> request) {
log.info("POST /api/auth/refresh");
String refreshToken = request.get("refreshToken");
AuthResponse response = authService.refreshToken(refreshToken);
return ResponseEntity.ok(response);
}
}
Create centralized configuration server.
Create a new Spring Boot project:
config-server/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>config-server</artifactId>
<version>1.0.0</version>
<name>Config Server</name>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.example.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
config-server/src/main/resources/application.yml:
server:
port: 8888
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: ${CONFIG_REPO_URI:file://${user.home}/config-repo}
default-label: main
clone-on-start: true
# Alternative: Use local filesystem
# native:
# search-locations: classpath:/configs
management:
endpoints:
web:
exposure:
include: health,info,refresh
Create a Git repository for configurations:
# Create config repository
mkdir ~/config-repo
cd ~/config-repo
git init
# Create configuration files
~/config-repo/task-manager.yml (default profile):
# Default configuration for Task Manager
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: update
jwt:
expiration: 3600000 # 1 hour
refresh-expiration: 86400000 # 24 hours
logging:
level:
com.example.taskmanager: INFO
~/config-repo/task-manager-dev.yml:
# Development profile
spring:
datasource:
url: jdbc:h2:mem:taskdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
logging:
level:
com.example.taskmanager: DEBUG
org.hibernate.SQL: DEBUG
~/config-repo/task-manager-prod.yml:
# Production profile
spring:
datasource:
url: jdbc:postgresql://localhost:5432/taskdb
driver-class-name: org.postgresql.Driver
username: postgres
password: "{cipher}AQA1234..." # Encrypted password
jpa:
show-sql: false
hibernate:
ddl-auto: validate
jwt:
secret: "{cipher}AQB5678..." # Encrypted JWT secret
logging:
level:
com.example.taskmanager: WARN
Commit configurations:
git add .
git commit -m "Initial configuration"
config-repo/
├── task-manager.yml # Default config
├── task-manager-dev.yml # Dev profile
├── task-manager-prod.yml # Prod profile
└── task-manager-test.yml # Test profile (optional)
Connect Task Manager API to Config Server.
Already added in earlier step:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
task-manager/src/main/resources/bootstrap.yml:
spring:
application:
name: task-manager
cloud:
config:
uri: http://localhost:8888
fail-fast: true
retry:
initial-interval: 1000
max-attempts: 6
max-interval: 2000
multiplier: 1.1
profiles:
active: dev
task-manager/src/main/resources/application.yml:
# Local overrides (will be overridden by config server)
server:
port: 8080
spring:
application:
name: task-manager
# Actuator for refresh endpoint
management:
endpoints:
web:
exposure:
include: health,info,refresh
1. Config Server (highest priority)
2. application.yml in project
3. Default values in code
Add @RefreshScope to components that need dynamic config:
package com.example.taskmanager.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "jwt")
@RefreshScope // Enable runtime refresh
@Data
public class JwtProperties {
private String secret;
private long expiration;
private long refreshExpiration;
private String tokenPrefix = "Bearer ";
private String headerString = "Authorization";
}
# Update configuration in Git repo
cd ~/config-repo
# Edit task-manager-dev.yml
git add .
git commit -m "Update JWT expiration"
# Refresh application configuration
curl -X POST http://localhost:8080/actuator/refresh
Test JWT authentication and configuration management.
cd config-server
mvn spring-boot:run
Verify: http://localhost:8888/task-manager/dev
cd task-manager
mvn spring-boot:run
Check logs for:
Fetching config from server at: http://localhost:8888
Located environment: name=task-manager, profiles=[dev], ...
1. Signup:
curl -X POST http://localhost:8080/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "test@example.com",
"fullName": "Test User",
"password": "password123"
}'
Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"tokenType": "Bearer",
"expiresIn": 3600,
"username": "testuser",
"roles": ["ROLE_USER"]
}
2. Login:
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"password": "password123"
}'
3. Use Token:
TOKEN="eyJhbGciOiJIUzI1NiJ9..."
curl -X POST http://localhost:8080/api/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "JWT Test Task",
"description": "Testing JWT authentication"
}'
4. Get My Tasks:
curl -X GET http://localhost:8080/api/tasks/my-tasks \
-H "Authorization: Bearer $TOKEN"
5. Refresh Token:
REFRESH_TOKEN="eyJhbGciOiJIUzI1NiJ9..."
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"refreshToken\": \"$REFRESH_TOKEN\"}"
1. Check current config:
curl http://localhost:8888/task-manager/dev
2. Update config:
cd ~/config-repo
# Edit task-manager-dev.yml - change jwt.expiration
git add . && git commit -m "Update expiration"
3. Refresh application:
curl -X POST http://localhost:8080/actuator/refresh
/api/auth/signup or /api/auth/logintoken from responseBearer Congratulations! 🎉 You've implemented JWT authentication and Spring Cloud Config!
JWT & Config features:
You've now built the foundation for microservices:
git add .
git commit -m "Codelab 3.5: JWT & Spring Cloud Config complete"
git tag codelab-3.5