Коммит aab19e53 создал по автору Radch-enko's avatar Radch-enko
Просмотр файлов

Authorization: implement JWT authentication and authorization

владелец b16adc9a
......@@ -204,10 +204,10 @@ When using Docker Compose, you can also configure the PostgreSQL container:
```bash
# Running locally with custom database credentials
SPRING_DATASOURCE_URL=jdbc:postgresql://custom-host:5432/mydb \
SPRING_DATASOURCE_USERNAME=myuser \
SPRING_DATASOURCE_PASSWORD=mypassword \
./gradlew :app:bootRun --args='--spring.profiles.active=local'
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/effectiveoffice \
SPRING_DATASOURCE_USERNAME=postgres \
SPRING_DATASOURCE_PASSWORD=postgres \
./gradlew :backend:app:bootRun --args='--spring.profiles.active=local'
# Running with Docker Compose with custom database credentials
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb \
......
......@@ -5,6 +5,7 @@ plugins {
dependencies {
implementation(project(":backend:domain"))
implementation(project(":backend:repository"))
implementation(project(":backend:feature:authorization"))
implementation("org.springframework:spring-tx")
implementation(libs.springdoc.openapi.starter.webmvc.ui)
......
package band.effective.office.backend.app.config
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.SecurityRequirement
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/**
* Configuration for OpenAPI documentation.
*/
@Configuration
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
`in` = SecuritySchemeIn.HEADER
)
class OpenAPIConfig {
/**
* Configures the OpenAPI documentation.
*
* @return The OpenAPI configuration.
*/
@Bean
fun openAPI(): OpenAPI {
return OpenAPI()
.info(
Info()
.title("Effective Office API")
.description("API for the Effective Office application")
.version("1.0.0")
)
.addSecurityItem(
SecurityRequirement().addList("bearerAuth")
)
.components(
Components()
.addSecuritySchemes(
"bearerAuth",
io.swagger.v3.oas.models.security.SecurityScheme()
.type(io.swagger.v3.oas.models.security.SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.`in`(io.swagger.v3.oas.models.security.SecurityScheme.In.HEADER)
)
)
}
}
\ Нет новой строки в конце файла
......@@ -7,6 +7,7 @@ import band.effective.office.backend.app.service.UserService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
......@@ -30,7 +31,8 @@ class UserController(private val userService: UserService) {
@GetMapping
@Operation(
summary = "Get all users",
description = "Returns a list of all users in the system"
description = "Returns a list of all users in the system",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "200", description = "Successfully retrieved users")
fun getAllUsers(): ResponseEntity<List<UserDto>> {
......@@ -47,7 +49,8 @@ class UserController(private val userService: UserService) {
@GetMapping("/{id}")
@Operation(
summary = "Get user by ID",
description = "Returns a user by their ID"
description = "Returns a user by their ID",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "200", description = "Successfully retrieved user")
@ApiResponse(responseCode = "404", description = "User not found")
......@@ -57,7 +60,7 @@ class UserController(private val userService: UserService) {
): ResponseEntity<UserDto> {
val user = userService.getUserById(id)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(UserDto.fromDomain(user))
}
......@@ -70,7 +73,8 @@ class UserController(private val userService: UserService) {
@GetMapping("/by-username/{username}")
@Operation(
summary = "Get user by username",
description = "Returns a user by their username"
description = "Returns a user by their username",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "200", description = "Successfully retrieved user")
@ApiResponse(responseCode = "404", description = "User not found")
......@@ -80,7 +84,7 @@ class UserController(private val userService: UserService) {
): ResponseEntity<UserDto> {
val user = userService.getUserByUsername(username)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(UserDto.fromDomain(user))
}
......@@ -94,7 +98,8 @@ class UserController(private val userService: UserService) {
@ResponseStatus(HttpStatus.CREATED)
@Operation(
summary = "Create a new user",
description = "Creates a new user with the provided data"
description = "Creates a new user with the provided data",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "201", description = "User successfully created")
@ApiResponse(responseCode = "400", description = "Invalid input data")
......@@ -116,7 +121,8 @@ class UserController(private val userService: UserService) {
@PutMapping("/{id}")
@Operation(
summary = "Update an existing user",
description = "Updates an existing user with the provided data"
description = "Updates an existing user with the provided data",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "200", description = "User successfully updated")
@ApiResponse(responseCode = "404", description = "User not found")
......@@ -124,16 +130,16 @@ class UserController(private val userService: UserService) {
fun updateUser(
@Parameter(description = "User ID", required = true)
@PathVariable id: UUID,
@Parameter(description = "Updated user data", required = true)
@Valid @RequestBody updateUserDto: UpdateUserDto
): ResponseEntity<UserDto> {
val existingUser = userService.getUserById(id)
?: return ResponseEntity.notFound().build()
val updatedUser = userService.updateUser(id, updateUserDto.toDomain(id, existingUser))
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(UserDto.fromDomain(updatedUser))
}
......@@ -146,7 +152,8 @@ class UserController(private val userService: UserService) {
@DeleteMapping("/{id}")
@Operation(
summary = "Delete a user",
description = "Deletes a user by their ID"
description = "Deletes a user by their ID",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "204", description = "User successfully deleted")
@ApiResponse(responseCode = "404", description = "User not found")
......@@ -155,7 +162,7 @@ class UserController(private val userService: UserService) {
@PathVariable id: UUID
): ResponseEntity<Void> {
val deleted = userService.deleteUser(id)
return if (deleted) {
ResponseEntity.noContent().build()
} else {
......@@ -171,11 +178,12 @@ class UserController(private val userService: UserService) {
@GetMapping("/active")
@Operation(
summary = "Get all active users",
description = "Returns a list of all active users in the system"
description = "Returns a list of all active users in the system",
security = [SecurityRequirement(name = "bearerAuth")]
)
@ApiResponse(responseCode = "200", description = "Successfully retrieved active users")
fun getActiveUsers(): ResponseEntity<List<UserDto>> {
val users = userService.getActiveUsers().map { UserDto.fromDomain(it) }
return ResponseEntity.ok(users)
}
}
\ Нет новой строки в конце файла
}
......@@ -26,7 +26,7 @@ springdoc:
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
packages-to-scan: band.effective.office.backend.app.controller
packages-to-scan: band.effective.office.backend.app.controller, band.effective.office.backend.feature.authorization.controller
management:
endpoints:
......
......@@ -3,5 +3,5 @@ plugins {
}
dependencies {
implementation("jakarta.validation:jakarta.validation-api:3.0.2")
implementation(libs.jakarta)
}
plugins {
id("band.effective.office.backend.spring-boot-common")
}
dependencies {
implementation(project(":backend:domain"))
implementation(project(":backend:repository"))
// Spring Security
implementation(libs.spring.boot.starter.security)
// JWT
implementation(libs.bundles.jwt)
implementation(libs.jakarta)
implementation(libs.jakarta.servlet.api)
implementation(libs.springdoc.openapi.starter.webmvc.ui)
}
package band.effective.office.backend.feature.authorization.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
/**
* Configuration for password encoding.
*/
@Configuration
class PasswordEncoderConfig {
/**
* Creates a password encoder bean.
*/
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.config
import band.effective.office.backend.feature.authorization.security.JwtAuthenticationFilter
import band.effective.office.backend.feature.authorization.service.AuthorizationService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
/**
* Spring Security configuration.
*/
@Configuration
@EnableWebSecurity
class SecurityConfig(
private val authorizationService: AuthorizationService
) {
/**
* Configures the security filter chain.
*/
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.cors { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests { authorize ->
authorize
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/swagger-ui.html/**", "/swagger-ui/**", "/api-docs/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
}
.addFilterBefore(
JwtAuthenticationFilter(authorizationService),
UsernamePasswordAuthenticationFilter::class.java
)
return http.build()
}
}
package band.effective.office.backend.feature.authorization.controller
import band.effective.office.backend.feature.authorization.dto.ErrorResponse
import band.effective.office.backend.feature.authorization.dto.LoginRequest
import band.effective.office.backend.feature.authorization.dto.RefreshTokenRequest
import band.effective.office.backend.feature.authorization.dto.TokenResponse
import band.effective.office.backend.feature.authorization.exception.AuthenticationException
import band.effective.office.backend.feature.authorization.exception.InvalidTokenException
import band.effective.office.backend.feature.authorization.exception.TokenExpiredException
import band.effective.office.backend.feature.authorization.service.AuthorizationService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import java.time.Duration
import kotlin.math.absoluteValue
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
/**
* Controller for authentication endpoints.
*/
@RestController
@RequestMapping("/auth")
@Tag(name = "Authentication", description = "API for authentication operations")
class AuthController(
private val authorizationService: AuthorizationService
) {
/**
* Login endpoint.
*/
@PostMapping("/login")
@Operation(
summary = "User login",
description = "Authenticates a user and returns access and refresh tokens"
)
@ApiResponse(responseCode = "200", description = "Successfully authenticated")
@ApiResponse(responseCode = "401", description = "Authentication failed")
fun login(
@Parameter(description = "Login credentials", required = true)
@Valid @RequestBody request: LoginRequest
): ResponseEntity<TokenResponse> {
val tokenPair = authorizationService.authenticate(request.username, request.password)
return ResponseEntity.ok(
TokenResponse(
accessToken = tokenPair.accessToken.token,
refreshToken = tokenPair.refreshToken.token,
expiresIn = Duration.between(
tokenPair.accessToken.expiresAt,
java.time.Instant.now()
).seconds.absoluteValue
)
)
}
/**
* Refresh token endpoint.
*/
@PostMapping("/refresh")
@Operation(
summary = "Refresh access token",
description = "Generates a new access token using a valid refresh token"
)
@ApiResponse(responseCode = "200", description = "Successfully refreshed tokens")
@ApiResponse(responseCode = "401", description = "Invalid or expired refresh token")
fun refresh(
@Parameter(description = "Refresh token", required = true)
@Valid @RequestBody request: RefreshTokenRequest
): ResponseEntity<TokenResponse> {
val tokenPair = authorizationService.refreshToken(request.refreshToken)
return ResponseEntity.ok(
TokenResponse(
accessToken = tokenPair.accessToken.token,
refreshToken = tokenPair.refreshToken.token,
expiresIn = Duration.between(
tokenPair.accessToken.expiresAt,
java.time.Instant.now()
).seconds.absoluteValue
)
)
}
/**
* Exception handler for authentication exceptions.
*/
@ExceptionHandler(AuthenticationException::class)
fun handleAuthenticationException(ex: AuthenticationException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
ErrorResponse(
status = HttpStatus.UNAUTHORIZED.value(),
message = ex.message ?: "Authentication failed"
)
)
}
/**
* Exception handler for invalid token exceptions.
*/
@ExceptionHandler(InvalidTokenException::class)
fun handleInvalidTokenException(ex: InvalidTokenException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
ErrorResponse(
status = HttpStatus.UNAUTHORIZED.value(),
message = ex.message ?: "Invalid token"
)
)
}
/**
* Exception handler for token expired exceptions.
*/
@ExceptionHandler(TokenExpiredException::class)
fun handleTokenExpiredException(ex: TokenExpiredException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
ErrorResponse(
status = HttpStatus.UNAUTHORIZED.value(),
message = ex.message ?: "Token expired"
)
)
}
}
package band.effective.office.backend.feature.authorization.dto
import jakarta.validation.constraints.NotBlank
/**
* DTO for login request.
*/
data class LoginRequest(
@field:NotBlank(message = "Username is required")
val username: String,
@field:NotBlank(message = "Password is required")
val password: String
)
/**
* DTO for refresh token request.
*/
data class RefreshTokenRequest(
@field:NotBlank(message = "Refresh token is required")
val refreshToken: String
)
/**
* DTO for token response.
*/
data class TokenResponse(
val accessToken: String,
val refreshToken: String,
val tokenType: String = "Bearer",
val expiresIn: Long
)
/**
* DTO for error response.
*/
data class ErrorResponse(
val status: Int,
val message: String,
val timestamp: Long = System.currentTimeMillis()
)
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.exception
/**
* Base class for all authorization-related exceptions.
*/
sealed class AuthorizationException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
/**
* Exception thrown when authentication fails.
*/
class AuthenticationException(message: String, cause: Throwable? = null) : AuthorizationException(message, cause)
/**
* Exception thrown when a token is invalid.
*/
class InvalidTokenException(message: String, cause: Throwable? = null) : AuthorizationException(message, cause)
/**
* Exception thrown when a token has expired.
*/
class TokenExpiredException(message: String, cause: Throwable? = null) : AuthorizationException(message, cause)
/**
* Exception thrown when a user does not have the required permissions.
*/
class AccessDeniedException(message: String, cause: Throwable? = null) : AuthorizationException(message, cause)
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.model
import java.time.Instant
/**
* Base interface for authentication tokens.
*/
interface AuthToken {
/**
* The token value.
*/
val token: String
/**
* The expiration time of the token.
*/
val expiresAt: Instant
/**
* Checks if the token is expired.
*/
fun isExpired(): Boolean = Instant.now().isAfter(expiresAt)
}
/**
* Represents an access token used for authentication.
*/
data class AccessToken(
override val token: String,
override val expiresAt: Instant
) : AuthToken
/**
* Represents a refresh token used to obtain a new access token.
*/
data class RefreshToken(
override val token: String,
override val expiresAt: Instant
) : AuthToken
/**
* Represents a pair of access and refresh tokens.
*/
data class TokenPair(
val accessToken: AccessToken,
val refreshToken: RefreshToken
)
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.security
import band.effective.office.backend.feature.authorization.exception.AuthenticationException
import band.effective.office.backend.feature.authorization.service.AuthorizationService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.web.filter.OncePerRequestFilter
/**
* Filter that extracts and validates the JWT token from the request headers.
*/
class JwtAuthenticationFilter(
private val authorizationService: AuthorizationService
) : OncePerRequestFilter() {
companion object {
private const val AUTHORIZATION_HEADER = "Authorization"
private const val BEARER_PREFIX = "Bearer "
}
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
// Skip filter for login and refresh endpoints
val path = request.requestURI
if (path.endsWith("/auth/login") || path.endsWith("/auth/refresh")) {
filterChain.doFilter(request, response)
return
}
try {
// Extract token from header
val authHeader = request.getHeader(AUTHORIZATION_HEADER)
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response)
return
}
val token = authHeader.substring(BEARER_PREFIX.length)
// Validate token and get user
val user = authorizationService.validateToken(token)
// Create authentication token
val authorities = listOf(SimpleGrantedAuthority("ROLE_USER"))
val authentication = UsernamePasswordAuthenticationToken(
user.username,
null,
authorities
)
authentication.details = WebAuthenticationDetailsSource().buildDetails(request)
// Set authentication in context
SecurityContextHolder.getContext().authentication = authentication
filterChain.doFilter(request, response)
} catch (ex: AuthenticationException) {
// If token validation fails, continue the filter chain without setting authentication
filterChain.doFilter(request, response)
} catch (ex: Exception) {
// For any other exception, continue the filter chain without setting authentication
filterChain.doFilter(request, response)
}
}
}
package band.effective.office.backend.feature.authorization.service
import band.effective.office.backend.domain.model.User
import band.effective.office.backend.feature.authorization.model.TokenPair
/**
* Service interface for authorization operations.
* This interface defines the contract for any authorization implementation.
*/
interface AuthorizationService {
/**
* Authenticates a user with the given credentials and returns a token pair.
*
* @param username The username of the user.
* @param password The password of the user.
* @return A token pair containing access and refresh tokens.
* @throws AuthenticationException If the credentials are invalid.
*/
fun authenticate(username: String, password: String): TokenPair
/**
* Refreshes an access token using a refresh token.
*
* @param refreshToken The refresh token.
* @return A new token pair containing fresh access and refresh tokens.
* @throws AuthenticationException If the refresh token is invalid or expired.
*/
fun refreshToken(refreshToken: String): TokenPair
/**
* Validates an access token and returns the associated user.
*
* @param accessToken The access token to validate.
* @return The user associated with the token.
* @throws AuthenticationException If the token is invalid or expired.
*/
fun validateToken(accessToken: String): User
/**
* Invalidates all tokens for a user (logout).
*
* @param userId The ID of the user.
*/
fun invalidateTokens(userId: String)
}
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.service
import band.effective.office.backend.domain.model.User
import band.effective.office.backend.feature.authorization.model.AccessToken
import band.effective.office.backend.feature.authorization.model.RefreshToken
import band.effective.office.backend.feature.authorization.model.TokenPair
/**
* Interface for token providers.
* This interface defines the contract for any token provider implementation.
*/
interface TokenProvider {
/**
* Generates a token pair for a user.
*
* @param user The user to generate tokens for.
* @return A token pair containing access and refresh tokens.
*/
fun generateTokenPair(user: User): TokenPair
/**
* Validates an access token and returns the user ID.
*
* @param token The access token to validate.
* @return The user ID associated with the token.
* @throws InvalidTokenException If the token is invalid.
* @throws TokenExpiredException If the token has expired.
*/
fun validateAccessToken(token: String): String
/**
* Validates a refresh token and returns the user ID.
*
* @param token The refresh token to validate.
* @return The user ID associated with the token.
* @throws InvalidTokenException If the token is invalid.
* @throws TokenExpiredException If the token has expired.
*/
fun validateRefreshToken(token: String): String
/**
* Invalidates all tokens for a user.
*
* @param userId The ID of the user.
*/
fun invalidateTokens(userId: String)
}
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.service.impl
import band.effective.office.backend.domain.model.User
import band.effective.office.backend.feature.authorization.exception.AuthenticationException
import band.effective.office.backend.feature.authorization.model.TokenPair
import band.effective.office.backend.feature.authorization.service.AuthorizationService
import band.effective.office.backend.feature.authorization.service.TokenProvider
import band.effective.office.backend.repository.UserRepository
import band.effective.office.backend.repository.mapper.UserMapper
import java.util.UUID
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
/**
* JWT implementation of the AuthorizationService interface.
*/
@Service
class JwtAuthorizationService(
private val tokenProvider: TokenProvider,
private val userRepository: UserRepository,
private val userMapper: UserMapper,
private val passwordEncoder: PasswordEncoder
) : AuthorizationService {
override fun authenticate(username: String, password: String): TokenPair {
val user = userRepository.findByUsername(username)
?.let { userMapper.toDomain(it) }
?: throw AuthenticationException("Invalid username or password")
// In a real application, we would check the password against a hashed value stored in the database
// For now, we'll just assume the password is correct (this will be updated when we add password to the user entity)
return tokenProvider.generateTokenPair(user)
}
override fun refreshToken(refreshToken: String): TokenPair {
val userId = try {
tokenProvider.validateRefreshToken(refreshToken)
} catch (ex: Exception) {
throw AuthenticationException("Invalid refresh token", ex)
}
val user = userRepository.findById(UUID.fromString(userId))
.map { userMapper.toDomain(it) }
.orElseThrow { AuthenticationException("User not found") }
return tokenProvider.generateTokenPair(user)
}
override fun validateToken(accessToken: String): User {
val userId = try {
tokenProvider.validateAccessToken(accessToken)
} catch (ex: Exception) {
throw AuthenticationException("Invalid access token", ex)
}
return userRepository.findById(UUID.fromString(userId))
.map { userMapper.toDomain(it) }
.orElseThrow { AuthenticationException("User not found") }
}
override fun invalidateTokens(userId: String) {
tokenProvider.invalidateTokens(userId)
}
}
\ Нет новой строки в конце файла
package band.effective.office.backend.feature.authorization.service.impl
import band.effective.office.backend.domain.model.User
import band.effective.office.backend.feature.authorization.exception.InvalidTokenException
import band.effective.office.backend.feature.authorization.exception.TokenExpiredException
import band.effective.office.backend.feature.authorization.model.AccessToken
import band.effective.office.backend.feature.authorization.model.RefreshToken
import band.effective.office.backend.feature.authorization.model.TokenPair
import band.effective.office.backend.feature.authorization.service.TokenProvider
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.UnsupportedJwtException
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.security.Key
import java.time.Instant
import java.util.Date
import java.util.UUID
import javax.crypto.SecretKey
/**
* JWT implementation of the TokenProvider interface.
*/
@Service
class JwtTokenProvider(
@Value("\${jwt.secret:defaultSecretKeyForDevelopmentOnly}") private val secret: String,
@Value("\${jwt.access-token-expiration-ms:900000}") private val accessTokenExpirationMs: Long, // 15 minutes
@Value("\${jwt.refresh-token-expiration-ms:2592000000}") private val refreshTokenExpirationMs: Long // 30 days
) : TokenProvider {
private val secretKey: SecretKey by lazy {
// Use Keys.secretKeyFor to generate a key that's guaranteed to be secure enough for HS512
Keys.secretKeyFor(SignatureAlgorithm.HS512)
}
// In-memory storage for invalidated tokens (in a real application, this would be a database or Redis)
private val invalidatedTokens: MutableSet<String> = mutableSetOf()
override fun generateTokenPair(user: User): TokenPair {
val now = Instant.now()
val accessTokenExpiration = now.plusMillis(accessTokenExpirationMs)
val accessToken = generateToken(user, accessTokenExpiration, "access")
val refreshTokenExpiration = now.plusMillis(refreshTokenExpirationMs)
val refreshToken = generateToken(user, refreshTokenExpiration, "refresh")
return TokenPair(
AccessToken(accessToken, accessTokenExpiration),
RefreshToken(refreshToken, refreshTokenExpiration)
)
}
override fun validateAccessToken(token: String): String {
return validateToken(token, "access")
}
override fun validateRefreshToken(token: String): String {
return validateToken(token, "refresh")
}
override fun invalidateTokens(userId: String) {
// In a real application, this would invalidate tokens in a database or Redis
invalidatedTokens.add(userId)
}
private fun generateToken(user: User, expiration: Instant, type: String): String {
return Jwts.builder()
.setSubject(user.id.toString())
.claim("type", type)
.claim("username", user.username)
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(expiration))
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact()
}
private fun validateToken(token: String, expectedType: String): String {
try {
val claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.body
// Check if token is of the expected type
val tokenType = claims.get("type", String::class.java)
if (tokenType != expectedType) {
throw InvalidTokenException("Invalid token type: expected $expectedType but got $tokenType")
}
// Check if token has been invalidated
val userId = claims.subject
if (invalidatedTokens.contains(userId)) {
throw InvalidTokenException("Token has been invalidated")
}
return userId
} catch (ex: ExpiredJwtException) {
throw TokenExpiredException("Token has expired", ex)
} catch (ex: UnsupportedJwtException) {
throw InvalidTokenException("Unsupported JWT token", ex)
} catch (ex: MalformedJwtException) {
throw InvalidTokenException("Malformed JWT token", ex)
} catch (ex: SignatureException) {
throw InvalidTokenException("Invalid JWT signature", ex)
} catch (ex: IllegalArgumentException) {
throw InvalidTokenException("JWT claims string is empty", ex)
}
}
}
# JWT Configuration
jwt:
secret: ${JWT_SECRET:defaultSecretKeyForDevelopmentOnly}
access-token-expiration-ms: ${JWT_ACCESS_TOKEN_EXPIRATION_MS:900000} # 15 minutes
refresh-token-expiration-ms: ${JWT_REFRESH_TOKEN_EXPIRATION_MS:2592000000} # 30 days
# Spring Security Configuration
spring:
security:
filter:
order: 10
\ Нет новой строки в конце файла
......@@ -2,10 +2,12 @@ package band.effective.office.backend.repository.mapper
import band.effective.office.backend.domain.model.User
import band.effective.office.backend.repository.entity.UserEntity
import org.springframework.stereotype.Component
/**
* Mapper for converting between User domain model and UserEntity JPA entity.
*/
@Component
object UserMapper {
/**
* Converts a UserEntity to a User domain model.
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать