API Security in Java: From Authentication to Authorization (2025)

API security is crucial for protecting your applications and data. This comprehensive guide explores authentication, authorization, and security best practices for Java APIs.
Pro Tip: Implementing proper API security is essential for protecting sensitive data and maintaining application integrity.
Table of Contents
Basic Authentication
Note: Basic authentication is a simple but effective way to secure APIs.
Basic Authentication Implementation
@Configuration
@EnableWebSecurity
public class BasicAuthConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
JWT Authentication
Pro Tip: JWT provides stateless authentication with better scalability.
JWT Implementation
@Component
public class JwtTokenProvider {
private static final String SECRET_KEY = "your-secret-key";
private static final long EXPIRATION_TIME = 86400000; // 24 hours
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
OAuth2 Implementation
Note: OAuth2 provides secure authorization for third-party applications.
OAuth2 Configuration
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret(passwordEncoder().encode("secret"))
.authorizedGrantTypes("password", "refresh_token")
.authorities("ROLE_CLIENT")
.scopes("read", "write")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
Authorization
Pro Tip: Implement fine-grained authorization using roles and permissions.
Role-Based Access Control
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null) {
return false;
}
String permissionString = (String) permission;
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// Implement custom permission logic
return userDetails.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals(permissionString));
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
throw new UnsupportedOperationException();
}
}
@RestController
@RequestMapping("/api/resources")
public class ResourceController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public ResponseEntity getAdminResource() {
return ResponseEntity.ok("Admin Resource");
}
@PreAuthorize("hasPermission(#id, 'Resource', 'READ')")
@GetMapping("/{id}")
public ResponseEntity getResource(@PathVariable Long id) {
return ResponseEntity.ok("Resource " + id);
}
}
Rate Limiting
Note: Rate limiting helps prevent abuse and ensures fair usage of your API.
Rate Limiting Implementation
@Component
public class RateLimiter {
private final Map buckets = new ConcurrentHashMap<>();
public boolean tryConsume(String key, int tokens) {
TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(10, 10, 1000));
return bucket.tryConsume(tokens);
}
}
class TokenBucket {
private final long capacity;
private final double refillRate;
private final long refillPeriod;
private double tokens;
private long lastRefillTime;
public TokenBucket(long capacity, double refillRate, long refillPeriod) {
this.capacity = capacity;
this.refillRate = refillRate;
this.refillPeriod = refillPeriod;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryConsume(int numTokens) {
refill();
if (tokens >= numTokens) {
tokens -= numTokens;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long timePassed = now - lastRefillTime;
double tokensToAdd = timePassed * refillRate / refillPeriod;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
}
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RateLimiter rateLimiter;
@Around("@annotation(rateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = getKey(joinPoint);
if (!rateLimiter.tryConsume(key, rateLimit.tokens())) {
throw new RateLimitExceededException("Rate limit exceeded");
}
return joinPoint.proceed();
}
private String getKey(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int tokens() default 1;
}
Best Practices
Pro Tip: Following security best practices ensures robust API protection.
API Security Best Practices
- Use HTTPS for all API communications
- Implement proper authentication mechanisms
- Use strong password policies
- Implement rate limiting
- Use secure session management
- Implement proper error handling
- Use secure headers
- Implement input validation
- Use secure configuration management
- Implement proper logging
- Use secure communication protocols
- Implement proper access controls
- Use secure key storage
- Implement proper monitoring
- Follow security standards
Conclusion
API security is essential for protecting your applications and data. By implementing these security measures and following best practices, you can create more secure and reliable APIs.