initial implementation of registration captcha
@ -2,13 +2,10 @@ package org.ros.chatto;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.ros.chatto.service.DBInitializerService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ChattoApplication extends SpringBootServletInitializer {
|
||||
@ -16,6 +13,21 @@ public class ChattoApplication extends SpringBootServletInitializer {
|
||||
public static void main(String[] args) throws SQLException {
|
||||
SpringApplication application = new SpringApplication(ChattoApplication.class);
|
||||
application.run();
|
||||
|
||||
// WebCaptcha webCaptcha = WebCaptcha.builder().captchaBehaviour(new SimpleCaptchaBehavior()).build();
|
||||
// webCaptcha.generateCaptcha();
|
||||
//
|
||||
// // @formatter:off
|
||||
// webCaptcha = WebCaptcha.builder()
|
||||
// .captchaBehaviour(
|
||||
// ManualCaptchaBehaviour.builder()
|
||||
// .length(8)
|
||||
// .style("black")
|
||||
// .build()
|
||||
// ).build();
|
||||
//
|
||||
// // @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -77,7 +77,8 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
|
||||
.anyRequest()
|
||||
// .hasAnyRole("USER", "ADMIN", "SUPER_USER")
|
||||
.authenticated().and().httpBasic().authenticationEntryPoint(authenticationEntryPoint)
|
||||
.authenticated()
|
||||
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint)
|
||||
// .and()
|
||||
// .logout().invalidateHttpSession(true).clearAuthentication(true)
|
||||
// .logoutRequestMatcher(new AntPathRequestMatcher("/api/perform_logout"))
|
||||
|
@ -0,0 +1,10 @@
|
||||
package org.ros.chatto.captcha;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
interface CaptchaBehaviour {
|
||||
public BufferedImage generateCaptcha();
|
||||
public BufferedImage generateCaptcha(String captchaText);
|
||||
public String getRandomChars(int size);
|
||||
public String getRandomChars();
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package org.ros.chatto.captcha;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
/*Class for providing your own captcha generator*/
|
||||
@Builder
|
||||
public class ManualCaptchaBehaviour implements CaptchaBehaviour{
|
||||
private final int length;
|
||||
private final String style;
|
||||
@Override
|
||||
public BufferedImage generateCaptcha() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage generateCaptcha(String captchaText) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRandomChars(int size) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRandomChars() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
183
chatto/src/main/java/org/ros/chatto/captcha/SimpleCaptcha.java
Normal file
@ -0,0 +1,183 @@
|
||||
package org.ros.chatto.captcha;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* This class represents a simple captcha consisting
|
||||
* of an image {@code png} and its text value.
|
||||
* Comic Neue Bold Font.
|
||||
* Capital english letters {@code ONLY}.
|
||||
*
|
||||
* @since 1.3
|
||||
* @author Gennadiy Golovin
|
||||
*/
|
||||
public final class SimpleCaptcha {
|
||||
|
||||
private BufferedImage imagePng;
|
||||
private char[] text;
|
||||
|
||||
/**
|
||||
* Initializes a newly created default object
|
||||
* consisting of 8 capital english letters.
|
||||
*/
|
||||
public SimpleCaptcha() {
|
||||
this.text = getRandomChars();
|
||||
|
||||
try {
|
||||
generateCaptcha();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a newly created object, which length
|
||||
* depends on the passed {@code int} parameter,
|
||||
* which {@code MUST} be greater than 0.
|
||||
* If the condition is not met, initializes a newly
|
||||
* created default object consisting of 8 symbols.
|
||||
*
|
||||
* @param length the quantity of symbols, that the
|
||||
* captcha consists of, greater than 0.
|
||||
*/
|
||||
public SimpleCaptcha(int length) {
|
||||
if (length < 1) {
|
||||
this.text = getRandomChars();
|
||||
} else {
|
||||
this.text = getRandomChars(length);
|
||||
}
|
||||
|
||||
try {
|
||||
generateCaptcha();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a newly created object based on the passed
|
||||
* {@link String} parameter, consisting of capital english
|
||||
* letters. If the condition is not met, initializes a newly
|
||||
* created default object consisting of 8 capital english letters.
|
||||
*
|
||||
* @param text the text string with the value of the captcha,
|
||||
* length greater than 0.
|
||||
*/
|
||||
public SimpleCaptcha(String text) {
|
||||
if (text == null || text.equals("")) {
|
||||
this.text = getRandomChars();
|
||||
} else {
|
||||
this.text = text.toCharArray();
|
||||
}
|
||||
|
||||
try {
|
||||
generateCaptcha();
|
||||
} catch (IOException e) {
|
||||
this.text = getRandomChars();
|
||||
try {
|
||||
generateCaptcha();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the picture with captcha
|
||||
*
|
||||
* @return {@link BufferedImage}
|
||||
*/
|
||||
public BufferedImage getImagePng() {
|
||||
return imagePng;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text value of the captcha
|
||||
*
|
||||
* @return {@link String}
|
||||
*/
|
||||
public String getText() {
|
||||
return String.valueOf(text);
|
||||
}
|
||||
|
||||
//////// //////// //////// //////// //////// //////// //////// ////////
|
||||
|
||||
private char[] getRandomChars() {
|
||||
return getRandomChars(8);
|
||||
}
|
||||
|
||||
private char[] getRandomChars(int quantity) {
|
||||
|
||||
char[] randomString = new char[quantity];
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
int capitalLetter;
|
||||
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
capitalLetter = 65 + random.nextInt(26);
|
||||
randomString[i] = (char) capitalLetter;
|
||||
}
|
||||
|
||||
return randomString;
|
||||
}
|
||||
|
||||
private void generateCaptcha() throws IOException {
|
||||
int charsQuantity = this.text.length;
|
||||
BufferedImage[] images = new BufferedImage[charsQuantity];
|
||||
|
||||
for (int i = 0; i < charsQuantity; i++) {
|
||||
images[i] = ImageIO.read(SimpleCaptcha.class.getResourceAsStream("/pictures/" + this.text[i] + ".png"));
|
||||
if (i % 2 == 0) {
|
||||
images[i] = rotateImage(images[i], 25);
|
||||
} else {
|
||||
images[i] = rotateImage(images[i], -20);
|
||||
}
|
||||
}
|
||||
|
||||
int imageSize = 30;
|
||||
int rotatedImageSize = (int) Math.sqrt(imageSize * imageSize * 2);
|
||||
|
||||
BufferedImage captchaImg = new BufferedImage(rotatedImageSize * (charsQuantity - 1) / 10 * 6 + rotatedImageSize, rotatedImageSize, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics2d = captchaImg.createGraphics();
|
||||
graphics2d.setBackground(Color.WHITE);
|
||||
graphics2d.clearRect(0, 0, captchaImg.getWidth(), captchaImg.getHeight());
|
||||
for (int i = 0; i < charsQuantity; i++) {
|
||||
captchaImg.getGraphics().drawImage(images[i], rotatedImageSize * i / 10 * 6, 0, null);
|
||||
}
|
||||
graphics2d.dispose();
|
||||
this.imagePng = captchaImg;
|
||||
}
|
||||
|
||||
private BufferedImage rotateImage(BufferedImage buffImage, double angle) {
|
||||
|
||||
double radian = Math.toRadians(angle);
|
||||
double sin = Math.abs(Math.sin(radian));
|
||||
double cos = Math.abs(Math.cos(radian));
|
||||
|
||||
int width = buffImage.getWidth();
|
||||
int height = buffImage.getHeight();
|
||||
|
||||
int nWidth = (int) Math.floor((double) width * cos + (double) height * sin);
|
||||
int nHeight = (int) Math.floor((double) height * cos + (double) width * sin);
|
||||
|
||||
BufferedImage rotatedImage = new BufferedImage(nWidth, nHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
Graphics2D graphics = rotatedImage.createGraphics();
|
||||
|
||||
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
||||
graphics.translate((nWidth - width) / 2, (nHeight - height) / 2);
|
||||
graphics.rotate(radian, (double) (width / 2), (double) (height / 2));
|
||||
graphics.drawImage(buffImage, 0, 0,null);
|
||||
graphics.dispose();
|
||||
|
||||
return rotatedImage;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.ros.chatto.captcha;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
public class SimpleCaptchaBehavior implements CaptchaBehaviour {
|
||||
@Override
|
||||
public BufferedImage generateCaptcha() {
|
||||
// TODO Auto-generated method stub
|
||||
SimpleCaptcha simpleCaptcha = new SimpleCaptcha();
|
||||
return simpleCaptcha.getImagePng();
|
||||
}
|
||||
@Override
|
||||
public BufferedImage generateCaptcha(String captchaText) {
|
||||
// TODO Auto-generated method stub
|
||||
SimpleCaptcha simpleCaptcha = new SimpleCaptcha(captchaText);
|
||||
return simpleCaptcha.getImagePng();
|
||||
}
|
||||
|
||||
public String getRandomChars() {
|
||||
return getRandomChars(8);
|
||||
}
|
||||
|
||||
public String getRandomChars(int quantity)
|
||||
{
|
||||
char[] randomString = new char[quantity];
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
int capitalLetter;
|
||||
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
capitalLetter = 65 + random.nextInt(26);
|
||||
randomString[i] = (char) capitalLetter;
|
||||
}
|
||||
|
||||
return new String(randomString);
|
||||
}
|
||||
|
||||
}
|
26
chatto/src/main/java/org/ros/chatto/captcha/WebCaptcha.java
Normal file
@ -0,0 +1,26 @@
|
||||
package org.ros.chatto.captcha;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public class WebCaptcha {
|
||||
private final CaptchaBehaviour captchaBehaviour;
|
||||
|
||||
public BufferedImage generateCaptcha() {
|
||||
return captchaBehaviour.generateCaptcha();
|
||||
}
|
||||
|
||||
public BufferedImage generateCaptcha(String captchaText) {
|
||||
return captchaBehaviour.generateCaptcha(captchaText);
|
||||
}
|
||||
|
||||
public String getRandomChars() {
|
||||
return captchaBehaviour.getRandomChars();
|
||||
}
|
||||
|
||||
public String getRandomChars(int quantity) {
|
||||
return captchaBehaviour.getRandomChars(quantity);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.ros.chatto.captcha;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public class WebCaptchaBuilder {
|
||||
private CaptchaBehaviour captchaBehaviour;
|
||||
// public WebCaptchaBuilder(CaptchaBehaviour captchaBehaviour)
|
||||
// {
|
||||
// this.captchaBehaviour = captchaBehaviour;
|
||||
// }
|
||||
public WebCaptcha build()
|
||||
{
|
||||
return new WebCaptcha(captchaBehaviour);
|
||||
}
|
||||
}
|
@ -1,15 +1,31 @@
|
||||
package org.ros.chatto.controller;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.ros.chatto.dto.UserRegistrationDTO;
|
||||
import org.ros.chatto.service.CaptchaService;
|
||||
import org.ros.chatto.service.UserService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
@Controller
|
||||
@ -18,9 +34,23 @@ public class RegistrationController {
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private CaptchaService captchaService;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private final Map<Long, String> captchaMap = new ConcurrentHashMap<>();
|
||||
|
||||
@GetMapping("/registration")
|
||||
public String registrationForm(Model model) {
|
||||
model.addAttribute("userRegistrationDTO", new UserRegistrationDTO());
|
||||
UserRegistrationDTO userRegistrationDTO = new UserRegistrationDTO();
|
||||
String captchaText = captchaService.getRandomText();
|
||||
userRegistrationDTO.setCaptchaText(captchaText);
|
||||
logger.debug("captcha text = {}", captchaText);
|
||||
Long captchaID = ThreadLocalRandom.current().nextLong();
|
||||
userRegistrationDTO.setCaptchaID(captchaID);
|
||||
captchaMap.put(captchaID, captchaText);
|
||||
model.addAttribute("userRegistrationDTO", userRegistrationDTO);
|
||||
return "registration";
|
||||
}
|
||||
|
||||
@ -29,10 +59,32 @@ public class RegistrationController {
|
||||
@ModelAttribute("userRegistrationDTO") @Valid UserRegistrationDTO userRegistrationDTO,
|
||||
BindingResult bindingResult) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
System.out.println("Input has errors!");
|
||||
logger.warn("Registration input has errors!");
|
||||
return "registration";
|
||||
}
|
||||
userService.registerUser(userRegistrationDTO);
|
||||
logger.debug("Captcha text from user input = {}", userRegistrationDTO.getCaptchaInput());
|
||||
logger.debug("Captcha text from captcha map = {}", captchaMap.get(userRegistrationDTO.getCaptchaID()));
|
||||
if (userRegistrationDTO.getCaptchaInput().equals(captchaMap.get(userRegistrationDTO.getCaptchaID()))) {
|
||||
logger.info("Registration captcha equal success");
|
||||
} else {
|
||||
logger.warn("Registration captcha equal fail");
|
||||
}
|
||||
// userService.registerUser(userRegistrationDTO);
|
||||
return "user/home";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/img/{image_id}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public ResponseEntity<byte[]> getImage(@PathVariable("image_id") Long imageId) throws IOException {
|
||||
|
||||
final String captchaText = captchaMap.get(imageId);
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.IMAGE_PNG);
|
||||
BufferedImage captchaBufferedImage = captchaService.createCaptchaImage(captchaText);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(captchaBufferedImage, "png", baos);
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
|
||||
return new ResponseEntity<byte[]>(imageBytes, headers, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserRegistrationDTO {
|
||||
@Size(min = 4, max = 10, message = "Username must be between 4 and 10 characters")
|
||||
@NotBlank(message = "Username should not be blank")
|
||||
@ -16,20 +19,8 @@ public class UserRegistrationDTO {
|
||||
// @Pattern(regexp = "^.*(?=.{6,})(?=.*d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$", message = "Invalid password format")
|
||||
private String password;
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
private Long captchaID;
|
||||
private String captchaText;
|
||||
|
||||
private String captchaInput;
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package org.ros.chatto.service;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.ros.chatto.captcha.SimpleCaptchaBehavior;
|
||||
import org.ros.chatto.captcha.WebCaptcha;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CaptchaService {
|
||||
private final WebCaptcha webCaptcha;
|
||||
|
||||
public CaptchaService() {
|
||||
webCaptcha = WebCaptcha.builder().captchaBehaviour(new SimpleCaptchaBehavior()).build();
|
||||
}
|
||||
|
||||
public BufferedImage createCaptchaImage(String captchaText)
|
||||
{
|
||||
return webCaptcha.generateCaptcha(captchaText);
|
||||
}
|
||||
|
||||
public String getRandomText()
|
||||
{
|
||||
return webCaptcha.getRandomChars();
|
||||
}
|
||||
}
|
BIN
chatto/src/main/resources/pictures/A.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
chatto/src/main/resources/pictures/B.png
Normal file
After Width: | Height: | Size: 603 B |
BIN
chatto/src/main/resources/pictures/C.png
Normal file
After Width: | Height: | Size: 590 B |
BIN
chatto/src/main/resources/pictures/D.png
Normal file
After Width: | Height: | Size: 561 B |
BIN
chatto/src/main/resources/pictures/E.png
Normal file
After Width: | Height: | Size: 446 B |
BIN
chatto/src/main/resources/pictures/F.png
Normal file
After Width: | Height: | Size: 391 B |
BIN
chatto/src/main/resources/pictures/G.png
Normal file
After Width: | Height: | Size: 694 B |
BIN
chatto/src/main/resources/pictures/H.png
Normal file
After Width: | Height: | Size: 466 B |
BIN
chatto/src/main/resources/pictures/I.png
Normal file
After Width: | Height: | Size: 381 B |
BIN
chatto/src/main/resources/pictures/J.png
Normal file
After Width: | Height: | Size: 439 B |
BIN
chatto/src/main/resources/pictures/K.png
Normal file
After Width: | Height: | Size: 582 B |
BIN
chatto/src/main/resources/pictures/L.png
Normal file
After Width: | Height: | Size: 387 B |
BIN
chatto/src/main/resources/pictures/M.png
Normal file
After Width: | Height: | Size: 911 B |
BIN
chatto/src/main/resources/pictures/N.png
Normal file
After Width: | Height: | Size: 606 B |
BIN
chatto/src/main/resources/pictures/O.png
Normal file
After Width: | Height: | Size: 732 B |
BIN
chatto/src/main/resources/pictures/P.png
Normal file
After Width: | Height: | Size: 588 B |
BIN
chatto/src/main/resources/pictures/Q.png
Normal file
After Width: | Height: | Size: 830 B |
BIN
chatto/src/main/resources/pictures/R.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
chatto/src/main/resources/pictures/S.png
Normal file
After Width: | Height: | Size: 775 B |
BIN
chatto/src/main/resources/pictures/T.png
Normal file
After Width: | Height: | Size: 378 B |
BIN
chatto/src/main/resources/pictures/U.png
Normal file
After Width: | Height: | Size: 656 B |
BIN
chatto/src/main/resources/pictures/V.png
Normal file
After Width: | Height: | Size: 705 B |
BIN
chatto/src/main/resources/pictures/W.png
Normal file
After Width: | Height: | Size: 755 B |
BIN
chatto/src/main/resources/pictures/X.png
Normal file
After Width: | Height: | Size: 715 B |
BIN
chatto/src/main/resources/pictures/Y.png
Normal file
After Width: | Height: | Size: 655 B |
BIN
chatto/src/main/resources/pictures/Z.png
Normal file
After Width: | Height: | Size: 568 B |
@ -54,6 +54,15 @@
|
||||
<label for="password-repeat">Repeat password: </label>
|
||||
<input class="form-control" type="password" id="password-repeat" required>
|
||||
</div>
|
||||
<input type="hidden" th:value="${userRegistrationDTO.captchaID}" name="captchaID">
|
||||
<!-- <span th:text="${userRegistrationDTO.captchaText}"></span> -->
|
||||
<div class="form-group">
|
||||
<label for="captcha">Enter this captcha:
|
||||
|
||||
<img th:src="@{'/img/' + ${userRegistrationDTO.captchaID}}" />
|
||||
</label>
|
||||
<input class="form-control" type="text" id="captcha" th:field="*{captchaInput}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-secondary" type="submit" value="Submit">
|
||||
</div>
|
||||
|