org.projectlombok
lombok
true
me.paulschwarz
spring-dotenv
4.0.0
com.nimbusds
nimbus-jose-jwt
9.41.1
org.springframework.security
spring-security-oauth2-jose
< /code>
- генерирование RSA-Keys < /li>
< /ul>
public static void generateRSAKey() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
System.out.println("---Start Public Key ---");
System.out.println(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
System.out.println("---End Public Key ---");
System.out.println("---Start Private Key ---");
System.out.println(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
System.out.println("---End Private Key ---");
}
< /code>
application.yaml & .env < /li>
< /ul>
spring:
application:
name: backend
datasource:
url: jdbc:mysql://localhost:3306/backend?createDatabaseIfNotExist=true
username: root
password: toor
jpa:
show-sql: true
jwe:
issuer: backend
registerRequestTokenExpiration: 600 # = 60 * 10 = 10 Minuten
accessTokenExpiration: 300 # = 60 * 5 = 5 Minuten
refreshTokenExpiration: 604800 # = 60 * 60 * 24 * 7 = 7 Tage
signing:
key-id: signing-key
public-key: ${JWT_PUBLIC_SIGNING_KEY}
private-key: ${JWT_PRIVATE_SIGNING_KEY}
encryption:
key-id: encryption-key
public-key: ${JWT_PUBLIC_ENCRYPTION_KEY}
private-key: ${JWT_PRIVATE_ENCRYPTION_KEY}
< /code>
JWT_PUBLIC_SIGNING_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1FdI02OHl3DqbLipUY9JW4wg8otcEcIyY+lRtdSRRm52PYR2TFPdmcuhHgY5b/is8WWSAtIo8aZOBN8NGWM/OVhtbdi8PB1jV1HZlUjqPgkbXDDmuUF5y2Fqmx2wv1ktvkkFkcAZdxfapKX8sqsfB0zRz009r+YBnQUIdfI4RKIRVLGzwIp1RPRSgJ4cP5D4hjkGtB3bKGana0NyW6VIYPKAvR0+g4CCBhGW49YLcI32evBa9boeSGGYMJu4MY4fLAQxZ3aHFYtEfAIoaymmPW3DZMM6Cj6BFrl5tMdFqurpoXi1LaADRNhSxKjSNWuiENYfCNgyqEu/62sy6cSrCwIDAQAB
JWT_PRIVATE_SIGNING_KEY=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUV0jTY4eXcOpsuKlRj0lbjCDyi1wRwjJj6VG11JFGbnY9hHZMU92Zy6EeBjlv+KzxZZIC0ijxpk4E3w0ZYz85WG1t2Lw8HWNXUdmVSOo+CRtcMOa5QXnLYWqbHbC/WS2+SQWRwBl3F9qkpfyyqx8HTNHPTT2v5gGdBQh18jhEohFUsbPAinVE9FKAnhw/kPiGOQa0HdsoZqdrQ3JbpUhg8oC9HT6DgIIGEZbj1gtwjfZ68Fr1uh5IYZgwm7gxjh8sBDFndocVi0R8AihrKaY9bcNkwzoKPoEWuXm0x0Wq6umheLUtoANE2FLEqNI1a6IQ1h8I2DKoS7/razLpxKsLAgMBAAECggEAIt3s7hPFzAQ/5M0IYIVwEkGW7LzIHX0PMStlaL9tgMHaqzdOWtzajR/MEQC3UGo7sQJwpgNEXd8PqJUdCDItIN7No9/sPiLFO9gLgQvmtXu2q74L5MJE8YnJgOGwiCW98ST5w85mCrw2rBqQEqEusyWAvignckleXR4vyXjrQbmI0V0h3+8k5NGvw3ijH0Oem77E4U1gk2koR4I/kRGzFkTCPMD/dZ/cJFn2nEBDoiE5l3DBR0XB2mGekRCKKyF1fE/KPKyyNYUyxCcARc1ng4bwnMwF0SSU0jV1zfGaMOUPB8AkQ72/bWdN2qZGjHdvDHFd9KfXjLR2xxqnJcQF4QKBgQDVLoxuaQjhFLeym5rDpruXZ42qyAUXRitNL80OJwRTo7MaxrmWIDenOtvwZA89gMc6EfolOVW11+Tu3c3ri3CkNp683284dT1LANNHm15yOmmaOyTVudK3RzohzYqGDCH/dtU1sycNw66B1YkefJl2WE4KX0HOJNkUtLxstEiJnwKBgQD+/X/gg9sp9svyVctleiDTKbTT+Z5ZvCDf+SySoEBOWEJ2g+dZD5h9UHjrFdT0IdbjJ3h+Da2ORrnrRAtZ/o4hN2+DRPgj+buul+jlpUBGzHcrMuWqIWutnB9lBGaU/p/PL8PZuGcOzeuqYZ/hNFld1sf32HzzEzpA/6YX38T/FQKBgChvpuV+eNewxVNUnpuD8cs7Miz1ubl7btU6PuvzTKfMwjlO/n7SZ5wJ1xQW9qkU1zVVmZwb7v0KEXuZiOinz1rmnV94BjGwU0r5whmwB88k80j09PyxPZ3UHduxkBfzDzPUx4ZjEAJVXrFz3olw2u8OuubOwL2oUaDS03bPD39DAoGBAPFhWadYSqTb6s55pvRl+yLKDVLIrrz1mQ1MnIQkzRsc87WrQdqOJ+Ugw4aPASslyXz5BVgWbKANlbv/ittjXHpXNunF5TxxENjDFwYlO8aJkZnEMaKuxnbvHs0KsG0E6JVB+x1kMbLS/e8pQSb33k01n9CgEBAuuGuFVl6tRpfNAoGBAJGm1IKTnvU6k1b8D865A51Oi8GSkSINh8p05l8g/Rs6/zPBwjydtlaTR2c4FPVwqYfDNUtNyzouZMq8LCQrb/Z/9wml9FVuO7hgI+69yv39D3rYgtPf/IVwpa/RfDX2rK41SdG2J4IVdSJuzJ9vMvPNW8lnlEEKCElMv/6kjUW8
JWT_PUBLIC_ENCRYPTION_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr4ZRIxaiUlYi+hpEkgx+O+M1LdXTjQ8h6/JuvhCqGq5c5DeGdNuTctUOWPvlP1U0jsRR55qlhJzLfwgy6S9hAoHPWngM7VCmb6KmwhbPZFiB6UH/GYx2r7CdY5pwCpq2eSIFuSDBHaKcMn0oCxQKHnhjoA2Lx7xdbg2tdRc6WaYvIenQuo6M2eLWHzig5XyIHzloCNfa4aH29uwYLge2Sr2hkJZktDMSJKhVyJCWBg5wnzCOTD0RxfmEGVfUFO9G/xWdPLVgf6JJxsNzSiNHqrd5ZAUVC+ALmJuF50KLk8CcnJtR6DFgl25DXSkT8Kj6inBK1eesJGFU/yvxon1h3wIDAQAB
JWT_PRIVATE_ENCRYPTION_KEY=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCvhlEjFqJSViL6GkSSDH474zUt1dONDyHr8m6+EKoarlzkN4Z025Ny1Q5Y++U/VTSOxFHnmqWEnMt/CDLpL2ECgc9aeAztUKZvoqbCFs9kWIHpQf8ZjHavsJ1jmnAKmrZ5IgW5IMEdopwyfSgLFAoeeGOgDYvHvF1uDa11FzpZpi8h6dC6jozZ4tYfOKDlfIgfOWgI19rhofb27BguB7ZKvaGQlmS0MxIkqFXIkJYGDnCfMI5MPRHF+YQZV9QU70b/FZ08tWB/oknGw3NKI0eqt3lkBRUL4AuYm4XnQouTwJycm1HoMWCXbkNdKRPwqPqKcErV56wkYVT/K/GifWHfAgMBAAECggEAFLT4UnbjnGoMA0xhBYfSxpbl2avtoqkD2/WESgjmQHKMg+HnC1cyHGx1HltNLr0LD8KqVPPiDfR/b1OUyfysaaLNxwL9p2uJzwa1vJT7T3+/h7ig8y8me1CPKMZ57E5XM5Anc7EhBzF9UtmABVqOXQzf1+xxLpWSwh579aHRYOzvVu55bLG/bLRfl2WUJ3bINpxtSCyDB/UZJB7xneSfXMCHnfAhCno8BkZbu/RskDnMpM/U8wvDI5mTbWiKonokKCF3s3yYiNrKs7vVbtZJh9T43M8a3ENPcrqDlLJ85Y0/9zCMaCxJt4F1kpRfvUEYbTKkbmoAM6f16IgwBIXSzQKBgQDmcH2lHFPR/UZOL9LumXQNI1RMzA0cvGQSwcDhczR6DieiVRWKKTD6c2ns0y3hzaw2C82OdE6pFo3Ywn7teWUCtkCS+rUsGMoMHC3YhLRxkFOuO/P495ArEFpy8+7UduNmWauz1qg6lJEx9vu+HDB0RaHnMmJ/FZDbVJJoXCrLIwKBgQDC/nqLoqCkfyUa9PiLG74OkIbkSTLv7eiTsLiGYmj5AiGqL+f+/rkZP49d59I9IWi01eQ9nd0V6Vu1cE+WOrJtIoSvPILT+7gFjvOrEqhjXlWIYNZU3p3MKuGvyq49C/B7btdTvivtYZKU3cKj3nnTjrEZFjhSIyfnTFQmXUHoFQKBgBhZeJ1SwfSla6FlYkd+BYpB2m2G/je0HGry+DuaXcgr6Lo5fV4s/hTozx+MLQP4JKNNWfochhdN380wuBLFygugUHB3d19iey8OZzXCyAJb+sulYCFFn4E9aCFPb0QaD+tHvGHzY7FU84axD2bGOcR/ex0f8NJ25+iVJidK3ea7AoGAGJ7vNEBljj+rnLq/wzjOh6JCFgMUFm1wx20x723vmTlmrMl9vpnFH2YCITZLOoLEaMj1F76eEs0zUjaLJgnlS5hnLoUyc7e95Z3GMJybfGiF3kFz7qVpQUVM19h8paKjS8KUF7PUchW232scz3og4dCLlgJTPDPKLw9ZNLrjvn0CgYBCMVd+ONz6/n7Es8zHqNlqAYf+An+DNjijXJ/dFBGIJl+Ncm4DfRT6OAeksyivn9TrPFA5L9KVpcCivwqv9rnupVMYh2b7u2KYuKMha8FPXRjqr/E7eX8jUyeYJ0DAvUx7oVRpgshoqbV/w+8MSqImvB8fF5maGENBhG1u1L/GNQ==
< /code>
JweConfig - Builds RSA Keys from applications.yaml / .env and holds Experiation Values
@ConfigurationProperties(prefix = "spring.jwe")
@Getter
@Setter
public class JweConfig {
@Data
public static class Pair {
private String keyId;
private String publicKey;
private String privateKey;
}
private String issuer;
private long registerRequestTokenExpiration;
private long refreshTokenExpiration;
private long accessTokenExpiration;
private Pair signing;
private Pair encryption;
public RSAKey signingKey() throws Exception {
return buildRsaKey(
signing.getPublicKey(),
signing.getPrivateKey(),
signing.getKeyId(),
true
);
}
public RSAKey encryptionKey() throws Exception {
return buildRsaKey(
encryption.getPublicKey(),
encryption.getPrivateKey(),
encryption.getKeyId(),
false
);
}
private RSAKey buildRsaKey(String pubKey, String priKey, String keyId, boolean signing) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodedPublicKey = Base64.getDecoder().decode(pubKey);
byte[] decodedPrivateKey = Base64.getDecoder().decode(priKey);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(decodedPublicKey);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec);
RSAKey.Builder builder = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(keyId);
if (signing) {
builder.algorithm(JWSAlgorithm.RS256).keyUse(KeyUse.SIGNATURE);
} else {
builder.algorithm(JWEAlgorithm.RSA_OAEP_256).keyUse(KeyUse.ENCRYPTION);
}
return builder.build();
}
@Bean
public JwtEncoder jwtEncoder() throws Exception {
JWKSet jwkSet = new JWKSet(
List.of(
signingKey(),
encryptionKey()
)
);
JWKSource source = (jwkSelector, context) -> jwkSelector.select(jwkSet);
return new NimbusJwtEncoder(source);
}
}
< /code>
- Jwe - Representation of Jwes
public class Jwe {
private final JwtClaimsSet claims;
private final RSAKey signingKey;
private final RSAKey encryptionKey;
private final JwtEncoder jwtEncoder;
public boolean isExpired() {
return claims.getExpiresAt().isBefore(Instant.now());
}
public String getSubject() {
return claims.getSubject();
}
public T get(String name, Class requiredType) {
Object value = claims.getClaim(name);
if (requiredType.isInstance(value)) {
return requiredType.cast(value);
}
return null;
}
@SneakyThrows
public String toString() {
JwsHeader jwsHeader = JwsHeader
.with(SignatureAlgorithm.RS256)
.keyId(signingKey.getKeyID())
.build();
String jws = jwtEncoder
.encode(JwtEncoderParameters.from(jwsHeader, claims))
.getTokenValue();
JWEHeader jweHeader = new JWEHeader
.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
.contentType("JWT")
.keyID(encryptionKey.getKeyID())
.build();
JWEObject jweObject = new JWEObject(jweHeader, new Payload(jws));
jweObject.encrypt(new RSAEncrypter(encryptionKey.toRSAPublicKey()));
return jweObject.serialize();
}
}
< /code>
- JweService - Generates Jwes and parses tokens in string format
@AllArgsConstructor
public class JweService {
private JweConfig config;
public Jwe generateRegisterRequestToken(RegisterUserRequest request, String verificationCode) throws Exception {
var claims = JwtClaimsSet.builder()
.subject(request.getEmail())
.claim("firstName", request.getFirstName())
.claim("lastName", request.getLastName())
.claim("password", request.getPassword())
.claim("code", verificationCode)
.issuer(config.getIssuer())
.issuedAt(Instant.now())
.expiresAt(Instant.ofEpochMilli(System.currentTimeMillis() + 1000 * config.getRegisterRequestTokenExpiration()))
.build();
return new Jwe(
claims,
config.signingKey(),
config.encryptionKey(),
config.jwtEncoder()
);
}
public Jwe parse(String token) {
try {
JWEObject jwe = JWEObject.parse(token);
jwe.decrypt(new RSADecrypter(config.encryptionKey().toRSAPrivateKey()));
JWSObject jwsObject = jwe.getPayload().toJWSObject();
if (jwsObject.verify(new RSASSAVerifier(config.signingKey()))) {
Payload payload = jwsObject.getPayload();
JwtClaimsSet.Builder jwtSetBuilder = JwtClaimsSet.builder();
for (Map.Entry entry: payload.toJSONObject().entrySet()) {
jwtSetBuilder.claim(entry.getKey(), entry.getValue());
}
return new Jwe(
jwtSetBuilder.build(),
config.signingKey(),
config.encryptionKey(),
config.jwtEncoder()
);
}
return null;
} catch (Exception e) {
return null;
}
}
}
< /code>
- UserController - exposes Endpoints
@RequestMapping("/users")
@AllArgsConstructor
public class UserController {
private final JweConfig jwtConfig;
private final JweService jweService;
private final UserRepository userRepository;
private final UserService userService;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@PostMapping()
public ResponseEntity registerUser(
@Valid @RequestBody RegisterUserRequest request,
HttpServletResponse response,
UriComponentsBuilder uriBuilder
) throws Exception {
var user = userRepository.findByEmail(request.getEmail()).orElse(null);
if (!(user == null || (user.getRole() == Role.EXTERNAL && request.getPassword() != null))) {
return ResponseEntity.badRequest().body(
Map.of("email", "Email is allready registered.")
);
}
if (user == null) {
user = userMapper.toEntity(request);
}
if (request.getPassword() != null) {
// Generate Random Code
IntStream stream = new Random().ints(6L, 0, 10);
String code = Arrays.toString(stream.toArray());
code = code.replaceAll("[^0-9]", "");
System.out.println("Der Code für die Email lautet: " + code);
// Create Token
var registerToken = jweService.generateRegisterRequestToken(request, code);
// Place Token in Response
var cookie = new Cookie("register_request_token", registerToken.toString());
cookie.setHttpOnly(true);
cookie.setPath("/users");
cookie.setMaxAge((int) jwtConfig.getRegisterRequestTokenExpiration());
cookie.setSecure(true);
response.addCookie(cookie);
// Send Email.
// Maybe badRequest is no fitting
return ResponseEntity.badRequest().body(
Map.of(
"token", registerToken.toString(),
"message", "Wir haben Ihnen eine Email mit einem Bestätigungscode and ihre Emailadresse " +
user.getEmail() + " geschickt."
)
);
}
user.setRole(Role.EXTERNAL);
userRepository.save(user);
var uri = uriBuilder.path("/users/{id}").buildAndExpand(user.getId()).toUri();
return ResponseEntity.created(uri).body(userMapper.toDto(user));
}
@PostMapping("/code")
public ResponseEntity confirmCode(
@Valid @RequestBody ConfirmCodeRequest codeRequest,
@CookieValue(value = "register_request_token") String token,
UriComponentsBuilder uriBuilder
) {
var jwt = jweService.parse(token);
if (jwt == null || jwt.isExpired()) {
return ResponseEntity.badRequest().body(
Map.of("message", "Token expired")
);
}
if (!codeRequest.getCode().matches(jwt.get("code", String.class))) {
return ResponseEntity.badRequest().body(
Map.of("message", "Invalid code")
);
}
var user = userRepository.findByEmail(jwt.getSubject()).orElse(null);
if (user == null) {
user = userMapper.toEntity(jwt);
}
user.setPassword(passwordEncoder.encode(jwt.get("password", String.class)));
userRepository.save(user);
var uri = uriBuilder.path("/users/{id}").buildAndExpand(user.getId()).toUri();
return ResponseEntity.created(uri).body(userMapper.toDto(user));
}
@GetMapping("/{id}")
public ResponseEntity getUser(@PathVariable UUID id) {
var user = userRepository.findById(id).orElse(null);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(userMapper.toDto(user));
}
@GetMapping()
public Map getAllUsers() {
return userService.getAllUsers();
}
}
< /code>
- SecurityConfig - controlls request to Endpoints
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
var provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(c ->
c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).csrf(c -> c.disable())
.authorizeHttpRequests(c -> c
.requestMatchers("/users/**").permitAll()
.requestMatchers("/registration/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.exceptionHandling(c ->
{
c.authenticationEntryPoint(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)
);
c.accessDeniedHandler((request, response, accessDeniedException) ->
response.setStatus(HttpStatus.FORBIDDEN.value())
);
}
);
return http.build();
}
}
Подробнее здесь: https://stackoverflow.com/questions/796 ... jwe-tokens