Security + JWT
Installation
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {
@Autowired
lateinit var rsaKeyConfigProperties: RsaKeyConfigProperties
@Autowired
lateinit var userDetailsService: JpaUserDetailsService
@Value("\${cors.allowedOrigins}")
private lateinit var allowedOrigins: String
@Bean
fun authManager(): AuthenticationManager {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(userDetailsService)
authProvider.setPasswordEncoder(passwordEncoder())
return ProviderManager(authProvider)
}
@Bean
@Throws(Exception::class)
fun filterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector?): SecurityFilterChain {
return http
.csrf { csrf: CsrfConfigurer<HttpSecurity> ->
csrf.disable()
}
.cors(Customizer.withDefaults())
.authorizeHttpRequests { auth ->
auth.requestMatchers("/error/**").permitAll()
auth.requestMatchers("/api/auth/**").permitAll()
auth.requestMatchers("/h2-console/**").permitAll()
auth.requestMatchers("/swagger-ui/**").permitAll()
auth.requestMatchers("/api-docs/**").permitAll()
auth.requestMatchers("/uploads/**").permitAll()
auth.requestMatchers("/images/**").permitAll()
auth.requestMatchers("/api/**").authenticated()
auth.anyRequest().authenticated()
}.headers { headers ->
headers.frameOptions { it.disable() }
}
.sessionManagement { s: SessionManagementConfigurer<HttpSecurity?> ->
s.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
}
.oauth2ResourceServer { oauth2: OAuth2ResourceServerConfigurer<HttpSecurity?> ->
oauth2.jwt { jwt ->
jwt.decoder(
jwtDecoder()
)
}
}
.userDetailsService(userDetailsService)
.httpBasic(Customizer.withDefaults())
.build()
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withPublicKey(rsaKeyConfigProperties.publicKey).build()
}
@Bean
fun jwtEncoder(): JwtEncoder {
val jwk: JWK =
RSAKey.Builder(rsaKeyConfigProperties.publicKey).privateKey(rsaKeyConfigProperties.privateKey).build()
val jwks: JWKSource<SecurityContext> = ImmutableJWKSet(JWKSet(jwk))
return NimbusJwtEncoder(jwks)
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val source = UrlBasedCorsConfigurationSource()
val config = CorsConfiguration()
config.allowedOrigins = allowedOrigins.split(",")
config.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD")
config.allowedHeaders = listOf("*")
config.allowCredentials = true
source.registerCorsConfiguration("/api/**", config)
return source
}
companion object {
private val log: Logger = LoggerFactory.getLogger(SecurityConfig::class.java)
}
}
RSA config
@ConfigurationProperties(prefix = "rsa") @JvmRecord data class RsaKeyConfigProperties(val publicKey: RSAPublicKey, val privateKey: RSAPrivateKey)
Génération des clés RSA
Avec git bash :
genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in private.pem -pubout -out public.pem
AuthUser
class AuthUser(val user: User) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return mutableListOf(SimpleGrantedAuthority("ROLE_${user.role.name}"))
}
override fun getPassword(): String? = user.password
override fun getUsername(): String? = user.username
override fun isAccountNonExpired(): Boolean = true
override fun isAccountNonLocked(): Boolean = true
override fun isCredentialsNonExpired(): Boolean = true
override fun isEnabled(): Boolean = user.enabled
}
Services
@Service
class JpaUserDetailsService(
val userRepository: UserRepository,
val logEventRepository: LogEventRepository,
) : UserDetailsService {
@Throws(UsernameNotFoundException::class)
@Transactional
override fun loadUserByUsername(usernameOrEmail: String): UserDetails {
val user: User = userRepository
.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow { UsernameNotFoundException("User name or email not found: $usernameOrEmail") }
return AuthUser(user)
}
}
@Service
class AuthService {
@Autowired
lateinit var jwtEncoder: JwtEncoder
@Autowired
lateinit var JwtDecoder: JwtDecoder
@Autowired
lateinit var passwordEncoder: PasswordEncoder
@Autowired
lateinit var userRepository: UserRepository
fun generateToken(authentication: Authentication): String {
val now = Instant.now()
val scope: String = authentication.getAuthorities()
.stream()
.map { obj: GrantedAuthority -> obj.authority }
.collect(Collectors.joining(" "))
val user = (authentication.principal as AuthUser).user
val claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(10, ChronoUnit.HOURS))
.subject(authentication.getName())
.claim("scope", scope)
.claim("sub", user.id)
.claim("role", user.role.name)
.claim("username", user.username)
.build()
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).tokenValue
}
fun getActiveUser(token: String): User {
val claims = JwtDecoder.decode(token).claims
val userId = claims["sub"] as UUID
return userRepository.findById(userId).orElseThrow { RuntimeException("User not found") }
}
fun hashPassword(password: String): String {
if (!isBCryptHash(password)) {
return passwordEncoder.encode(password)
}
return password
}
fun isBCryptHash(password: String): Boolean {
return password.matches(Regex("^\\$2[aby]\\$\\d{2}\\$[./A-Za-z0-9]{53}$"))
}
}