I'm following this from an article here
I'm using Spring boot 3 and java 21 to code.
Based on the articles, I only customize the class name from JwtService to CredentialManager
My version below
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final CredentialManager credentialManager;
private final UserService userService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, "Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
username = credentialManager.extractUsername(jwt);
if (StringUtils.isNotEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.userDetailsService().loadUserByUsername(username);
if (credentialManager.isTokenValid(jwt, userDetails)) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
}
}
filterChain.doFilter(request, response);
}
}
the custom configuration file
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request.requestMatchers("/user/**")
.permitAll().anyRequest().authenticated())
.sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService.userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(@Lazy AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
services
@Service
public class CredentialManager {
@Value("${token.secret-key}")
private String jwtSigningKey;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String userName = extractUsername(token);
return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) {
final Claims claims = extractAllClaims(token);
return claimsResolvers.apply(claims);
}
private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24))
.signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token)
.getBody();
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final LoginServiceImpl loginService;
private final SignUpServiceImpl signUpService;
@Override
public UserDetailsService userDetailsService() {
return username -> (UserDetails) userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@Override
public LoginResponse login(LoginRequest request) {
return loginService.login(request);
}
@Override
public ResponseBody signUp(SignUpRequest request) {
return signUpService.signUp(request);
}
}
@Service
@RequiredArgsConstructor
public class LoginServiceImpl {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final CredentialManager credentialManager;
private final UserRepository userRepository;
private final AuthenticationManager authenticationManager;
public LoginResponse login(LoginRequest request) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.username(), request.password()));
var user = userRepository.findByUsername(request.username())
.orElseThrow(() -> {
logger.error("Invalid username. Username: {}", request.username());
return new BusinessException("Invalid username.");
});
var jwt = credentialManager.generateToken((UserDetails) user);
return new LoginResponse(jwt);
}
}
I'm receiving error message during running the application
┌─────┐
| jwtAuthenticationFilter defined in file [/configuration/JwtAuthenticationFilter.class]
↑ ↓
| userServiceImpl defined in file [/service/impl/UserServiceImpl.class]
↑ ↓
| loginServiceImpl defined in file [/service/impl/user/LoginServiceImpl.class]
↑ ↓
| securityConfiguration defined in file [/configuration/SecurityConfiguration.class]
└─────┘
Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
Anyone can help on this? Is it because of being initiate multiple times in service classes?
Really appreciate it.
my offer is use Converter<Jwt, JwtAuthenticationToken> for fill UserDetails and use default flow for token validate OAuth2TokenValidator