Открыть боковую панель
effective-dev-opensource
Effective-Office
Коммиты
aab19e53
Коммит
aab19e53
создал
Май 30, 2025
по автору
Radch-enko
Просмотр файлов
Authorization: implement JWT authentication and authorization
владелец
b16adc9a
Изменения
22
Скрыть пробелы
Построчно
Рядом
README.md
Просмотр файла @
aab19e53
...
...
@@ -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://
local
host: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
\
...
...
backend/app/build.gradle.kts
Просмотр файла @
aab19e53
...
...
@@ -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
)
...
...
backend/app/src/main/kotlin/band/effective/office/backend/app/config/OpenAPIConfig.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
)
)
}
}
\ Нет новой строки в конце файла
backend/app/src/main/kotlin/band/effective/office/backend/app/controller/UserController.kt
Просмотр файла @
aab19e53
...
...
@@ -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
)
}
}
\ Нет новой строки в конце файла
}
backend/app/src/main/resources/application.yml
Просмотр файла @
aab19e53
...
...
@@ -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
:
...
...
backend/domain/build.gradle.kts
Просмотр файла @
aab19e53
...
...
@@ -3,5 +3,5 @@ plugins {
}
dependencies
{
implementation
(
"jakarta.validation:jakarta.validation-api:3.0.2"
)
implementation
(
libs
.
jakarta
)
}
backend/feature/authorization/build.gradle.kts
0 → 100644
Просмотр файла @
aab19e53
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
)
}
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/config/PasswordEncoderConfig.kt
0 → 100644
Просмотр файла @
aab19e53
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
()
}
}
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/config/SecurityConfig.kt
0 → 100644
Просмотр файла @
aab19e53
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
()
}
}
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/controller/AuthController.kt
0 → 100644
Просмотр файла @
aab19e53
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"
)
)
}
}
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/dto/AuthDto.kt
0 → 100644
Просмотр файла @
aab19e53
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
()
)
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/exception/AuthorizationExceptions.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/model/AuthToken.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/security/JwtAuthenticationFilter.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
}
}
}
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/service/AuthorizationService.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
}
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/service/TokenProvider.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
}
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/service/impl/JwtAuthorizationService.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
}
}
\ Нет новой строки в конце файла
backend/feature/authorization/src/main/kotlin/band/effective/office/backend/feature/authorization/service/impl/JwtTokenProvider.kt
0 → 100644
Просмотр файла @
aab19e53
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
)
}
}
}
backend/feature/authorization/src/main/resources/application.yml
0 → 100644
Просмотр файла @
aab19e53
# 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
\ Нет новой строки в конце файла
backend/repository/src/main/kotlin/band/effective/office/backend/repository/mapper/UserMapper.kt
Просмотр файла @
aab19e53
...
...
@@ -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.
...
...
Пред
1
2
След
Редактирование
Предварительный просмотр
Поддерживает Markdown
0%
Попробовать снова
или
прикрепить новый файл
.
Отмена
You are about to add
0
people
to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Отмена
Пожалуйста,
зарегистрируйтесь
или
войдите
чтобы прокомментировать