这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spring-security-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<module>spring-security-compromised-password</module>
<module>spring-security-authorization</module>
<module>spring-security-dynamic-registration</module>
<module>spring-security-ott</module>
</modules>

<build>
Expand Down
67 changes: 67 additions & 0 deletions spring-security-modules/spring-security-ott/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-ott</artifactId>
<description>Spring Security with OTT authentication</description>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-3</relativePath>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>

</dependencies>

<properties>
<spring-boot.version>3.4.1</spring-boot.version>
<logback.version>1.5.7</logback.version>
</properties>


</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.baeldung.security.ott;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SampleOttApplication {

public static void main(String[] args) {
SpringApplication.run(SampleOttApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.baeldung.security.ott.config;

import com.baeldung.security.ott.service.OttSenderService;
import com.baeldung.security.ott.service.FakeOttSenderService;
import com.baeldung.security.ott.web.OttLoginLinkSuccessHandler;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class OttSecurityConfiguration {

@Bean
SecurityFilterChain ottSecurityFilterChain(HttpSecurity http) throws Exception {

return http
.authorizeHttpRequests( ht ->
ht.anyRequest().authenticated())
.formLogin(withDefaults())
.oneTimeTokenLogin( withDefaults())
.build();
}

@Bean
OneTimeTokenGenerationSuccessHandler ottSuccessHandler(OttSenderService ottSenderService) {
return new OttLoginLinkSuccessHandler(ottSenderService);
}

@Bean
OttSenderService ottSenderService() {
return new FakeOttSenderService();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.baeldung.security.ott.service;

import lombok.extern.slf4j.Slf4j;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
public class FakeOttSenderService implements OttSenderService {

private final Map<String,String> lastTokenByUser = new HashMap<>();

@Override
public void sendTokenToUser(String username, String token, Instant expiresAt) {
lastTokenByUser.put(username, token);
log.info("Sending token to username '{}'. token={}, expiresAt={}", username,token,expiresAt);
}

@Override
public Optional<String> getLastTokenForUser(String username) {
return Optional.ofNullable(lastTokenByUser.get(username));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.baeldung.security.ott.service;

import java.time.Instant;
import java.util.Optional;

public interface OttSenderService {

void sendTokenToUser(String username, String token, Instant expirationTime);

// Optional method used for tests
default Optional<String> getLastTokenForUser(String username) { return Optional.empty();}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.baeldung.security.ott.web;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

@GetMapping({"/", "/index"})
public String index(Authentication auth, Model model) {
model.addAttribute("user", auth.getName());
return "index";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.baeldung.security.ott.web;

import com.baeldung.security.ott.service.OttSenderService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;

import java.io.IOException;

@RequiredArgsConstructor
public class OttLoginLinkSuccessHandler implements OneTimeTokenGenerationSuccessHandler {

private final OttSenderService senderService;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/login/ott");

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
senderService.sendTokenToUser(oneTimeToken.getUsername(),oneTimeToken.getTokenValue(),oneTimeToken.getExpiresAt());
redirectHandler.handle(request, response, oneTimeToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#logging.level.web=DEBUG
#logging.level.org.springframework.security=TRACE
server.servlet.session.persistent=false
#spring.security.user.name=alice

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="/css/pico.min.css">
<title>OTT Tutorial :: Token Sent</title>
</head>
<body >
<main class="container">
<h1>Token sent!</h1>
<p>A one-time-token has been sent to the e-mail and/or SMS number associated with your account.</p>
<p>Once you've received it, clink <a href="/login/ott">here</a> to proceed.</p>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark" >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="/css/pico.min.css">
<title>Home</title>
</head>
<body >
<main class="container">
<h1>Hello, <span data-th-text="${user}" id="current-username">{user}</span></h1>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.baeldung.security.ott;

import com.baeldung.security.ott.service.OttSenderService;
import org.jsoup.Jsoup;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.Map;

import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SampleOttApplicationUnitTest {

@LocalServerPort
int port;

@Autowired
OttSenderService ottSenderService;

@Test
void whenLoginWithOtt_thenSuccess() throws Exception {
var baseUrl = "http://localhost:" + port;

var conn = Jsoup.newSession().followRedirects(true);
var loginPage = conn.newRequest(baseUrl)
.followRedirects(true)
.get();

var tokenForms = loginPage.select("form#ott-form");
assertEquals(1,tokenForms.size());
var tokenForm = tokenForms.get(0);
var generateAction = tokenForm.attr("action");
assertNotNull(generateAction);
var csrfToken = requireNonNull(tokenForm.selectFirst("input[name=_csrf]")).attr("value");
assertNotNull(csrfToken);

var tokenSubmitPage = conn.newRequest(baseUrl + generateAction)
.data("username","user")
.data("_csrf",csrfToken)
.post();

var tokenSubmitForm = tokenSubmitPage.selectFirst("form.login-form");
assertNotNull(tokenSubmitForm);
var tokenSubmitAction = tokenSubmitForm.attr("action");
csrfToken = requireNonNull(tokenSubmitForm.selectFirst("input[name=_csrf]")).attr("value");
assertNotNull(csrfToken);

// Retrieve the generated token
var optToken = this.ottSenderService.getLastTokenForUser("user");
assertTrue(optToken.isPresent());

var homePage = conn.newRequest(baseUrl + tokenSubmitAction)
.data("token", optToken.get())
.data("_csrf",csrfToken)
.post();

var username = requireNonNull(homePage.selectFirst("span#current-username")).text();
assertEquals("user",username);

}
}