Différences
Ci-dessous, les différences entre deux révisions de la page.
Les deux révisions précédentes Révision précédente Prochaine révision | Révision précédente | ||
web:framework:spring:oauth2 [2024/04/16 12:16] – [Dépendances] jcheron | web:framework:spring:oauth2 [2024/04/16 13:58] (Version actuelle) – jcheron | ||
---|---|---|---|
Ligne 32: | Ligne 32: | ||
Créer la clé privée dans **certs** | Créer la clé privée dans **certs** | ||
- | <sxh bash> | + | <sxh bash; |
openssl genpkey -algorithm RSA -out private-key.pem | openssl genpkey -algorithm RSA -out private-key.pem | ||
</ | </ | ||
Extraire la clé publique à partir de la clé privée : | Extraire la clé publique à partir de la clé privée : | ||
- | <sxh bash> | + | <sxh bash; |
openssl rsa -pubout -in private-key.pem -out public-key.pem | openssl rsa -pubout -in private-key.pem -out public-key.pem | ||
</ | </ | ||
Convertir la clé privée au format PKCS : | Convertir la clé privée au format PKCS : | ||
- | <sxh bash> | + | <sxh bash; |
openssl pkcs8 -topk8 -inform PEM -outform PEM -in private-key.pem -out private-key-used.pem -nocrypt | openssl pkcs8 -topk8 -inform PEM -outform PEM -in private-key.pem -out private-key-used.pem -nocrypt | ||
</ | </ | ||
Ligne 58: | Ligne 58: | ||
En résumé, les clés RSA (publique et privée) sont utilisées dans le cadre d' | En résumé, les clés RSA (publique et privée) sont utilisées dans le cadre d' | ||
+ | |||
+ | ==== Intégration RSA/Spring ==== | ||
+ | |||
+ | Créer une classe pour gérer les properties à ajouter pour stocker les 2 clés : | ||
+ | |||
+ | Dans un package **security** à créer : | ||
+ | |||
+ | <sxh kotlin; | ||
+ | import org.springframework.boot.context.properties.ConfigurationProperties | ||
+ | import java.security.interfaces.RSAPrivateKey | ||
+ | import java.security.interfaces.RSAPublicKey | ||
+ | |||
+ | |||
+ | @ConfigurationProperties(prefix = " | ||
+ | @JvmRecord | ||
+ | data class RsaKeyConfigProperties(val publicKey: RSAPublicKey, | ||
+ | </ | ||
+ | |||
+ | Activer cette classe de propriétés directement sur la classe de votre application Spring : | ||
+ | |||
+ | <sxh kotlin; | ||
+ | import fr.zerp.api.security.RsaKeyConfigProperties | ||
+ | import org.springframework.boot.autoconfigure.SpringBootApplication | ||
+ | import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
+ | import org.springframework.boot.runApplication | ||
+ | |||
+ | |||
+ | @SpringBootApplication | ||
+ | @EnableConfigurationProperties(RsaKeyConfigProperties:: | ||
+ | class MyApplication | ||
+ | </ | ||
+ | |||
+ | |||
+ | Ajouter les 2 clés à **application.properties** : | ||
+ | |||
+ | <sxh yaml;title: application.properties> | ||
+ | #JWT | ||
+ | rsa.private-key=classpath: | ||
+ | rsa.public-key=classpath: | ||
+ | </ | ||
+ | |||
+ | ==== Services et authentification ==== | ||
+ | |||
+ | === AuthUser === | ||
+ | Créer une classe AuthUser encapsulant un **User** et implémentant l' | ||
+ | |||
+ | <sxh kotlin; | ||
+ | class AuthUser(user: | ||
+ | |||
+ | val user: User = user | ||
+ | |||
+ | override fun getAuthorities(): | ||
+ | return mutableListOf(SimpleGrantedAuthority(" | ||
+ | } | ||
+ | |||
+ | override fun getPassword(): | ||
+ | |||
+ | override fun getUsername(): | ||
+ | |||
+ | override fun isAccountNonExpired(): | ||
+ | |||
+ | override fun isAccountNonLocked(): | ||
+ | |||
+ | override fun isCredentialsNonExpired(): | ||
+ | |||
+ | override fun isEnabled(): | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === UserRepository === | ||
+ | Modifier votre **UserRepository** pour qu'il permette de rechercher un utilisateur par son login/ | ||
+ | |||
+ | <sxh kotlin> | ||
+ | @RepositoryRestResource(collectionResourceRel = " | ||
+ | interface UserRepository : JpaRepository< | ||
+ | fun findByUsernameOrEmail(username: | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === UserDetailsService === | ||
+ | |||
+ | <sxh kotlin> | ||
+ | import org.springframework.beans.factory.annotation.Autowired | ||
+ | import org.springframework.security.core.userdetails.UserDetails | ||
+ | import org.springframework.security.core.userdetails.UserDetailsService | ||
+ | import org.springframework.security.core.userdetails.UsernameNotFoundException | ||
+ | import org.springframework.stereotype.Service | ||
+ | |||
+ | |||
+ | @Service | ||
+ | class JpaUserDetailsService : UserDetailsService { | ||
+ | |||
+ | @Autowired | ||
+ | lateinit var userRepository: | ||
+ | |||
+ | @Throws(UsernameNotFoundException:: | ||
+ | override fun loadUserByUsername(usernameOrEmail: | ||
+ | val user: AuthUser = userRepository | ||
+ | .findByUsernameOrEmail(usernameOrEmail, | ||
+ | .map { AuthUser(it) } | ||
+ | .orElseThrow { UsernameNotFoundException(" | ||
+ | |||
+ | return user | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === AuthService === | ||
+ | |||
+ | <sxh kotlin; | ||
+ | import org.springframework.beans.factory.annotation.Autowired | ||
+ | import org.springframework.security.core.Authentication | ||
+ | import org.springframework.security.core.GrantedAuthority | ||
+ | import org.springframework.security.crypto.password.PasswordEncoder | ||
+ | import org.springframework.security.oauth2.jwt.JwtClaimsSet | ||
+ | import org.springframework.security.oauth2.jwt.JwtDecoder | ||
+ | import org.springframework.security.oauth2.jwt.JwtEncoder | ||
+ | import org.springframework.security.oauth2.jwt.JwtEncoderParameters | ||
+ | import org.springframework.stereotype.Service | ||
+ | import java.time.Instant | ||
+ | import java.time.temporal.ChronoUnit | ||
+ | import java.util.* | ||
+ | import java.util.stream.Collectors | ||
+ | |||
+ | |||
+ | @Service | ||
+ | class AuthService { | ||
+ | |||
+ | @Autowired | ||
+ | private val jwtEncoder: JwtEncoder? = null | ||
+ | |||
+ | @Autowired | ||
+ | lateinit var JwtDecoder: JwtDecoder | ||
+ | |||
+ | @Autowired | ||
+ | private val passwordEncoder: | ||
+ | |||
+ | @Autowired | ||
+ | private val userRepository: | ||
+ | |||
+ | fun generateToken(authentication: | ||
+ | val now = Instant.now() | ||
+ | |||
+ | val scope: String = authentication.getAuthorities() | ||
+ | .stream() | ||
+ | .map { obj: GrantedAuthority -> obj.authority } | ||
+ | .collect(Collectors.joining(" | ||
+ | |||
+ | val claims = JwtClaimsSet.builder() | ||
+ | .issuer(" | ||
+ | .issuedAt(now) | ||
+ | .expiresAt(now.plus(10, | ||
+ | .subject(authentication.getName()) | ||
+ | .claim(" | ||
+ | .claim(" | ||
+ | .build() | ||
+ | |||
+ | return jwtEncoder!!.encode(JwtEncoderParameters.from(claims)).tokenValue | ||
+ | } | ||
+ | |||
+ | //Exemple de récupération de données dans le token JWT | ||
+ | fun getActiveUser(token: | ||
+ | val claims = JwtDecoder.decode(token).claims | ||
+ | val userId = claims[" | ||
+ | return userRepository!!.findById(userId).orElseThrow { RuntimeException(" | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Configuration ==== | ||
+ | |||
+ | <sxh kotlin; | ||
+ | import com.nimbusds.jose.jwk.JWK | ||
+ | import com.nimbusds.jose.jwk.JWKSet | ||
+ | import com.nimbusds.jose.jwk.RSAKey | ||
+ | import com.nimbusds.jose.jwk.source.ImmutableJWKSet | ||
+ | import com.nimbusds.jose.jwk.source.JWKSource | ||
+ | import com.nimbusds.jose.proc.SecurityContext | ||
+ | import fr.zerp.api.security.JpaUserDetailsService | ||
+ | import fr.zerp.api.security.RsaKeyConfigProperties | ||
+ | import org.slf4j.Logger | ||
+ | import org.slf4j.LoggerFactory | ||
+ | import org.springframework.beans.factory.annotation.Autowired | ||
+ | import org.springframework.context.annotation.Bean | ||
+ | import org.springframework.context.annotation.Configuration | ||
+ | import org.springframework.security.authentication.AuthenticationManager | ||
+ | import org.springframework.security.authentication.ProviderManager | ||
+ | import org.springframework.security.authentication.dao.DaoAuthenticationProvider | ||
+ | import org.springframework.security.config.Customizer | ||
+ | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity | ||
+ | import org.springframework.security.config.annotation.web.builders.HttpSecurity | ||
+ | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity | ||
+ | import org.springframework.security.config.annotation.web.configurers.CorsConfigurer | ||
+ | import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer | ||
+ | import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer | ||
+ | import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer | ||
+ | import org.springframework.security.config.http.SessionCreationPolicy | ||
+ | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder | ||
+ | import org.springframework.security.crypto.password.PasswordEncoder | ||
+ | import org.springframework.security.oauth2.jwt.JwtDecoder | ||
+ | import org.springframework.security.oauth2.jwt.JwtEncoder | ||
+ | import org.springframework.security.oauth2.jwt.NimbusJwtDecoder | ||
+ | import org.springframework.security.oauth2.jwt.NimbusJwtEncoder | ||
+ | import org.springframework.security.web.SecurityFilterChain | ||
+ | import org.springframework.web.servlet.handler.HandlerMappingIntrospector | ||
+ | |||
+ | |||
+ | @Configuration | ||
+ | @EnableWebSecurity | ||
+ | @EnableMethodSecurity | ||
+ | class SecurityConfig { | ||
+ | |||
+ | @Autowired | ||
+ | lateinit var rsaKeyConfigProperties: | ||
+ | |||
+ | @Autowired | ||
+ | lateinit var userDetailsService: | ||
+ | |||
+ | |||
+ | @Bean | ||
+ | fun authManager(): | ||
+ | val authProvider = DaoAuthenticationProvider() | ||
+ | authProvider.setUserDetailsService(userDetailsService) | ||
+ | authProvider.setPasswordEncoder(passwordEncoder()) | ||
+ | return ProviderManager(authProvider) | ||
+ | } | ||
+ | |||
+ | |||
+ | @Bean | ||
+ | @Throws(Exception:: | ||
+ | fun filterChain(http: | ||
+ | return http | ||
+ | .csrf { csrf: CsrfConfigurer< | ||
+ | csrf.disable() | ||
+ | } | ||
+ | .cors { cors: CorsConfigurer< | ||
+ | .authorizeHttpRequests { auth -> | ||
+ | auth.requestMatchers("/ | ||
+ | auth.requestMatchers("/ | ||
+ | auth.requestMatchers("/ | ||
+ | auth.anyRequest().authenticated() | ||
+ | }.headers { headers -> | ||
+ | headers.frameOptions { it.sameOrigin() } | ||
+ | } | ||
+ | .sessionManagement { s: SessionManagementConfigurer< | ||
+ | s.sessionCreationPolicy( | ||
+ | SessionCreationPolicy.STATELESS | ||
+ | ) | ||
+ | } | ||
+ | .oauth2ResourceServer { oauth2: OAuth2ResourceServerConfigurer< | ||
+ | oauth2.jwt { jwt -> | ||
+ | jwt.decoder( | ||
+ | jwtDecoder() | ||
+ | ) | ||
+ | } | ||
+ | } | ||
+ | .userDetailsService(userDetailsService) | ||
+ | .httpBasic(Customizer.withDefaults()) | ||
+ | .build() | ||
+ | } | ||
+ | |||
+ | @Bean | ||
+ | fun jwtDecoder(): | ||
+ | return NimbusJwtDecoder.withPublicKey(rsaKeyConfigProperties.publicKey).build() | ||
+ | } | ||
+ | |||
+ | @Bean | ||
+ | fun jwtEncoder(): | ||
+ | val jwk: JWK = | ||
+ | RSAKey.Builder(rsaKeyConfigProperties.publicKey).privateKey(rsaKeyConfigProperties.privateKey).build() | ||
+ | |||
+ | val jwks: JWKSource< | ||
+ | return NimbusJwtEncoder(jwks) | ||
+ | } | ||
+ | |||
+ | @Bean | ||
+ | fun passwordEncoder(): | ||
+ | return BCryptPasswordEncoder() | ||
+ | } | ||
+ | |||
+ | companion object { | ||
+ | private val log: Logger = LoggerFactory.getLogger(SecurityConfig:: | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | ==== Authentification ==== | ||
+ | === DTO === | ||
+ | |||
+ | <sxh kotlin> | ||
+ | class AuthDTO { | ||
+ | @JvmRecord | ||
+ | data class LoginRequest(val username: String, val password: String) | ||
+ | |||
+ | @JvmRecord | ||
+ | data class Response(val message: String, val token: String) | ||
+ | } | ||
+ | </ | ||
+ | === Controller === | ||
+ | |||
+ | <sxh kotlin> | ||
+ | @RestController | ||
+ | @RequestMapping("/ | ||
+ | @Validated | ||
+ | class AuthController { | ||
+ | |||
+ | @Autowired | ||
+ | lateinit var authService: | ||
+ | |||
+ | @Autowired | ||
+ | lateinit var authenticationManager: | ||
+ | |||
+ | @PostMapping("/ | ||
+ | @Throws(IllegalAccessException:: | ||
+ | fun login(@RequestBody userLogin: AuthDTO.LoginRequest): | ||
+ | val authentication: | ||
+ | authenticationManager | ||
+ | .authenticate( | ||
+ | UsernamePasswordAuthenticationToken( | ||
+ | userLogin.username, | ||
+ | userLogin.password | ||
+ | ) | ||
+ | ) | ||
+ | SecurityContextHolder.getContext().authentication = authentication | ||
+ | val userDetails = authentication.getPrincipal() as AuthUser | ||
+ | log.info(" | ||
+ | val token = authService.generateToken(authentication) | ||
+ | val response: AuthDTO.Response = AuthDTO.Response(" | ||
+ | return ResponseEntity.ok< | ||
+ | } | ||
+ | |||
+ | companion object { | ||
+ | private val log: Logger = LoggerFactory.getLogger(AuthController:: | ||
+ | } | ||
+ | } | ||
+ | </ |