'The dependencies in the application context form a cycle
The dependencies of some of the beans in the application context form a cycle:
authController defined in file [...\AuthController.class]
↓
userServiceImpl defined in file [...\UserServiceImpl.class]
↓
jwtManager defined in file [...\JwtManager.class]
┌─────┐
| securityConfig defined in file [...\SecurityConfig.class]
└─────┘
I have tried with @Lazy but not working.
When I try to change the constructor into setters it gives me errors like:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userServiceImpl' defined in file [...\UserServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 3;
SecurityConfig
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger LOG = LoggerFactory.getLogger(getClass());
private UserDetailsService userService;
private PasswordEncoder bCryptPasswordEncoder;
private ObjectMapper mapper;
@Value("${app.security.jwt.keystore-location}")
private String keyStorePath;
@Value("${app.security.jwt.keystore-password}")
private String keyStorePassword;
@Value("${app.security.jwt.key-alias}")
private String keyAlias;
@Value("${app.security.jwt.private-key-passphrase}")
private String privateKeyPassphrase;
public SecurityConfig(UserDetailsService userService,
PasswordEncoder bCryptPasswordEncoder, ObjectMapper mapper) {
this.userService = userService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.mapper = mapper;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable().formLogin().disable()
.csrf().ignoringAntMatchers(API_URL_PREFIX, H2_URL_PREFIX)
.and()
.headers().frameOptions().sameOrigin() // for H2 Console
.and()
.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, TOKEN_URL).permitAll()
.antMatchers(HttpMethod.DELETE, TOKEN_URL).permitAll()
.antMatchers(HttpMethod.POST, SIGNUP_URL).permitAll()
.antMatchers(HttpMethod.POST, REFRESH_URL).permitAll()
.antMatchers(HttpMethod.GET, PRODUCTS_URL).permitAll()
.mvcMatchers(HttpMethod.POST, "/api/v1/restaurants**")
.hasAuthority(RoleEnum.ADMIN.getAuthority())
.anyRequest().authenticated()
.and()
/* Filter based security configuration
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.addFilterBefore(failureHandler , BearerTokenAuthenticationFilter.class)
.addFilter(new LoginFilter(super.authenticationManager(), mapper))
.addFilter(new JwtAuthenticationFilter(super.authenticationManager()))
*/
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(
jwt -> jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter())))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authorityConverter = new JwtGrantedAuthoritiesConverter();
authorityConverter.setAuthorityPrefix(AUTHORITY_PREFIX);
authorityConverter.setAuthoritiesClaimName(ROLE_CLAIM);
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authorityConverter);
return converter;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return userService;
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH"));
//configuration.setAllowCredentials(true);
// For CORS response headers
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public KeyStore keyStore() {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(keyStorePath);
keyStore.load(resourceAsStream, keyStorePassword.toCharArray());
return keyStore;
} catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) {
LOG.error("Unable to load keystore: {}", keyStorePath, e);
}
throw new IllegalArgumentException("Unable to load keystore");
}
@Bean
public RSAPrivateKey jwtSigningKey(KeyStore keyStore) {
try {
Key key = keyStore.getKey(keyAlias, privateKeyPassphrase.toCharArray());
if (key instanceof RSAPrivateKey) {
return (RSAPrivateKey) key;
}
} catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
LOG.error("Unable to load private key from keystore: {}", keyStorePath, e);
}
throw new IllegalArgumentException("Unable to load private key");
}
@Bean
public RSAPublicKey jwtValidationKey(KeyStore keyStore) {
try {
Certificate certificate = keyStore.getCertificate(keyAlias);
PublicKey publicKey = certificate.getPublicKey();
if (publicKey instanceof RSAPublicKey) {
return (RSAPublicKey) publicKey;
}
} catch (KeyStoreException e) {
LOG.error("Unable to load private key from keystore: {}", keyStorePath, e);
}
throw new IllegalArgumentException("Unable to load RSA public key");
}
@Bean
public JwtDecoder jwtDecoder(RSAPublicKey rsaPublicKey) {
return NimbusJwtDecoder.withPublicKey(rsaPublicKey).build();
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
private final UserRepository repository;
private final UserTokenRepository userTokenRepository;
private final PasswordEncoder bCryptPasswordEncoder;
private final JwtManager tokenManager;
public UserServiceImpl(UserRepository repository, UserTokenRepository userTokenRepository,
PasswordEncoder bCryptPasswordEncoder, JwtManager tokenManager) {
this.repository = repository;
this.userTokenRepository = userTokenRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.tokenManager = tokenManager;
}
JWTManager JwtManager is a custom class that is responsible for generating a new JWT. It uses the java-jwt library from auth0.com. I use public/private keys for signing the token.
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import static java.util.stream.Collectors.toList;
import static the.postbooking.app.security.Constants.EXPIRATION_TIME;
import static the.postbooking.app.security.Constants.ROLE_CLAIM;
@Component
public class JwtManager {
private final RSAPrivateKey privateKey;
private final RSAPublicKey publicKey;
public JwtManager(RSAPrivateKey privateKey, RSAPublicKey publicKey) {
this.privateKey = privateKey;
this.publicKey = publicKey;
}
public String create(UserDetails principal) {
final long now = System.currentTimeMillis();
return JWT.create()
.withIssuer("The Post Booking-App")
.withSubject(principal.getUsername())
.withClaim(ROLE_CLAIM,
principal.getAuthorities().stream().map(GrantedAuthority::getAuthority)
.collect(toList()))
.withIssuedAt(new Date(now))
.withExpiresAt(new Date(now + EXPIRATION_TIME))
//.sign(Algorithm.HMAC512(SECRET_KEY.getBytes(StandardCharsets.UTF_8)));
.sign(Algorithm.RSA256(publicKey, privateKey));
}
}
AuthController The AuthController class is annotated with @RestController to mark it as a REST controller. Then, it uses two beans, UserService and PasswordEncoder, which will be injected at the time of the AuthController construction.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.RestController;
import postbookingapp.api.*;
import the.postbooking.app.entity.UserEntity;
import the.postbooking.app.exception.InvalidRefreshTokenException;
import the.postbooking.app.service.UserService;
import javax.validation.Valid;
import static org.springframework.http.ResponseEntity.*;
@RestController
public class AuthController implements UserApi {
private final UserService service;
private final PasswordEncoder passwordEncoder;
public AuthController(UserService service, PasswordEncoder passwordEncoder) {
this.service = service;
this.passwordEncoder = passwordEncoder;
}
@Override
public ResponseEntity<SignedInUser> signIn(@Valid SignInReq signInReq) {
UserEntity userEntity = service.findUserByUsername(signInReq.getUsername());
if (passwordEncoder.matches(
signInReq.getPassword(),
userEntity.getPassword())) {
return ok(service.getSignedInUser(userEntity));
}
throw new InsufficientAuthenticationException("Unauthorized.");
}
@Override
public ResponseEntity<Void> signOut(@Valid RefreshToken refreshToken) {
service.removeRefreshToken(refreshToken);
return accepted().build();
}
@Override
public ResponseEntity<SignedInUser> signUp(@Valid User user) {
return status(HttpStatus.CREATED).body(service.createUser(user).get());
}
@Override
public ResponseEntity<SignedInUser> getAccessToken(@Valid RefreshToken refreshToken) {
return ok(service.getAccessToken(refreshToken)
.orElseThrow(InvalidRefreshTokenException::new));
}
}
Solution 1:[1]
Try adding spring.main.allow-circular-reference=true
in your application.properties
file
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Chinedu |