This guide provides a comprehensive walkthrough for building robust authentication and authorization mechanisms using Spring Security with JWT tokens, Redis session storage, and database-backed user management.
Core Concepts of Access Control
Any system involving user participation requires access control mechanisms as part of its security architecture. Access control governs user interactions with system resources, enforcing security policies that restrict users to only their authorized operations. The framework comprises two fundamental components: identity verification (authentication) and permission validation (authorization).
Identity Verification
Consider modern applications like banking apps or streaming platforms. Before accessing personal financial data or premium content, users must verify their identity—typically through credentials, biometrics, or one-time codes. This verification process establishes trust between the user and system.
Common authentication methods include password-based login, QR code scanning, SMS verification, and fingerprint recognition. The primary goal is to validate whether a user is genuinely who they claim to be before granting any resource access.
Permission Validation
After successful authentication, authorization determines what actions a user can perform. For instance, a streaming platform might allow all users to browse content, but only subscribers with specific tiers can download videos for offline viewing. This granular control protects sensitive data and functionality.
Authorization occurs post-authentication, evaluating user privileges against resource access requirements to permit or deny operations.
Spring Security Overview
In the Java ecosystem, Spring Security stands as the de facto standard for application security. While alternatives like Apache Shiro exist, Spring Security's seamless integration with the Spring ecosystem, comprehensive OAuth2 support, and cloud-native capabilities make it ideal for microservices architectures.
Historically derived from Acegi Security, early versions suffered from verbose XML configuration. Modern Spring Boot auto-configuration has dramatically simplified setup—often requiring only a single dependency to secure all endpoints automatically.
Architecture Components
Spring Security maintains a clean separation between authentication and authorization concerns, enabling flexible integration with external identity providers.
Authentication Flow
The security filter chain orchestrates the authentication process through specialized components:
- SecurityContextPersistenceFilter: Bookends each request, retrieving and storing security context from the configured repository
- UsernamePasswordAuthenticationFilter: Processes credential submissions from login forms, delegating to configurable success/failure handlers
- ExceptionTranslationFilter: Captures and translates security exceptions, redirecting to authentication entry points when needed
- FilterSecurityInterceptor: Enforces authorization decisions for protected resources using configured access decision managers
The authentication sequence proceeds as follows:
- User credentials are captured and encapsulated in an
Authenticationtoken - The token is submitted to
AuthenticationManagerfor validation - Upon success, a populated
Authenticationobject (containing granted authorities) is returned - The security context is updated with the authenticated principal
The AuthenticationManager delegates to AuthenticationProvider implementations. For database-backed authentication, DaoAuthenticationProvider collaborates with a UserDetailsService to load user information.
Authorization Flow
Post-authentication, authorization decisions involve three key interfaces:
- FilterSecurityInterceptor intercepts requests to protected resources
- SecurityMetadataSource provides required permissions for the requested resource
- AccessDecisionManager evaluates the user's authorities against required permissions
The AccessDecisionManager consults multiple AccessDecisionVoter instances, similar to how ProviderManager aggregates AuthenticationProvider instances. Voters examine user authorities and required ConfigAttribute objects to cast approval, denial, or abstention votes.
Project Initialization
Create a Spring Boot application with the following dependencies:
<dependencies>
<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-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies>
Define a basic controller:
@RestController
@RequestMapping("/api/greeting")
public class GreetingController {
@GetMapping
public String greet() {
return "Secure Hello World";
}
}
Upon startup, Spring Security generates a default password and secures all endpoints. Accessing /api/greeting redirects to a login page where user and the generated password grant access.
Database-Backed User Store
Replace the in-memory user store with database authentication.
User Entity and Data Access
Create the user entity and repository:
@TableName("app_user")
@Data
public class AppUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String email;
private Boolean enabled;
}
@Mapper
public interface UserRepository extends BaseMapper<AppUser> {
@Select("SELECT * FROM app_user WHERE username = #{username}")
AppUser findByUsername(@Param("username") String username);
}
Custom UserDetailsService
Implement a service to load user details from the database:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Account not found: " + username);
}
SecurityUserPrincipal principal = new SecurityUserPrincipal();
principal.setUser(user);
return principal;
}
}
Security User Principal
@Data
public class SecurityUserPrincipal implements UserDetails {
private AppUser user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.getEnabled();
}
}
Password Encoding Configuration
Configure BCrypt password hashing:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("plaintext_password"));
}
}
Custom Authentication Endpoint
Replace the default form login with a RESTful authentication endpoint.
Authentication Controller
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
@Autowired
private AuthenticationService authService;
@PostMapping("/signin")
public ApiResponse authenticate(@RequestBody CredentialDto credentials) {
return authService.performAuthentication(credentials);
}
}
Authentication Service
@Service
public class AuthenticationService {
@Autowired
private AuthenticationManager authManager;
public ApiResponse performAuthentication(CredentialDto credentials) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
credentials.getPassword()
);
Authentication authentication = authManager.authenticate(authToken);
if (authentication == null || !authentication.isAuthenticated()) {
throw new BadCredentialsException("Invalid credentials");
}
SecurityUserPrincipal principal = (SecurityUserPrincipal) authentication.getPrincipal();
String userId = principal.getUser().getId().toString();
return ApiResponse.success("Authentication successful", userId);
}
}
Security Configuration Update
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
JWT Implementation
Token Generation
Create a utility for JWT operations:
@Component
public class JwtTokenProvider {
private final String secretKey = "your-256-bit-secret-key-minimum-32-chars";
private final long validityMs = 3600000; // 1 hour
public String generateToken(String userId) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validityMs);
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
}
Token-Based Authentication Response
Modify the authentication service to return JWT tokens:
@Service
public class AuthenticationService {
@Autowired
private AuthenticationManager authManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public ApiResponse performAuthentication(CredentialDto credentials) {
Authentication authentication = authManager.authenticate(
new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
credentials.getPassword()
)
);
SecurityUserPrincipal principal = (SecurityUserPrincipal) authentication.getPrincipal();
String userId = principal.getUser().getId().toString();
String jwt = tokenProvider.generateToken(userId);
redisTemplate.opsForValue().set(
"auth:" + userId,
principal,
30,
TimeUnit.MINUTES
);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("accessToken", jwt);
return ApiResponse.success("Login successful", tokenMap);
}
}
Redis Integration for Session Management
Disable Session Creation
Configure stateless session management:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
Token Validation Filter
Create a filter to validate JWT tokens on each request:
@Component
public class JwtTokenValidationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractToken(request);
if (StringUtils.hasText(token)) {
try {
Claims claims = tokenProvider.parseToken(token);
String userId = claims.getSubject();
SecurityUserPrincipal principal = (SecurityUserPrincipal)
redisTemplate.opsForValue().get("auth:" + userId);
if (principal == null) {
throw new AuthenticationCredentialsNotFoundException("Session expired");
}
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
principal,
null,
principal.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
Register the Filter
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(
jwtTokenValidationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
Logout Implementation
Implement token revocation on logout:
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("/logout")
public ApiResponse logout(HttpServletRequest request) {
String token = extractToken(request);
Claims claims = tokenProvider.parseToken(token);
String userId = claims.getSubject();
redisTemplate.delete("auth:" + userId);
SecurityContextHolder.clearContext();
return ApiResponse.success("Logout successful", null);
}
}
Custom Exception Handling
Configure authentication and access denial handlers:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
ApiResponse errorResponse = ApiResponse.error(
HttpStatus.UNAUTHORIZED.value(),
"Authentication required"
);
writeJsonResponse(response, errorResponse);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
ApiResponse errorResponse = ApiResponse.error(
HttpStatus.FORBIDDEN.value(),
"Insufficient privileges"
);
writeJsonResponse(response, errorResponse);
})
);
return http.build();
}
private void writeJsonResponse(HttpServletResponse response, ApiResponse apiResponse)
throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(apiResponse));
}
Authorization Implementation
Permission Models
Two primary approaches exist:
- Role-Based: Users → Roles → Resources
- Permission-Based: Users → Permissions → Resources
Spring Security treats both similarly, though roles typically receive an automatic ROLE_ prefix.
Database Schema
Define roles and permissions entities:
@Data
@TableName("user_role")
public class UserRole {
@TableId
private Integer id;
private String roleName;
}
@Data
@TableName("user_permission")
public class UserPermission {
@TableId
private Integer id;
private String permissionCode;
private String description;
}
Enhanced User Principal
@Data
public class SecurityUserPrincipal implements UserDetails {
private AppUser user;
private List<UserRole> roles = new ArrayList<>();
private List<UserPermission> permissions = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()))
);
permissions.forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission.getPermissionCode()))
);
return authorities;
}
// Other UserDetails methods...
}
Permission Loading Service
@Service
public class PermissionService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PermissionRepository permissionRepository;
public List<UserPermission> loadPermissionsByUsername(String username) {
AppUser user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
List<Integer> roleIds = parseIdList(user.getRoleIds());
List<UserRole> roles = roleRepository.selectBatchIds(roleIds);
Set<Integer> permissionIds = new HashSet<>();
roles.forEach(role ->
permissionIds.addAll(parseIdList(role.getPermissionIds()))
);
return permissionRepository.selectBatchIds(new ArrayList<>(permissionIds));
}
private List<Integer> parseIdList(String idString) {
return Arrays.stream(idString.split(","))
.map(String::trim)
.map(Integer::parseInt)
.collect(Collectors.toList());
}
}
Method-Level Security
Enable method security annotations:
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfiguration {}
Apply permissions to controller methods:
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/dashboard")
@PreAuthorize("hasAuthority('admin:read')")
public String viewDashboard() {
return "Admin Dashboard";
}
@PostMapping("/users")
@PreAuthorize("hasAuthority('admin:create')")
public String createUser(@RequestBody UserDto user) {
return "User created";
}
@PutMapping("/users/{id}")
@PreAuthorize("hasAnyAuthority({'admin:update', 'user:self-update'})")
public String updateUser(@PathVariable Long id, @RequestBody UserDto user) {
return "User updated";
}
}
Frontend Axios Integration
Configure request and response interceptors:
// Request interceptor to attach token
axios.interceptors.request.use(
config => {
const token = sessionStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// Response interceptor to handle errors
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 403) {
alert('Access denied: ' + error.response.data.message);
window.location.href = '/login.html';
}
if (error.response?.status === 401) {
alert('Session expired. Please login again.');
sessionStorage.removeItem('accessToken');
window.location.href = '/login.html';
}
return Promise.reject(error);
}
);
// Store token after successful login
function handleLoginSuccess(response) {
const token = response.data.data.accessToken;
sessionStorage.setItem('accessToken', token);
window.location.href = '/dashboard.html';
}