Skip to content

Commit d2354b7

Browse files
authored
Merge pull request #62 from TP-RENTPLACE/feature/(TP-128)-move-refresh-tokens-to-db
Feature/(tp 128) move refresh tokens to db
2 parents 1859ca6 + a1a21e6 commit d2354b7

14 files changed

Lines changed: 277 additions & 61 deletions

File tree

rentplace/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
3434
implementation 'org.flywaydb:flyway-core'
3535
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:8.0.1'
36+
implementation("commons-codec:commons-codec:1.17.2")
3637

3738
implementation("org.springframework.boot:spring-boot-starter-mail:3.4.3")
3839

rentplace/src/main/java/kattsyn/dev/rentplace/configs/SecurityConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public CorsConfigurationSource corsConfigurationSource() {
9393
config.setAllowedOriginPatterns(List.of("*"));
9494
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
9595
config.setAllowedHeaders(List.of("*"));
96-
config.setExposedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Set-Cookie"));
96+
config.setExposedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Set-Cookie", "User-Agent", "X-Forwarded-For"));
9797
config.setAllowCredentials(true);
9898
config.setMaxAge(3600L);
9999

rentplace/src/main/java/kattsyn/dev/rentplace/controllers/AuthController.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
55
import io.swagger.v3.oas.annotations.tags.Tag;
66
import jakarta.security.auth.message.AuthException;
7+
import jakarta.servlet.http.HttpServletRequest;
78
import kattsyn.dev.rentplace.dtos.requests.CodeRequest;
89
import kattsyn.dev.rentplace.dtos.requests.JwtRequest;
910
import kattsyn.dev.rentplace.dtos.requests.RefreshJwtRequest;
@@ -38,9 +39,11 @@ public ResponseEntity<CodeResponse> requestCode(@RequestBody CodeRequest codeReq
3839
description = "Получает email и код с почты. Возвращает JWT токены"
3940
)
4041
@PostMapping("/login")
41-
public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest authRequest/*,
42+
public ResponseEntity<JwtResponse> login(HttpServletRequest request, @RequestBody JwtRequest authRequest/*,
4243
HttpServletResponse response*/) throws AuthException {
43-
JwtResponse tokens = authService.login(authRequest);
44+
45+
46+
JwtResponse tokens = authService.login(authRequest, request);
4447

4548
/*
4649
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", tokens.getRefreshToken())
@@ -76,10 +79,9 @@ public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest authRequest/*,
7679
description = "Получает email и код с почты. Возвращает JWT токены. Пускает только администраторов."
7780
)
7881
@PostMapping("/admin/login")
79-
public ResponseEntity<JwtResponse> adminLogin(@RequestBody JwtRequest authRequest/*,
82+
public ResponseEntity<JwtResponse> adminLogin(@RequestBody JwtRequest authRequest, HttpServletRequest httpServletRequest/*,
8083
HttpServletResponse response*/) throws AuthException {
81-
JwtResponse tokens = authService.adminLogin(authRequest);
82-
84+
JwtResponse tokens = authService.adminLogin(authRequest, httpServletRequest);
8385
return ResponseEntity.ok()
8486
.body(tokens);
8587
}
@@ -89,9 +91,9 @@ public ResponseEntity<JwtResponse> adminLogin(@RequestBody JwtRequest authReques
8991
description = "Получает email и код с почты, а также имя и фамилию пользователя. Возвращает JWT токены"
9092
)
9193
@PostMapping("/register")
92-
public ResponseEntity<JwtResponse> register(@RequestBody RegisterRequest registerRequest/*,
94+
public ResponseEntity<JwtResponse> register(@RequestBody RegisterRequest registerRequest, HttpServletRequest httpServletRequest/*,
9395
HttpServletResponse response*/) throws AuthException {
94-
JwtResponse tokens = authService.register(registerRequest);
96+
JwtResponse tokens = authService.register(registerRequest, httpServletRequest);
9597

9698
return ResponseEntity.ok()
9799
.body(tokens);
@@ -109,14 +111,13 @@ public ResponseEntity<Void> checkCode(@RequestBody JwtRequest authRequest/*,
109111
}
110112

111113

112-
113114
@Operation(
114115
summary = "Запрос на обновление AccessToken'а",
115116
description = "Получает RefreshToken, возвращает новый AccessToken"
116117
)
117118
@PostMapping("/token")
118-
public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequest request) {
119-
final JwtResponse token = authService.getAccessToken(request.getRefreshToken());
119+
public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequest request, HttpServletRequest httpServletRequest) throws AuthException {
120+
final JwtResponse token = authService.getAccessToken(request.getRefreshToken(), httpServletRequest);
120121
return ResponseEntity.ok(token);
121122
}
122123

@@ -125,8 +126,8 @@ public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequ
125126
description = "Принимает еще не истекший RefreshToken и возвращает новый, продленный."
126127
)
127128
@PostMapping("/refresh")
128-
public ResponseEntity<JwtResponse> refresh(/*@CookieValue(name = "refreshToken") String refreshToken, HttpServletResponse response*/ @RequestBody RefreshJwtRequest request) throws AuthException {
129-
JwtResponse jwtResponse = authService.refresh(request.getRefreshToken());
129+
public ResponseEntity<JwtResponse> refresh(/*@CookieValue(name = "refreshToken") String refreshToken, HttpServletResponse response*/ @RequestBody RefreshJwtRequest refreshJwtRequest, HttpServletRequest request) throws AuthException {
130+
JwtResponse jwtResponse = authService.refresh(refreshJwtRequest.getRefreshToken(), request);
130131

131132
/*
132133
Cookie refreshCookie = new Cookie("refreshToken", jwtResponse.getRefreshToken());
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package kattsyn.dev.rentplace.entities;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
import java.time.LocalDateTime;
10+
11+
@Entity
12+
@Getter
13+
@Setter
14+
@AllArgsConstructor
15+
@NoArgsConstructor
16+
@Table(name = "refresh_tokens")
17+
public class RefreshToken {
18+
@Id
19+
@GeneratedValue(strategy = GenerationType.IDENTITY)
20+
@Column(name = "refresh_token_id")
21+
private Long refreshTokenId;
22+
@Column(name = "token")
23+
private String token;
24+
@Column(name = "device_hash")
25+
private String deviceHash;
26+
@Column(name = "created_at")
27+
private LocalDateTime createdAt;
28+
29+
@ManyToOne(fetch = FetchType.LAZY)
30+
@JoinColumn(name = "user_id")
31+
private User user;
32+
}

rentplace/src/main/java/kattsyn/dev/rentplace/entities/User.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,7 @@ public class User {
6060

6161
@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true)
6262
private Set<Property> properties = new HashSet<>();
63+
64+
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
65+
private Set<RefreshToken> tokens = new HashSet<>();
6366
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package kattsyn.dev.rentplace.repositories;
2+
3+
import kattsyn.dev.rentplace.entities.RefreshToken;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
7+
import java.time.LocalDateTime;
8+
import java.util.List;
9+
import java.util.Optional;
10+
11+
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
12+
13+
@Query("""
14+
SELECT DISTINCT r
15+
FROM RefreshToken r
16+
WHERE r.user.userId=:userId
17+
""")
18+
List<RefreshToken> findAllByUserId(Long userId);
19+
20+
@Query("""
21+
SELECT DISTINCT r
22+
FROM RefreshToken r
23+
WHERE r.user.userId=:userId AND r.token=:refreshToken
24+
""")
25+
Optional<RefreshToken> findByUserAndToken(Long userId, String refreshToken);
26+
27+
void deleteByCreatedAtBefore(LocalDateTime createdAt);
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package kattsyn.dev.rentplace.schedulers;
2+
3+
import jakarta.transaction.Transactional;
4+
import kattsyn.dev.rentplace.repositories.RefreshTokenRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.scheduling.annotation.Scheduled;
9+
import org.springframework.stereotype.Component;
10+
11+
import java.time.LocalDateTime;
12+
13+
@Slf4j
14+
@Component
15+
@RequiredArgsConstructor
16+
public class ExpiredRefreshTokenCleanupTask {
17+
18+
private final RefreshTokenRepository refreshTokenRepository;
19+
@Value("${jwt.expiration-time-in-days.refresh}")
20+
private int refreshTokenExpTimeInDays;
21+
22+
@Scheduled(cron = "0 0 3 * * ?")
23+
@Transactional
24+
public void cleanExpiredRefreshTokens() {
25+
LocalDateTime cutoff = LocalDateTime.now().minusDays(refreshTokenExpTimeInDays);
26+
refreshTokenRepository.deleteByCreatedAtBefore(cutoff);
27+
}
28+
29+
}

rentplace/src/main/java/kattsyn/dev/rentplace/services/AuthService.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kattsyn.dev.rentplace.services;
22

33
import jakarta.security.auth.message.AuthException;
4+
import jakarta.servlet.http.HttpServletRequest;
45
import kattsyn.dev.rentplace.dtos.requests.JwtRequest;
56
import kattsyn.dev.rentplace.dtos.responses.CodeResponse;
67
import kattsyn.dev.rentplace.dtos.responses.JwtResponse;
@@ -11,15 +12,15 @@ public interface AuthService {
1112

1213
CodeResponse getCodeResponse(String email);
1314

14-
JwtResponse login(JwtRequest authRequest) throws AuthException;
15+
JwtResponse login(JwtRequest authRequest, HttpServletRequest httpServletRequest) throws AuthException;
1516

16-
JwtResponse adminLogin(JwtRequest authRequest) throws AuthException;
17+
JwtResponse adminLogin(JwtRequest authRequest, HttpServletRequest httpServletRequest) throws AuthException;
1718

18-
JwtResponse register(RegisterRequest registerRequest) throws AuthException;
19+
JwtResponse register(RegisterRequest registerRequest, HttpServletRequest httpServletRequest) throws AuthException;
1920

20-
JwtResponse getAccessToken(String refreshToken);
21+
JwtResponse getAccessToken(String refreshToken, HttpServletRequest httpServletRequest) throws AuthException;
2122

22-
JwtResponse refresh(String refreshToken) throws AuthException;
23+
JwtResponse refresh(String refreshToken, HttpServletRequest request) throws AuthException;
2324

2425
UserDTO getUserInfo() throws AuthException;
2526

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package kattsyn.dev.rentplace.services;
2+
3+
import io.micrometer.common.lang.NonNull;
4+
import jakarta.security.auth.message.AuthException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import kattsyn.dev.rentplace.dtos.responses.JwtResponse;
7+
import kattsyn.dev.rentplace.entities.User;
8+
9+
public interface RefreshTokenService {
10+
11+
void put(String refreshToken, User user, HttpServletRequest httpServletRequest);
12+
13+
JwtResponse refresh(String refreshToken, HttpServletRequest httpServletRequest) throws AuthException;
14+
15+
JwtResponse refreshAccessToken(@NonNull String refreshToken, HttpServletRequest request) throws AuthException;
16+
}
Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package kattsyn.dev.rentplace.services.impl;
22

3-
import io.jsonwebtoken.Claims;
43
import io.micrometer.common.lang.NonNull;
54
import jakarta.security.auth.message.AuthException;
5+
import jakarta.servlet.http.HttpServletRequest;
66
import kattsyn.dev.rentplace.dtos.requests.JwtRequest;
77
import kattsyn.dev.rentplace.dtos.responses.CodeResponse;
88
import kattsyn.dev.rentplace.dtos.responses.JwtResponse;
@@ -14,6 +14,7 @@
1414
import kattsyn.dev.rentplace.enums.UserStatus;
1515
import kattsyn.dev.rentplace.exceptions.ForbiddenException;
1616
import kattsyn.dev.rentplace.services.AuthService;
17+
import kattsyn.dev.rentplace.services.RefreshTokenService;
1718
import kattsyn.dev.rentplace.services.UserService;
1819
import kattsyn.dev.rentplace.auth.JwtProvider;
1920
import kattsyn.dev.rentplace.services.VerificationCodeService;
@@ -23,28 +24,23 @@
2324
import org.springframework.security.core.context.SecurityContextHolder;
2425
import org.springframework.stereotype.Service;
2526

26-
import java.util.HashMap;
27-
import java.util.Map;
2827
import java.util.Optional;
2928

3029
@Service
3130
@RequiredArgsConstructor
3231
@Slf4j
3332
public class AuthServiceImpl implements AuthService {
3433

35-
//todo: сделать хранение Refresh Токенов в БД, вместе с ip, либо именами устройств.
36-
//TODO: также при превышении кол-ва макс устройств разлогинить везде пользователя
37-
3834
private final UserService userService;
39-
private final Map<String, String> refreshStorage = new HashMap<>();
4035
private final JwtProvider jwtProvider;
4136
private final VerificationCodeService verificationCodeService;
37+
private final RefreshTokenService refreshTokenService;
4238

4339
@Override
44-
public JwtResponse register(@NonNull RegisterRequest registerRequest) throws AuthException {
40+
public JwtResponse register(@NonNull RegisterRequest registerRequest, HttpServletRequest httpServletRequest) throws AuthException {
4541
User user = userService.createUserWithRegisterRequest(registerRequest);
4642

47-
return getJwtResponse(user, registerRequest.getEmail(), registerRequest.getCode());
43+
return getJwtResponse(user, registerRequest.getEmail(), registerRequest.getCode(), httpServletRequest);
4844
}
4945

5046
@Override
@@ -65,28 +61,28 @@ public CodeResponse getCodeResponse(String email) {
6561
}
6662

6763
@Override
68-
public JwtResponse login(@NonNull JwtRequest authRequest) throws AuthException {
64+
public JwtResponse login(@NonNull JwtRequest authRequest, HttpServletRequest httpServletRequest) throws AuthException {
6965
final User user = userService.getUserByEmail(authRequest.getEmail());
7066

71-
return getJwtResponse(user, authRequest.getEmail(), authRequest.getCode());
67+
return getJwtResponse(user, authRequest.getEmail(), authRequest.getCode(), httpServletRequest);
7268
}
7369

7470
@Override
75-
public JwtResponse adminLogin(@NonNull JwtRequest authRequest) throws AuthException {
71+
public JwtResponse adminLogin(@NonNull JwtRequest authRequest, HttpServletRequest httpServletRequest) throws AuthException {
7672
final User user = userService.getUserByEmail(authRequest.getEmail());
7773

7874
if (user.getRole() != Role.ROLE_ADMIN) {
7975
throw new ForbiddenException("You are not allowed to access admin-panel.");
8076
}
8177

82-
return getJwtResponse(user, authRequest.getEmail(), authRequest.getCode());
78+
return getJwtResponse(user, authRequest.getEmail(), authRequest.getCode(), httpServletRequest);
8379
}
8480

85-
private JwtResponse getJwtResponse(User user, String email, String code) throws AuthException {
81+
private JwtResponse getJwtResponse(User user, String email, String code, HttpServletRequest httpServletRequest) throws AuthException {
8682
if ((email.equals("testadmin@gmail.com") && code.equals("12345")) || verificationCodeService.validateCode(email, code)) { //todo: delete test user
8783
final String accessToken = jwtProvider.generateAccessToken(user);
8884
final String refreshToken = jwtProvider.generateRefreshToken(user);
89-
refreshStorage.put(user.getEmail(), refreshToken);
85+
refreshTokenService.put(refreshToken, user, httpServletRequest);
9086
return new JwtResponse(accessToken, refreshToken);
9187
} else {
9288
throw new AuthException("Код неправильный");
@@ -98,34 +94,16 @@ public void validateCode(JwtRequest request) {
9894
verificationCodeService.validateCode(request.getEmail(), request.getCode());
9995
}
10096

101-
public JwtResponse getAccessToken(@NonNull String refreshToken) {
102-
if (jwtProvider.validateRefreshToken(refreshToken)) {
103-
final Claims claims = jwtProvider.getRefreshClaims(refreshToken);
104-
final String email = claims.getSubject();
105-
final String saveRefreshToken = refreshStorage.get(email);
106-
if (saveRefreshToken != null && saveRefreshToken.equals(refreshToken)) {
107-
final User user = userService.getUserByEmail(email);
108-
final String accessToken = jwtProvider.generateAccessToken(user);
109-
return new JwtResponse(accessToken, null);
110-
}
97+
public JwtResponse getAccessToken(@NonNull String refreshToken, HttpServletRequest httpServletRequest) {
98+
try {
99+
return refreshTokenService.refreshAccessToken(refreshToken, httpServletRequest);
100+
} catch (AuthException e) {
101+
return new JwtResponse(null, null);
111102
}
112-
return new JwtResponse(null, null);
113103
}
114104

115-
public JwtResponse refresh(@NonNull String refreshToken) throws AuthException {
116-
if (jwtProvider.validateRefreshToken(refreshToken)) {
117-
final Claims claims = jwtProvider.getRefreshClaims(refreshToken);
118-
final String email = claims.getSubject();
119-
final String saveRefreshToken = refreshStorage.get(email);
120-
if (saveRefreshToken != null && saveRefreshToken.equals(refreshToken)) {
121-
final User user = userService.getUserByEmail(email);
122-
final String accessToken = jwtProvider.generateAccessToken(user);
123-
final String newRefreshToken = jwtProvider.generateRefreshToken(user);
124-
refreshStorage.put(user.getEmail(), newRefreshToken);
125-
return new JwtResponse(accessToken, newRefreshToken);
126-
}
127-
}
128-
throw new AuthException("Невалидный JWT токен");
105+
public JwtResponse refresh(@NonNull String refreshToken, HttpServletRequest request) throws AuthException {
106+
return refreshTokenService.refresh(refreshToken, request);
129107
}
130108

131109
public UserDTO getUserInfo() throws AuthException {
@@ -137,6 +115,4 @@ public UserDTO getUserInfo() throws AuthException {
137115
String email = authentication.getName();
138116
return userService.getUserDTOByEmail(email);
139117
}
140-
141-
142118
}

0 commit comments

Comments
 (0)