рд╢реБрдн рджреЛрдкрд╣рд░, рд╣реЗрдмрд░ рдХреЗ рдкреНрд░рд┐рдп рдирд┐рд╡рд╛рд╕рд┐рдпреЛрдВ!
рдЬреИрд╕рд╛ рдХрд┐ рдирд╛рдо рд╕реЗ рд╣реА рд╕реНрдкрд╖реНрдЯ рд╣реИ, рдпрд╣ рд▓реЗрдЦ
рдХреЛрдЯрд▓рд┐рди + рд╕реНрдкреНрд░рд┐рдВрдЧ рдмреВрдЯ + рд╡реАрдпреВ.рдЬреЗрдПрд╕ рдкрд░ рдкрд╣рд▓реЗ рд╕реЗ рд▓рд┐рдЦреЗ рдЧрдП
рд╡реЗрдм рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдЕрддрд┐рд░рд┐рдХреНрдд рд╣реИ, рдЬреЛ рд╣рдореЗрдВ рднрд╡рд┐рд╖реНрдп рдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдХрдВрдХрд╛рд▓ рдХреЛ рдмреЗрд╣рддрд░ рдмрдирд╛рдиреЗ рдФрд░ рдЗрд╕рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдирд╛ рдЖрд╕рд╛рди рдмрдирд╛рддрд╛ рд╣реИред
рдХрд╣рд╛рдиреА рд╢реБрд░реВ рдХрд░рдиреЗ рд╕реЗ рдкрд╣рд▓реЗ, рдореИрдВ рдЙрди рд╕рднреА рдХреЛ рдзрдиреНрдпрд╡рд╛рдж рджреЗрддрд╛ рд╣реВрдВ рдЬрд┐рдиреНрд╣реЛрдВрдиреЗ рдкрд┐рдЫрд▓реЗ рд▓реЗрдЦ рдореЗрдВ рдЯрд┐рдкреНрдкрдгреА рдХреА рдереАред
рд╕рд╛рдордЧреНрд░реА
рд╕реАрдЖрдИ / рд╕реАрдбреА рд╕реЗрдЯрдЕрдк (рд╣рд░реЛрдХреВ)
рдПрдХ рдЙрджрд╛рд╣рд░рдг рдХреЗ рд░реВрдк рдореЗрдВ
рд╣рд░реЛрдХреВ рдХреНрд▓рд╛рдЙрдб PaS рдкреНрд▓реЗрдЯрдлреЙрд░реНрдо рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдирд┐рд░рдВрддрд░ рдПрдХреАрдХрд░рдг рдФрд░ рд╡рд┐рддрд░рдг рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдкрд░ рд╡рд┐рдЪрд╛рд░ рдХрд░реЗрдВред
рдкрд╣рд▓реА рдЪреАрдЬ рдЬреЛ рд╣рдореЗрдВ рдХрд░рдиреЗ рдХреА рдЬрд╝рд░реВрд░рдд рд╣реИ рд╡рд╣ рдЧрд┐рдЯрд╣рдм рдкрд░ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛрдб
рдбрд╛рд▓рддреА рд╣реИ ред рддрд╛рдХрд┐ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ рдХреБрдЫ рднреА рдХрдо рдирд╣реАрдВ рд╣реЛ, рдореИрдВ
.ignignore рдлрд╛рдЗрд▓ рдХреА рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рд╕рд╛рдордЧреНрд░реА рдХреА рд╕рд┐рдлрд╛рд░рд┐рд╢ рдХрд░рддрд╛ рд╣реВрдВ:
.gitignore*.class # Help # backend/*.md # Package Files # *.jar *.war *.ear # Eclipse # .settings .project .classpath .studio target # NetBeans # backend/nbproject/private/ backend/nbbuild/ backend/dist/ backend/nbdist/ backend/.nb-gradle/ backend/build/ # Apple # .DS_Store # Intellij # .idea *.iml *.log # logback logback.out.xml backend/src/main/resources/public/ backend/target backend/.mvn backend/mvnw frontend/dist/ frontend/node/ frontend/node_modules/ frontend/npm-debug.log frontend/target !.mvn/wrapper/maven-wrapper.jar
рдорд╣рддреНрд╡рдкреВрд░реНрдг: рд╣рд░реЛрдХреВ рдХреЗ рд╕рд╛рде рдХрд╛рдо рд╢реБрд░реВ рдХрд░рдиреЗ рд╕реЗ рдкрд╣рд▓реЗ, рд▓рд╛рдЗрди рдХреЗ рд╕рд╛рде рд░реВрдЯ рдирд┐рд░реНрджреЗрд╢рд┐рдХрд╛ рдореЗрдВ
Proffile (рдмрд┐рдирд╛ рдХрд┐рд╕реА рдПрдХреНрд╕рдЯреЗрдВрд╢рди рдХреЗ) рдирд╛рдордХ рдПрдХ рдлрд╝рд╛рдЗрд▓ рдЬреЛрдбрд╝реЗрдВ:
web: java -Dserver.port=$PORT -jar backend/target/backend-0.0.1-SNAPSHOT.jar
, рдЬрд╣рд╛рдВ
рдмреИрдХрдПрдВрдб- web: java -Dserver.port=$PORT -jar backend/target/backend-0.0.1-SNAPSHOT.jar
JAR рдлрд╝рд╛рдЗрд▓ рдХрд╛ рдирд╛рдо рд╣реИред рдФрд░
рдХрдорд┐рдЯрдореЗрдВрдЯ рдФрд░ рдкреБрд╢рдЕрдк рдЬрд░реВрд░ рдХрд░реЗрдВред
рдиреЛрдЯ: рдЖрдк Heroff рдкрд░ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдирд┐рд░реНрдорд╛рдг рдФрд░ рдкрд░рд┐рдирд┐рдпреЛрдЬрди рд╕рдордп рдХреЛ рдХрдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдореВрд▓ рдирд┐рд░реНрджреЗрд╢рд┐рдХрд╛ рдореЗрдВ travis.yaml рдлрд╝рд╛рдЗрд▓ рднреА рдЬреЛрдбрд╝ рд╕рдХрддреЗ рд╣реИрдВ:
travis.yaml language: java jdk: - oraclejdk8 script: mvn clean install jacoco:report coveralls:report cache: directories: - node_modules
рддреЛ:
рд╣реЗрд░реЛрдХреВ рдореЗрдВ # 1 рд░рдЬрд┐рд╕реНрдЯрд░ред
# 2 рдПрдХ рдирдпрд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдПрдВ:
рдПрдХ рдирдпрд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдПрдВ # 3 рд╣рд░реЛрдХреВ рдЖрдкрдХреЛ рдЕрддрд┐рд░рд┐рдХреНрдд рд╕рдВрд╕рд╛рдзрдиреЛрдВ рдХреЛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╕реЗ рдЬреЛрдбрд╝рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИ, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдкреЛрд╕реНрдЯрд░реЗрдХреНрдпреВрдПрд▓ рдбреЗрдЯрд╛рдмреЗрд╕ред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдпрд╣ рдХрд░реЗрдВ:
рдПрдкреНрд▓рд┐рдХреЗрд╢рди -> рд╕рдВрд╕рд╛рдзрди -> рдРрдб-рдСрди -> рд╣рд░реЛрдХреВ рдкреЛрд╕реНрдЯрдЧреНрд░реИрдЬреЗрд╕ :
рд╣рд░реЛрдХреВ рдкреЛрд╕реНрдЯрдЧреНрд░реЗрдЯ рдХрд░рддрд╛ рд╣реИ # 4 рдПрдХ рдпреЛрдЬрдирд╛ рдЪреБрдиреЗрдВ:
рдпреЛрдЬрдирд╛ рдЪрдпрди # 5 рдЕрдм рдЖрдк рдЬреБрдбрд╝реЗ рд╣реБрдП рд╕рдВрд╕рд╛рдзрди рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ:
рдЬреБрдбрд╝рд╛ рд╣реБрдЖ рд╕рдВрд╕рд╛рдзрди # 6 рдХреНрд░реЗрдбреЗрдВрд╢рд┐рдпрд▓реНрд╕ рдХреЛ рджреЗрдЦреЗрдВ, рдЙрдиреНрд╣реЗрдВ рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░ рд╕реНрдерд╛рдкрд┐рдд рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдЧреА:
рд╕реЗрдЯрд┐рдВрдЧреНрд╕ -> рдХреНрд░реЗрдбреЗрдВрд╢рд┐рдпрд▓ рджреЗрдЦреЗрдВ :
рд╕рд╛рдЦ рджреЗрдЦреЗрдВ # 7 рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░ рд╕реЗрдЯ рдХрд░реЗрдВ:
рдЕрдиреБрдкреНрд░рдпреЛрдЧ -> рд╕реЗрдЯрд┐рдВрдЧреНрд╕ -> рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рд╡рд╛рд░реНрд╕ рдкреНрд░рдХрдЯ рдХрд░реЗрдВ :
рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░ # 8 рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рдкреНрд░рд╛рд░реВрдк рдореЗрдВ рдХрдиреЗрдХреНрд╢рди рдХреЗ рд▓рд┐рдП рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░ рд╕реЗрдЯ рдХрд░реЗрдВ:
SPRING_DATASOURCE_URL = jdbc:postgresql://<i>hostname:port</i>/<i>db_name</i> SPRING_DATASOURCE_USERNAME = <i>username</i> SPRING_DATASOURCE_PASSWORD = <i>password</i>
рдпрд╣ рдХреИрд╕рд╛ рджрд┐рдЦрддрд╛ рд╣реИ # 9 рдирдП рдбреЗрдЯрд╛рдмреЗрд╕ рдореЗрдВ рд╕рднреА рдЖрд╡рд╢реНрдпрдХ рдЯреЗрдмрд▓ рдмрдирд╛рдПрдВред
# 10 рдХреНрд░рдорд╢рдГ Application.properties рдлрд╝рд╛рдЗрд▓, рдХреБрдЫ рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрдирд╛ рдЪрд╛рд╣рд┐рдП:
application.properties spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} spring.jpa.generate-ddl=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
# 11 рдПрдХ
рдирдИ рдкрд╛рдЗрдкрд▓рд╛рдЗрди рдмрдирд╛рдПрдБ -
рдирдИ рдкрд╛рдЗрдкрд▓рд╛рдЗрди рдмрдирд╛рдПрдБ :
рдирдИ рдкрд╛рдЗрдкрд▓рд╛рдЗрди рдмрдирд╛рдПрдВ # 12 рдкрд░рд┐рдирд┐рдпреЛрдЬрди рд╡рд┐рдзрд┐ -
GitHub (
GitHub рд╕реЗ рдХрдиреЗрдХреНрдЯ рдХрд░реЗрдВ рдкрд░ рдХреНрд▓рд┐рдХ
рдХрд░реЗрдВ рдФрд░ рдирдИ рд╡рд┐рдВрдбреЛ рдореЗрдВ рдирд┐рд░реНрджреЗрд╢реЛрдВ рдХрд╛ рдкрд╛рд▓рди рдХрд░реЗрдВ)ред
# 13 рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдбрд┐рдкреНрд▓реЙрдпрдЬрд╝ рд╕рдХреНрд╖рдо рдХрд░реЗрдВ :
рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдбрд┐рдкреНрд▓реЙрдпрдЬрд╝ рд╕рдХреНрд╖рдо рдХрд░реЗрдВ # 14 рдореИрдиреБрдЕрд▓ рддреИрдирд╛рддреА - рдкрд╣рд▓реА рддреИрдирд╛рддреА рдХреЗ рд▓рд┐рдП
рддреИрдирд╛рддреА рд╢рд╛рдЦрд╛ рдкрд░ рдХреНрд▓рд┐рдХ
рдХрд░реЗрдВ ред рдмреНрд░рд╛рдЙрдЬрд░ рдореЗрдВ рд░рд╛рдЗрдЯ рдЖрдкрдХреЛ рдХрдорд╛рдВрдб рд▓рд╛рдЗрди рдХрд╛ рдЖрдЙрдЯрдкреБрдЯ рджрд┐рдЦрд╛рдИ рджреЗрдЧрд╛ред
рдореИрдиреБрдЕрд▓ рдХреА рддреИрдирд╛рддреА # 15 рддреИрдирд╛рдд рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХреЛ рдЦреЛрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рдлрд▓ рдирд┐рд░реНрдорд╛рдг рдХреЗ рдмрд╛рдж
рджреЗрдЦреЗрдВ рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░реЗрдВ:
рдмреЙрдЯ рд╕рдВрд░рдХреНрд╖рдг (reCAPTCHA)
рд╣рдорд╛рд░реЗ рдЖрд╡реЗрджрди рдореЗрдВ reCAPTCHA рд╕рддреНрдпрд╛рдкрди рд╕рдХреНрд╖рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкрд╣рд▓рд╛ рдХрджрдо
Google рд╡реНрдпрд╡рд╕реНрдерд╛рдкрдХ рдкреИрдирд▓ рдореЗрдВ рдПрдХ рдирдпрд╛ reCAPTCH рдмрдирд╛рдирд╛ рд╣реИред рд╡рд╣рд╛рдВ рд╣рдо рдПрдХ рдирдИ рд╕рд╛рдЗрдЯ рдмрдирд╛рддреЗ рд╣реИрдВ (рдирдИ рд╕рд╛рдЗрдЯ рдЬреЛрдбрд╝реЗрдВ / рдмрдирд╛рдПрдВ) рдФрд░ рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рд╕реЗрдЯ рдХрд░реЗрдВ:
ReCAPTCHA рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдбреЛрдореЗрди рдЕрдиреБрднрд╛рдЧ рдореЗрдВ, рдЖрдкрдХреЛ рдЙрд╕ рдкрддреЗ рдХреЗ рдЕрд▓рд╛рд╡рд╛ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП рдЬрд╣рд╛рдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓рд╛рдЗрд╡ рд╣реЛрдЧрд╛, рдЖрдкрдХреЛ
localhost
рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП, рддрд╛рдХрд┐ рдбрд┐рдмрдЧрд┐рдВрдЧ рдХреЗ рджреМрд░рд╛рди рдЖрдк рдЕрдкрдиреЗ рдЖрд╡реЗрджрди рдореЗрдВ рд▓реЙрдЧ рдЗрди рдХрд░рдиреЗ рдореЗрдВ рдЕрд╕рдорд░реНрдерддрд╛ рдХреЗ рд░реВрдк рдореЗрдВ рдкрд░реЗрд╢рд╛рдирд┐рдпреЛрдВ рд╕реЗ рдмрдЪреЗрдВред
рдмреИрдХрдПрдВрдбрд╕рд╛рдЗрдЯ рдХреБрдВрдЬреА рдФрд░
рдЧреБрдкреНрдд рдХреБрдВрдЬреА рд╕рд╣реЗрдЬреЗрдВ ...
рд╕рд╛рдЗрдЯ рдХреБрдВрдЬреА / рдЧреБрдкреНрдд рдХреБрдВрдЬреА ... рдлрд┐рд░ рдЙрдиреНрд╣реЗрдВ рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░, рдФрд░ рдЪрд░ рдирд╛рдореЛрдВ рдХреЛ рд╕реМрдВрдкрдиреЗ рдХреЗ рд▓рд┐рдП, рдмрджрд▓реЗ рдореЗрдВ, рдирдП
application.properties рдЧреБрдгреЛрдВ рдХреЛ рдЕрд╕рд╛рдЗрди рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП:
google.recaptcha.key.site=${GOOGLE_RECAPTCHA_KEY_SITE} google.recaptcha.key.secret=${GOOGLE_RECAPTCHA_KEY_SECRET}
Google рдХреА рдУрд░ рд╕реЗ reCAPTCHA рдЯреЛрдХрди рдХреЗ рд╕рддреНрдпрд╛рдкрди рдХреЗ рд▓рд┐рдП
pom.xml рдореЗрдВ рдПрдХ рдирдИ рдирд┐рд░реНрднрд░рддрд╛ рдЬреЛрдбрд╝реЗрдВ, рдЬреЛ рдХреНрд▓рд╛рдЗрдВрдЯ рд╣рдореЗрдВ рднреЗрдЬреЗрдЧрд╛:
<dependency> <groupId>com.mashape.unirest</groupId> <artifactId>unirest-java</artifactId> <version>1.4.9</version> </dependency>
рдЕрдм рдЙрди рд╕рдВрд╕реНрдерд╛рдУрдВ рдХреЛ рдЕрджреНрдпрддрди рдХрд░рдиреЗ рдХрд╛ рд╕рдордп рд╣реИ, рдЬрд┐рдирдХрд╛ рдЙрдкрдпреЛрдЧ рд╣рдо рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛рдУрдВ рдХреЛ рдПрдХ рд╣реА reCAPTCHA рдЯреЛрдХрди рдХреЗ рд▓рд┐рдП рдПрдХ рд╕реНрдЯреНрд░рд┐рдВрдЧ рдлрд╝реАрд▓реНрдб рдЬреЛрдбрд╝рдХрд░ рдЕрдзрд┐рдХреГрдд рдФрд░ рдкрдВрдЬреАрдХреГрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд░рддреЗ рд╣реИрдВ:
LoginUser.kt import com.fasterxml.jackson.annotation.JsonProperty import java.io.Serializable class LoginUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("password") var password: String? = null @JsonProperty("recapctha_token") var recaptchaToken: String? = null constructor() {} constructor(username: String, password: String, recaptchaToken: String) { this.username = username this.password = password this.recaptchaToken = recaptchaToken } companion object { private const val serialVersionUID = -1764970284520387975L } }
NewUser.kt import com.fasterxml.jackson.annotation.JsonProperty import java.io.Serializable class NewUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("firstName") var firstName: String? = null @JsonProperty("lastName") var lastName: String? = null @JsonProperty("email") var email: String? = null @JsonProperty("password") var password: String? = null @JsonProperty("recapctha_token") var recaptchaToken: String? = null constructor() {} constructor(username: String, firstName: String, lastName: String, email: String, password: String, recaptchaToken: String) { this.username = username this.firstName = firstName this.lastName = lastName this.email = email this.password = password this.recaptchaToken = recaptchaToken } companion object { private const val serialVersionUID = -1764970284520387975L } }
рдПрдХ рдЫреЛрдЯреА рд╕реЗрд╡рд╛ рдЬреЛрдбрд╝реЗрдВ рдЬреЛ reCAPTCHA рдЯреЛрдХрди рдХреЛ рдПрдХ рд╡рд┐рд╢реЗрд╖ Google рд╕реЗрд╡рд╛ рдореЗрдВ рдкреНрд░рд╕рд╛рд░рд┐рдд рдХрд░реЗрдЧреА рдФрд░ рдЬрд╡рд╛рдм рдореЗрдВ рд╕реВрдЪрд┐рдд рдХрд░реЗрдЧреА рдХрд┐ рдЯреЛрдХрди рдкрд╛рд░рд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ рдпрд╛ рдирд╣реАрдВ:
ReCaptchaService.kt import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.web.client.RestOperations import org.springframework.beans.factory.annotation.Autowired import com.mashape.unirest.http.HttpResponse import com.mashape.unirest.http.JsonNode import com.mashape.unirest.http.Unirest @Service("captchaService") class ReCaptchaService { val BASE_VERIFY_URL: String = "https://www.google.com/recaptcha/api/siteverify" @Autowired private val restTemplate: RestOperations? = null @Value("\${google.recaptcha.key.site}") lateinit var keySite: String @Value("\${google.recaptcha.key.secret}") lateinit var keySecret: String fun validateCaptcha(token: String): Boolean { val url: String = String.format(BASE_VERIFY_URL + "?secret=%s&response=%s", keySecret, token) val jsonResponse: HttpResponse<JsonNode> = Unirest.get(url) .header("accept", "application/json").queryString("apiKey", "123") .asJson() return (jsonResponse.getStatus() == 200) } }
рдЗрд╕ рд╕реЗрд╡рд╛ рдХрд╛ рдЙрдкрдпреЛрдЧ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдкрдВрдЬреАрдХрд░рдг рдФрд░ рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдирд┐рдпрдВрддреНрд░рдХ рдореЗрдВ рдХрд┐рдпрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП:
AuthController.kt import com.kotlinspringvue.backend.service.ReCaptchaService тАж @Autowired lateinit var captchaService: ReCaptchaService тАж if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else [if]... тАж if (!captchaService.validateCaptcha(newUser.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else...
рджреГрд╢реНрдпрдкрдЯрд▓рдкрд╣рд▓рд╛ рдХрджрдо
reCAPTHA
рдкреИрдХреЗрдЬ рдХреЛ рд╕реНрдерд╛рдкрд┐рдд рдФрд░ рд╕рд╣реЗрдЬрдирд╛ рд╣реИ:
$ npm install --save vue-recaptcha
рдлрд┐рд░
index.html рдореЗрдВ рд╕реНрдХреНрд░рд┐рдкреНрдЯ рд╕реЗ рдХрдиреЗрдХреНрдЯ рдХрд░реЗрдВ:
<script src="https://www.google.com/recaptcha/api.js onload=vueRecaptchaApiLoaded&render=explicit" async defer></script>
рдкреГрд╖реНрда рдкрд░ рдХрд┐рд╕реА рднреА рд░рд┐рдХреНрдд рд╕реНрдерд╛рди рдкрд░ рдХреЛрдИ рдХреИрдкреНрдЪрд╛ рдЬреЛрдбрд╝реЗрдВ:
<vue-recaptcha ref="recaptcha" size="invisible" :sitekey="sitekey" @verify="onCapthcaVerified" @expired="onCaptchaExpired" />
рдФрд░ рд▓рдХреНрд╖реНрдп рдХрд╛рд░реНрд░рд╡рд╛рдИ (рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдпрд╛ рдкрдВрдЬреАрдХрд░рдг) рдХреЗ рд▓рд┐рдП рдмрдЯрди рдЕрдм рдкрд╣рд▓реЗ рд╕рддреНрдпрд╛рдкрди рд╡рд┐рдзрд┐ рдХреЛ рдмреБрд▓рд╛рдПрдЧрд╛:
<b-button v-on:click="validateCaptcha" variant="primary">Login</b-button>
рдШрдЯрдХреЛрдВ рдХреЗ рд▓рд┐рдП рдирд┐рд░реНрднрд░рддрд╛ рдЬреЛрдбрд╝реЗрдВ:
import VueRecaptcha from 'vue-recaptcha'
рдирд┐рд░реНрдпрд╛рдд рдбрд┐рдлрд╝реЙрд▓реНрдЯ рд╕рдВрдкрд╛рджрд┐рдд рдХрд░реЗрдВ:
components: { VueRecaptcha }, тАж data() { тАж siteKey: <i> </i> тАж }
рдФрд░ рдирдИ рд╡рд┐рдзрд┐рдпрд╛рдБ рдЬреЛрдбрд╝реЗрдВ:
validateCaptcha()
- рдЬрд┐рд╕реЗ рдмрдЯрди рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░рдХреЗ рдмреБрд▓рд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИonCapthcaVerified(recaptchaToken) onCaptchaExpired()
- рдЬреЛ рд╕реНрд╡рдпрдВ рдХреИрдкреНрдЪрд╛ рдХреЙрд▓ рдХрд░рддрд╛ рд╣реИ
рдирдП рддрд░реАрдХреЗ validateCaptcha() { this.$refs.recaptcha.execute() }, onCapthcaVerified(recaptchaToken) { AXIOS.post(`/auth/signin`, {'username': this.$data.username, 'password': this.$data.password, 'recapctha_token': recaptchaToken}) .then(response => { this.$store.dispatch('login', {'token': response.data.accessToken, 'roles': response.data.authorities, 'username': response.data.username}); this.$router.push('/home') }, error => { this.showAlert(error.response.data.message); }) .catch(e => { console.log(e); this.showAlert('Server error. Please, report this error website owners'); }) }, onCaptchaExpired() { this.$refs.recaptcha.reset() }
рдИрдореЗрд▓ рднреЗрдЬрдирд╛
рдПрдХ рд╕рд╛рд░реНрд╡рдЬрдирд┐рдХ рдореЗрд▓ рд╕рд░реНрд╡рд░, рдЬреИрд╕реЗ Google рдпрд╛ Mail.ru рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╣рдорд╛рд░реЗ рдЖрд╡реЗрджрди рдкрддреНрд░ рднреЗрдЬрдиреЗ рдХреА рд╕рдВрднрд╛рд╡рдирд╛ рдкрд░ рд╡рд┐рдЪрд╛рд░ рдХрд░реЗрдВред
рдкрд╣рд▓рд╛ рдЪрд░рдг, рдХреНрд░рдорд╢рдГ, рдЪрдпрдирд┐рдд рдореЗрд▓ рд╕рд░реНрд╡рд░ рдкрд░ рдПрдХ рдЦрд╛рддрд╛ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП рд╣реЛрдЧрд╛, рдЕрдЧрд░ рдпрд╣ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдирд╣реАрдВ рд╣реИред
рджреВрд╕рд░рд╛ рдЪрд░рдг рд╣рдореЗрдВ
pom.xml рдореЗрдВ рдирд┐рдореНрди рдирд┐рд░реНрднрд░рддрд╛рдПрдБ рдЬреЛрдбрд╝рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ:
рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency>
рдЖрдкрдХреЛ
application.properties рдореЗрдВ рдирдП рдЧреБрдг рдЬреЛрдбрд╝рдиреЗ рдХреА рднреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ:
SMTP рдЧреБрдг spring.mail.host=${SMTP_MAIL_HOST} spring.mail.port=${SMTP_MAIL_PORT} spring.mail.username=${SMTP_MAIL_USERNAME} spring.mail.password=${SMTP_MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.properties.mail.smtp.connectiontimeout=5000 spring.mail.properties.mail.smtp.timeout=5000 spring.mail.properties.mail.smtp.writetimeout=5000
рдЖрдк рдпрд╣рд╛рдВ SMTP рд╕реЗрдЯрд┐рдВрдЧ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ:
Google рдФрд░
Mail.ruрдПрдХ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдмрдирд╛рдПрдБ рдЬрд╣рд╛рдБ рд╣рдо рдХрдИ рддрд░реАрдХреЗ рдШреЛрд╖рд┐рдд рдХрд░рддреЗ рд╣реИрдВ:
- рдПрдХ рдирд┐рдпрдорд┐рдд рдкрд╛рда рд╕рдВрджреЗрд╢ рднреЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП
- HTML рдИрдореЗрд▓ рднреЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП
- рдЯреЗрдореНрдкрд▓реЗрдЯ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдПрдХ рдИрдореЗрд▓ рднреЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП
EmailService.kt package com.kotlinspringvue.backend.email import org.springframework.mail.SimpleMailMessage internal interface EmailService { fun sendSimpleMessage(to: String, subject: String, text: String) fun sendSimpleMessageUsingTemplate(to: String, subject: String, template: String, params:MutableMap<String, Any>) fun sendHtmlMessage(to: String, subject: String, htmlMsg: String) }
рдЕрдм рдЖрдЗрдП рдЗрд╕ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдХрд╛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдмрдирд╛рдПрдВ - рдИрдореЗрд▓ рднреЗрдЬрдиреЗ рдХреА рд╕реЗрд╡рд╛:
EmailServiceImpl.kt package com.kotlinspringvue.backend.email import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.core.io.FileSystemResource import org.springframework.mail.MailException import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.MimeMessageHelper import org.springframework.stereotype.Component import org.thymeleaf.spring5.SpringTemplateEngine import org.thymeleaf.context.Context import java.io.File import javax.mail.MessagingException import javax.mail.internet.MimeMessage import org.apache.commons.io.IOUtils import org.springframework.core.env.Environment @Component class EmailServiceImpl : EmailService { @Value("\${spring.mail.username}") lateinit var sender: String @Autowired lateinit var environment: Environment @Autowired var emailSender: JavaMailSender? = null @Autowired lateinit var templateEngine: SpringTemplateEngine override fun sendSimpleMessage(to: String, subject: String, text: String) { try { val message = SimpleMailMessage() message.setTo(to) message.setFrom(sender) message.setSubject(subject) message.setText(text) emailSender!!.send(message) } catch (exception: MailException) { exception.printStackTrace() } } override fun sendSimpleMessageUsingTemplate(to: String, subject: String, template: String, params:MutableMap<String, Any>) { val message = emailSender!!.createMimeMessage() val helper = MimeMessageHelper(message, true, "utf-8") var context: Context = Context() context.setVariables(params) val html: String = templateEngine.process(template, context) helper.setTo(to) helper.setFrom(sender) helper.setText(html, true) helper.setSubject(subject) emailSender!!.send(message) } override fun sendHtmlMessage(to: String, subject: String, htmlMsg: String) { try { val message = emailSender!!.createMimeMessage() message.setContent(htmlMsg, "text/html") val helper = MimeMessageHelper(message, false, "utf-8") helper.setTo(to) helper.setFrom(sender) helper.setSubject(subject) emailSender!!.send(message) } catch (exception: MailException) { exception.printStackTrace() } } }
- рд╣рдо рдИрдореЗрд▓ рднреЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП рд╕реНрдкреНрд░рд┐рдВрдЧ рдХреЗ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд┐рдП рдЧрдП JavaMailSender рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реИрдВ
- рдирд┐рдпрдорд┐рдд рдкрддреНрд░ рднреЗрдЬрдирд╛ рдмреЗрд╣рдж рд╕рд░рд▓ рд╣реИ - рдЖрдкрдХреЛ рдХреЗрд╡рд▓ рдкрддреНрд░ рдХреЗ рдореБрдЦреНрдп рднрд╛рдЧ рдореЗрдВ рдкрд╛рда рдЬреЛрдбрд╝рдиреЗ рдФрд░ рднреЗрдЬрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ
- HTML рдИрдореЗрд▓ рдХреЛ Mime рдкреНрд░рдХрд╛рд░ рдХреЗ рд╕рдВрджреЗрд╢реЛрдВ рдФрд░ рдЙрдирдХреА рд╕рд╛рдордЧреНрд░реА рдХреЛ
text/html
рд░реВрдк рдореЗрдВ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ - HTML рд╕рдВрджреЗрд╢ рдЯреЗрдореНрдкрд▓реЗрдЯ рдХреЛ рд╕рдВрд╕рд╛рдзрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдо рд╕реНрдкреНрд░рд┐рдВрдЧ рдЯреЗрдореНрдкрд▓реЗрдЯ рдЗрдВрдЬрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реИрдВ
рдЖрдЗрдП рдЗрд╕реЗ
src / main / resource / templates / рдореЗрдВ рд░рдЦрдХрд░
Thymeleaf рдлреНрд░реЗрдорд╡рд░реНрдХ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд▓рд┐рдЦрдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рд╕рд░рд▓ рдЯреЗрдореНрдкрд▓реЗрдЯ рдмрдирд╛рдПрдБ:
emailTemplate.html <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Hello</title> </head> <body style="font-family: Arial, Helvetica, sans-serif;"> <h3>Hello!</h3> <div style="margin-top: 20px; margin-bottom: 30px; margin-left: 20px;"> <p>Hello, dear: <b><span th:text="${addresseeName}"></span></b></p> </div> <div> <img th:src="${signatureImage}" width="200px;"/> </div> </body> </html>
рдЯреЗрдореНрдкрд▓реЗрдЯ рдХреЗ рдмрджрд▓рддреЗ рддрддреНрд╡ (рд╣рдорд╛рд░реЗ рдорд╛рдорд▓реЗ рдореЗрдВ, рдкреНрд░рд╛рдкреНрддрдХрд░реНрддрд╛ рдХрд╛ рдирд╛рдо рдФрд░ рд╣рд╕реНрддрд╛рдХреНрд╖рд░ рдХреЗ рд▓рд┐рдП рдЪрд┐рддреНрд░ рдХрд╛ рдорд╛рд░реНрдЧ) рдкреНрд▓реЗрд╕рд╣реЛрд▓реНрдбрд░реНрд╕ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдШреЛрд╖рд┐рдд рдХрд┐рдП рдЬрд╛рддреЗ рд╣реИрдВред
рдЕрдм рдПрдХ рдирд┐рдпрдВрддреНрд░рдХ рдмрдирд╛рдПрдВ рдпрд╛ рдЕрдкрдбреЗрдЯ рдХрд░реЗрдВ рдЬреЛ рдкрддреНрд░ рднреЗрдЬреЗрдЧрд╛:
BackendController.kt import com.kotlinspringvue.backend.email.EmailServiceImpl import com.kotlinspringvue.backend.web.response.ResponseMessage import org.springframework.beans.factory.annotation.Value import org.springframework.http.ResponseEntity import org.springframework.http.HttpStatus тАж @Autowired lateinit var emailService: EmailService @Value("\${spring.mail.username}") lateinit var addressee: String тАж @GetMapping("/sendSimpleEmail") @PreAuthorize("hasRole('USER')") fun sendSimpleEmail(): ResponseEntity<*> { try { emailService.sendSimpleMessage(addressee, "Simple Email", "Hello! This is simple email") } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } @GetMapping("/sendTemplateEmail") @PreAuthorize("hasRole('USER')") fun sendTemplateEmail(): ResponseEntity<*> { try { var params:MutableMap<String, Any> = mutableMapOf() params["addresseeName"] = addressee params["signatureImage"] = "https://coderlook.com/wp-content/uploads/2019/07/spring-by-pivotal.png" emailService.sendSimpleMessageUsingTemplate(addressee, "Template Email", "emailTemplate", params) } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } @GetMapping("/sendHtmlEmail") @PreAuthorize("hasRole('USER')") fun sendHtmlEmail(): ResponseEntity<*> { try { emailService.sendHtmlMessage(addressee, "HTML Email", "<h1>Hello!</h1><p>This is HTML email</p>") } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) }
рдиреЛрдЯ: рдпрд╣ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐ рд╕рдм рдХреБрдЫ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ, рдкрд╣рд▓реЗ рд╣рдо рдЦреБрдж рдХреЛ рдкрддреНрд░ рднреЗрдЬреЗрдВрдЧреЗред
рдпрд╣ рднреА рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐ рд╕рдм рдХреБрдЫ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ, рд╣рдо рдПрдХ рдорд╛рдореВрд▓реА рд╡реЗрдм рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВ рдЬреЛ рдмрд╕ рд╡реЗрдм рд╕реЗрд╡рд╛ рд╡рд┐рдзрд┐рдпреЛрдВ рдХреЛ рдЦреАрдВрдЪреЗрдЧрд╛:
Email.vue <template> <div id="email"> <b-button v-on:click="sendSimpleMessage" variant="primary">Simple Email</b-button><br/> <b-button v-on:click="sendEmailUsingTemplate" variant="primary">Template Email</b-button><br/> <b-button v-on:click="sendHTMLEmail" variant="primary">HTML Email</b-button><br/> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'EmailPage', data() { return { counter: 0, username: '', header: {'Authorization': 'Bearer ' + this.$store.getters.getToken} } }, methods: { sendSimpleMessage() { AXIOS.get('/sendSimpleEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK"); }) .catch(error => { console.log('ERROR: ' + error.response.data); }) }, sendEmailUsingTemplate() { AXIOS.get('/sendTemplateEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK") }) .catch(error => { console.log('ERROR: ' + error.response.data); }) }, sendHTMLEmail() { AXIOS.get('/sendHtmlEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK") }) .catch(error => { console.log('ERROR: ' + error.response.data); }) } } } </script> <style> #email { margin-left: 38%; margin-top: 50px; } button { width: 150px; } </style>
рдиреЛрдЯ: router.js
рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдордд рднреВрд▓рдирд╛ред рдпрджрд┐ рдЖрдк рдПрдХ рдирдпрд╛ рдШрдЯрдХ рдмрдирд╛ рд░рд╣реЗ рд╣реИрдВ, рддреЛ
App.vue
рдиреЗрд╡рд┐рдЧреЗрд╢рди
App.vue
рдореЗрдВ рд▓рд┐рдВрдХ рдЬреЛрдбрд╝реЗрдВред
рдЧреНрд░реЗрдбрд┐рдВрдЧ рдорд╛рдЗрдЧреНрд░реЗрд╢рди
рдореИрдВ рддреБрд░рдВрдд рд╕реНрдкрд╖реНрдЯ рдХрд░ рджреВрдВрдЧрд╛: рдХреНрдпрд╛ рдЗрд╕ рдЖрдЗрдЯрдо рдХреЛ рдПрдХ рд╕реБрдзрд╛рд░ рдорд╛рдирд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП, рд╣рд░ рдХрд┐рд╕реА рдХреЛ рдЕрдкрдиреА рдкрд░рд┐рдпреЛрдЬрдирд╛ рдХреЗ рд▓рд┐рдП рдирд┐рд░реНрдгрдп рд▓реЗрдирд╛ рдЪрд╛рд╣рд┐рдПред рд╣рдо рд╕рд┐рд░реНрдл рдпрд╣ рджреЗрдЦрддреЗ рд╣реИрдВ рдХрд┐ рдпрд╣ рдХреИрд╕реЗ рдХрд░рдирд╛ рд╣реИред
рд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рдЖрдк
5 рдорд┐рдирдЯ рдХреЗ рдирд┐рд░реНрджреЗрд╢реЛрдВ рдХреЗ
рддрд╣рдд рдорд╛рд╡реЗрди рд╕реЗ рдЧреНрд░реИрдбрд▓ рдореЗрдВ рдореВрд╡рд┐рдВрдЧ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рд▓реЗрдХрд┐рди рдкрд░рд┐рдгрд╛рдо рдЙрдореНрдореАрджреЛрдВ рдкрд░ рдЦрд░рд╛
рдЙрддрд░рдиреЗ рдХреА рд╕рдВрднрд╛рд╡рдирд╛ рдирд╣реАрдВ рд╣реИред рдореИрдВ рдЕрднреА рднреА рдореИрдиреНрдпреБрдЕрд▓ рд░реВрдк рд╕реЗ рдорд╛рдЗрдЧреНрд░реЗрд╢рди рдХрд░рдиреЗ рдХреА рд╕рд▓рд╛рд╣ рджреВрдВрдЧрд╛, рдЗрд╕рдореЗрдВ рдЕрдзрд┐рдХ рд╕рдордп рдирд╣реАрдВ рд▓рдЧреЗрдЧрд╛ред
рдкрд╣рд▓реА рдЪреАрдЬ рдЬреЛ рд╣рдореЗрдВ рдХрд░рдиреА рд╣реИ, рд╡рд╣ рд╣реИ
рдЧреНрд░реЗрдбрд▓ ред
рдлрд┐рд░ рд╣рдореЗрдВ рджреЛрдиреЛрдВ рдЙрдкрдкреНрд░рдХрд╛рд░реЛрдВ рдХреЗ рд▓рд┐рдП рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рдкреНрд░рдХреНрд░рд┐рдпрд╛ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ -
backend
рдФрд░
fronted
:
# 1 рдореЗрд╡реЗрди рдлрд╛рдЗрд▓реЗрдВ рд╣рдЯрд╛рдПрдВ -
pom.xml ,
.mvn ред
# 2 рд╕рдмрдкреНрд░реЛрдЬреЗрдХреНрдЯ рдбрд╛рдпрд░реЗрдХреНрдЯрд░реА рдореЗрдВ, рдЧреНрд░реЗрдб рдЗрдирд┐рдЯ рдЪрд▓рд╛рдПрдВ рдФрд░ рд╕рд╡рд╛рд▓реЛрдВ рдХреЗ рдЬрд╡рд╛рдм рджреЗрдВ:
- рдЙрддреНрдкрдиреНрди рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХрд╛ рдкреНрд░рдХрд╛рд░ рдЪреБрдиреЗрдВ: рдореВрд▓
- рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рднрд╛рд╖рд╛ рдЪреБрдиреЗрдВ: рдХреЛрдЯрд▓рд┐рди
- рдмрд┐рд▓реНрдб рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдХрд╛ рдЪрдпрди рдХрд░реЗрдВ рдбреАрдПрд╕рдПрд▓: рдХреЛрдЯрд▓рд┐рди (рдЪреВрдВрдХрд┐ рд╣рдо рдХреЛрдЯрд▓рд┐рди рдореЗрдВ рдПрдХ рдкрд░рд┐рдпреЛрдЬрдирд╛ рд▓рд┐рдЦ тАЛтАЛрд░рд╣реЗ рд╣реИрдВ)
# 3 settings.gradle.kts рд╣рдЯрд╛рдПрдВ - рдЗрд╕ рдлрд╛рдЗрд▓ рдХреЛ рдХреЗрд╡рд▓ рд░реВрдЯ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХреЗ рд▓рд┐рдП рдЖрд╡рд╢реНрдпрдХ рд╣реИред
# 4 рд░рди
gradle wrapper
ред
рдЕрдм рд╣рдо рдЕрдкрдиреА рдореВрд▓ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдХреА рдУрд░ рдореБрдбрд╝рддреЗ рд╣реИрдВред рдЗрд╕рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЙрдк-рдкрд░рд┐рдпреЛрдЬрдирд╛рдУрдВ рдХреЗ рд▓рд┐рдП рдКрдкрд░ рд╡рд░реНрдгрд┐рдд рдЪрд░рдгреЛрдВ 1, 2 рдФрд░ 4 рдХрд╛ рдкрд╛рд▓рди рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ -
рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рд╣рдЯрд╛рдиреЗ рдХреЗ рдЕрд▓рд╛рд╡рд╛ рд╕рдм рдХреБрдЫ рд╕рдорд╛рди рд╣реИред
рдмреИрдХрдПрдВрдб рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХреЗ рд▓рд┐рдП рдмрд┐рд▓реНрдб рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рдЗрд╕ рддрд░рд╣ рджрд┐рдЦреЗрдЧрд╛:
build.gradle.kts import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.1.3.RELEASE" id("io.spring.dependency-management") version "1.0.8.RELEASE" kotlin("jvm") version "1.3.50" kotlin("plugin.spring") version "1.3.50" id("org.jetbrains.kotlin.plugin.jpa") version "1.3.50" } group = "com.kotlin-spring-vue" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } } dependencies { runtimeOnly(project(":frontend")) implementation("org.springframework.boot:spring-boot-starter-actuator:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-web:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-mail:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-security:2.1.3.RELEASE") implementation("org.postgresql:postgresql:42.2.5") implementation("org.springframework.boot:spring-boot-starter-thymeleaf:2.1.3.RELEASE") implementation("commons-io:commons-io:2.4") implementation("io.jsonwebtoken:jjwt:0.9.0") implementation("io.jsonwebtoken:jjwt-api:0.10.6") implementation("com.mashape.unirest:unirest-java:1.4.9") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") runtimeOnly("org.springframework.boot:spring-boot-devtools:2.1.3.RELEASE") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-noarg:1.3.50") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "1.8" } }
- рд╕рднреА рдЖрд╡рд╢реНрдпрдХ рдХреЛрдЯрд▓рд┐рди рдФрд░ рд╕реНрдкреНрд░рд┐рдВрдЧ рдкреНрд▓рдЧрдЗрдиреНрд╕ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд┐рдП рдЬрд╛рдиреЗ рдЪрд╛рд╣рд┐рдПред
- рдкреНрд▓рдЧрдЗрди рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдордд рднреВрд▓рдирд╛ org.jetbrains.kotlin.plugin.jpa - рдбреЗрдЯрд╛рдмреЗрд╕ рд╕реЗ рдХрдиреЗрдХреНрдЯ рдХрд░рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реИ
- рдирд┐рд░реНрднрд░рддрд╛ рдореЗрдВ, рдЖрдкрдХреЛ
runtimeOnly(project(":frontend"))
рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ runtimeOnly(project(":frontend"))
- рд╣рдореЗрдВ рдкрд╣рд▓реЗ runtimeOnly(project(":frontend"))
рдмрдирд╛рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ
рджреГрд╢реНрдпрдкрдЯрд▓ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдХреЗ рд▓рд┐рдП рд╡рд┐рдиреНрдпрд╛рд╕ рдмрдирд╛рдПрдБ:
build.gradle.kts plugins { id("org.siouan.frontend") version "1.2.1" id("java") } group = "com.kotlin-spring-vue" version = "0.0.1-SNAPSHOT" java { targetCompatibility = JavaVersion.VERSION_1_8 } buildscript { repositories { mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } } } frontend { nodeVersion.set("10.16.0") cleanScript.set("run clean") installScript.set("install") assembleScript.set("run build") } tasks.named("jar", Jar::class) { dependsOn("assembleFrontend") from("$buildDir/dist") into("static") }
- рдореЗрд░реЗ рдЙрджрд╛рд╣рд░рдг рдореЗрдВ, рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП
org.siouan.frontend
рдкреНрд▓рдЧрдЗрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ frontend {...}
рд╕реЗрдХреНрд╢рди frontend {...}
Node.js рдХреЗ рд╕рдВрд╕реНрдХрд░рдг рдХреЛ рдЗрдВрдЧрд┐рдд рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП, рд╕рд╛рде рд╣реА рдЙрди рдХрдорд╛рдВрдбреНрд╕ рдХреЛ рдЗрдВрдЧрд┐рдд рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП рдЬреЛ package.json
. package.json
рдореЗрдВ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рд╕рдлрд╛рдИ, рд╕реНрдерд╛рдкрдирд╛ рдФрд░ рдЕрд╕реЗрдВрдмрд▓реА рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдХрд╛ рдЖрд╣реНрд╡рд╛рди рдХрд░рддреЗ рд╣реИрдВ package.json
- рдЕрдм рд╣рдо рдЕрдкрдиреЗ рдлреНрд░рдВрдЯрдПрдВрдб рд╕рдмрдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХреЛ рдПрдХ JAR рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рдкреИрдХ рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рдЗрд╕реЗ рдПрдХ рдирд┐рд░реНрднрд░рддрд╛ рдХреЗ рд░реВрдк рдореЗрдВ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реИрдВ (
runtimeOnly(project(":frontend"))
), рдЗрд╕рд▓рд┐рдП рд╣рдореЗрдВ рдПрдХ рдХрд╛рд░реНрдп рдХрд╛ рд╡рд░реНрдгрди рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рдЬреЛ рдЕрд╕реЗрдВрдмрд▓реА рдбрд╛рдпрд░реЗрдХреНрдЯрд░реА рд╕реЗ рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЛ рдХреЙрдкреА рдХрд░рддрд╛ рд╣реИ / рд╕рд╛рд░реНрд╡рдЬрдирд┐рдХ рдФрд░ рдПрдХ рдЬрд╛рд░ рдлрд╝рд╛рдЗрд▓ рдмрдирд╛рддрд╛ рд╣реИ
рдиреЛрдЯ:vue.config.js
рд╕рдВрдкрд╛рджрд┐рдд рдХрд░реЗрдВ, рдирд┐рд░реНрдорд╛рдг рдирд┐рд░реНрджреЗрд╢рд┐рдХрд╛ рдХреЛ рдмрджрд▓рдиреЗ / рдирд┐рд░реНрдорд╛рдг рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП редpackage.json
рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдлрд╝рд╛рдЗрд▓ рдХрд╛ рдирд┐рд░реНрдорд╛рдг , vue-cli-service build
рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░реЗрдВ рдпрд╛ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░реЗрдВ рдХрд┐ рдпрд╣ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рд╣реИ
рд░реВрдЯ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдореЗрдВ settings.gradle.kts рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рдирд┐рдореНрди рдХреЛрдб рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдП: ...
rootProject.name = "demo" include(":frontend", ":backend")
... рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдФрд░ рд╕рдмрдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХрд╛ рдирд╛рдо рд╣реИред
рдФрд░ рдЕрдм рд╣рдо рдХрдорд╛рдВрдб рдЪрд▓рд╛рдХрд░ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХрд╛ рдирд┐рд░реНрдорд╛рдг рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ:
./gradlew build
рдиреЛрдЯ: рдпрджрд┐
рдЖрд╡реЗрджрди рдореЗрдВ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдкреНрд▓реЗрд╕рд╣реЛрд▓реНрдбрд░реНрд╕ рдХреЗ рд▓рд┐рдПред
${SPRING_DATASOURCE_URL}
(рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП,
${SPRING_DATASOURCE_URL}
) рдХреЛрдИ рд╕рдВрдЧрдд рд╡рд╛рддрд╛рд╡рд░рдг рдЪрд░ рдирд╣реАрдВ рд╣реИрдВ, рддреЛ рдЕрд╕реЗрдВрдмрд▓реА рд╡рд┐рдлрд▓ рд╣реЛ рдЬрд╛рдПрдЧреАред рдЗрд╕рд╕реЗ рдмрдЪрдиреЗ рдХреЗ рд▓рд┐рдП
/gradlew build -x
рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ
рдЖрдк
gradle -q projects
рдХрдорд╛рдВрдб рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдкрд░рд┐рдпреЛрдЬрдирд╛рдУрдВ рдХреА рд╕рдВрд░рдЪрдирд╛ рдХреА рдЬрд╛рдВрдЪ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдкрд░рд┐рдгрд╛рдо рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрдирд╛ рдЪрд╛рд╣рд┐рдП:
Root project 'demo' +--- Project ':backend' \--- Project ':frontend'
рдФрд░ рдЕрдВрдд рдореЗрдВ, рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдЪрд▓рд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЪрд▓рдирд╛ рд╣реЛрдЧрд╛
./gradlew bootRun
.gitignore
рдирд┐рдореНрди рдлрд╝рд╛рдЗрд▓реЛрдВ рдФрд░ рдлрд╝реЛрд▓реНрдбрд░реЛрдВ рдХреЛ
.gitignore рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рдЬреЛрдбрд╝рд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП:
- рдмреИрдХрдПрдВрдб / рдмрд┐рд▓реНрдб /
- рд╕реАрдорд╛ / рдирд┐рд░реНрдорд╛рдг /
- рдирд┐рд░реНрдорд╛рдг
- .gradle
рдорд╣рддреНрд╡рдкреВрд░реНрдг: рдЖрдкрдХреЛ
gradlew
рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЛ рдирд╣реАрдВ рдЬреЛрдбрд╝рдирд╛ рдЪрд╛рд╣рд┐рдП - рдЙрдирдореЗрдВ рдХреБрдЫ рднреА рдЦрддрд░рдирд╛рдХ рдирд╣реАрдВ рд╣реИ, рд▓реЗрдХрд┐рди рджреВрд░рд╕реНрде рд╕рд░реНрд╡рд░ рдкрд░ рд╕рдлрд▓ рдЕрд╕реЗрдВрдмрд▓реА рдХреЗ рд▓рд┐рдП рдЙрдирдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред
рд╣рд░реЛрдХреВ рдкрд░ рддреИрдирд╛рддреА
рдЖрдЗрдП рджреЗрдЦреЗрдВ рдХрд┐ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд╕реБрд░рдХреНрд╖рд┐рдд рд░реВрдк рд╕реЗ рд╣рд░реЛрдХреВ рдореЗрдВ рддреИрдирд╛рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╣рдореЗрдВ рдХрд┐рди рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред
# 1 Procfileрд╣рдореЗрдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓реЙрдиреНрдЪ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╣рд░реЛрдХреВ рдирдП рдирд┐рд░реНрджреЗрд╢реЛрдВ рдХреЛ рдкреВрдЫрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ:
web: java -Dserver.port=$PORT -jar backend/build/libs/backend-0.0.1-SNAPSHOT.jar
# 2 рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░рд╣рд░реЛрдХреВ рдЖрд╡реЗрджрди рдХреЗ рдкреНрд░рдХрд╛рд░ рдХреЛ рд░реАрдореЗрдХ рдХрд░рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реИ (рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рд╕реНрдкреНрд░рд┐рдВрдЧ рдмреВрдЯ рдПрдкреНрд▓рд┐рдХреЗрд╢рди) рдФрд░ рдЙрдкрдпреБрдХреНрдд рд╡рд┐рдзрд╛рдирд╕рднрд╛ рдирд┐рд░реНрджреЗрд╢реЛрдВ рдХрд╛ рдкрд╛рд▓рди рдХрд░реЗрдВред рд▓реЗрдХрд┐рди рд╣рдорд╛рд░рд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди (рд░реВрдЯ рдкреНрд░реЛрдЬреЗрдХреНрдЯ) рд╣рд░реЛрдХреВ рдХреЛ рд╕реНрдкреНрд░рд┐рдВрдЧ рдмреВрдЯ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреА рддрд░рд╣ рдирд╣реАрдВ рджрд┐рдЦрддрд╛ рд╣реИред рдпрджрд┐ рд╣рдо рд╕рдм рдХреБрдЫ рд╡реИрд╕рд╛ рд╣реА рдЫреЛрдбрд╝ рджреЗрддреЗ рд╣реИрдВ, рддреЛ рд╣рд░реЛрдХреВ рд╣рдореЗрдВ
stage
рдХреЛ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд╣реЗрдВрдЧреЗред рдИрдорд╛рдирджрд╛рд░реА рд╕реЗ, рдореБрдЭреЗ рдирд╣реАрдВ рдкрддрд╛ рдХрд┐ рдпрд╣ рд░рд╛рд╕реНрддрд╛ рдХрд╣рд╛рдВ рд╕рдорд╛рдкреНрдд рд╣реЛрддрд╛ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдореИрдВрдиреЗ рдЗрд╕рдХрд╛ рдкрд╛рд▓рди рдирд╣реАрдВ рдХрд┐рдпрд╛ред
build
рдорд╛рди рдХреЗ рд╕рд╛рде
GRADLE_TASK
рдЪрд░ рдХреЛ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдХрд░рдирд╛ рдЖрд╕рд╛рди рд╣реИ:
# 3 reCAPTCHAрдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдПрдХ рдирдП рдбреЛрдореЗрди рдореЗрдВ рд░рдЦрддреЗ рд╕рдордп, рдХреИрдкреНрдЪрд╛,
GOOGLE_RECAPTCHA_KEY_SITE
рдФрд░
GOOGLE_RECAPTCHA_KEY_SECRET
рдкрд░рд┐рд╡реЗрд╢ рдЪрд░ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдирд╛ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░реЗрдВ, рдФрд░ рджреГрд╢реНрдпрдкрдЯрд▓ рдЙрдкрдкреНрд░реЛрдЬреЗрдХреНрдЯ рдореЗрдВ рд╕рд╛рдЗрдЯ рдХреБрдВрдЬреА рднреА рдЕрдкрдбреЗрдЯ рдХрд░реЗрдВред
рдХреБрдХреАрдЬрд╝ рдореЗрдВ рднрдВрдбрд╛рд░рдг JWT рдЯреЛрдХрди
рд╕рдмрд╕реЗ рдкрд╣рд▓реЗ, рдореИрдВ рджреГрдврд╝рддрд╛ рд╕реЗ рдЕрдиреБрд╢рдВрд╕рд╛ рдХрд░рддрд╛ рд╣реВрдВ рдХрд┐ рдЖрдк
рд╕реНрдерд╛рдиреАрдп рднрдВрдбрд╛рд░рдг рд▓реЗрдЦ
рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдХреГрдкрдпрд╛ рд╕реНрдЯреЙрдк рдХреЛ рдкрдврд╝реЗрдВ, рд╡рд┐рд╢реЗрд╖ рд░реВрдк рд╕реЗ
рдХреНрдпреЛрдВ рд╕реНрдерд╛рдиреАрдп рднрдВрдбрд╛рд░рдг рдЕрд╕реБрд░рдХреНрд╖рд┐рдд рд╣реИ рдФрд░ рдЖрдкрдХреЛ рдЗрд╕реЗ рд╕рдВрд╡реЗрджрдирд╢реАрд▓ рдбреЗрдЯрд╛ рдЕрдиреБрднрд╛рдЧ
рдХреЛ рд╕реНрдЯреЛрд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЙрдкрдпреЛрдЧ рдирд╣реАрдВ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП ред
рдЖрдЗрдП рджреЗрдЦреЗрдВ рдХрд┐ рдЖрдк
httpOnly
рдЭрдВрдбреЗ рдореЗрдВ JWT рдЯреЛрдХрди рдХреЛ рдПрдХ рдЕрдзрд┐рдХ рд╕реБрд░рдХреНрд╖рд┐рдд рд╕реНрдерд╛рди рдкрд░ рдХреИрд╕реЗ рд╕реНрдЯреЛрд░ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ - рдЬрд╣рд╛рдВ рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рдкрдврд╝рдиреЗ / рдмрджрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рдпрд╣ рдЕрдиреБрдкрд▓рдмреНрдз рд╣реЛрдЧрд╛ред
# 1 рджреГрд╢реНрдпрдкрдЯрд▓ рд╕реЗ рд╕рднреА JWT рд╕рдВрдмрдВрдзрд┐рдд рддрд░реНрдХ рдХреЛ рд╣рдЯрд╛рдирд╛:
рдЪреВрдВрдХрд┐ рдЯреЛрдХрди рдЕрднреА рднреА рдирдИ рд╕рдВрдЧреНрд░рд╣рдг рдкрджреНрдзрддрд┐ рдХреЗ рд╕рд╛рде рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдХреЗ рд╕рд╛рде рдЙрдкрд▓рдмреНрдз рдирд╣реАрдВ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЖрдк рд╣рдорд╛рд░реЗ рд╕рдмрдкреНрд░реЛрдЬреЗрдХреНрдЯ рд╕реЗ рд╕реБрд░рдХреНрд╖рд┐рдд рд░реВрдк рд╕реЗ рдЗрд╕рдХреЗ рд╕рднреА рд╕рдВрджрд░реНрдн рд╣рдЯрд╛ рд╕рдХрддреЗ рд╣реИрдВред
рд▓реЗрдХрд┐рди рдХрд┐рд╕реА рднреА рдЕрдиреНрдп рдбреЗрдЯрд╛ рдХреЗ рд╕рдВрджрд░реНрдн рдХреЗ рдмрд┐рдирд╛ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреА рднреВрдорд┐рдХрд╛ рдЗрддрдиреА рдорд╣рддреНрд╡рдкреВрд░реНрдг рдЬрд╛рдирдХрд╛рд░реА рдирд╣реАрдВ рд╣реИ, рдпрд╣ рдЕрднреА рднреА рд╕реНрдерд╛рдиреАрдп рднрдВрдбрд╛рд░рдг рдореЗрдВ рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ рдФрд░ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ рдХрд┐ рдХреНрдпрд╛ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдЗрд╕ рднреВрдорд┐рдХрд╛ рдХреЛ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдХрд░рддрд╛ рд╣реИ рдпрд╛ рдирд╣реАрдВ, рдЗрд╕рдХреЗ рдЖрдзрд╛рд░ рдкрд░ред
store / index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const state = { role: localStorage.getItem('user-role') || '', username: localStorage.getItem('user-name') || '', authorities: localStorage.getItem('authorities') || '', }; const getters = { isAuthenticated: state => { if (state.role != null && state.role != '') { return true; } else { return false; } }, isAdmin: state => { if (state.role === 'admin') { return true; } else { return false; } }, getUsername: state => { return state.username; }, getAuthorities: state => { return state.authorities; } }; const mutations = { auth_login: (state, user) => { localStorage.setItem('user-name', user.username); localStorage.setItem('user-authorities', user.roles); state.username = user.username; state.authorities = user.roles; var isUser = false; var isAdmin = false; for (var i = 0; i < user.roles.length; i++) { if (user.roles[i].authority === 'ROLE_USER') { isUser = true; } else if (user.roles[i].authority === 'ROLE_ADMIN') { isAdmin = true; } } if (isUser) { localStorage.setItem('user-role', 'user'); state.role = 'user'; } if (isAdmin) { localStorage.setItem('user-role', 'admin'); state.role = 'admin'; } }, auth_logout: () => { state.token = ''; state.role = ''; state.username = ''; state.authorities = []; localStorage.removeItem('user-role'); localStorage.removeItem('user-name'); localStorage.removeItem('user-authorities'); } }; const actions = { login: (context, user) => { context.commit('auth_login', user) }, logout: (context) => { context.commit('auth_logout'); } }; export const store = new Vuex.Store({ state, getters, mutations, actions });
рд░рд┐рдлреИрдХреНрдЯрд░рд┐рдВрдЧ рдХрд░рддреЗ рд╕рдордп рд╕рд╛рд╡рдзрд╛рди рд░рд╣реЗрдВ store/index.js
: рдпрджрд┐ рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдФрд░ рдбреАрдереЛрд░рд╛рдЗрдЬреЗрд╢рди рд╕рд╣реА рдврдВрдЧ рд╕реЗ рдХрд╛рдо рдирд╣реАрдВ рдХрд░рддреЗ рд╣реИрдВ, рддреЛ рддреНрд░реБрдЯрд┐ рд╕рдВрджреЗрд╢ рдХрдВрд╕реЛрд▓ рдореЗрдВ рд▓рдЧрд╛рддрд╛рд░ рдЬрд╛рд░реА рд░рд╣реЗрдЧрд╛ред# 2 рдЬреЗрдбрдмреНрд▓реНрдпреВрдЯреА рдХреЛ рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдирд┐рдпрдВрддреНрд░рдХ рдореЗрдВ рдХреБрдХреА рдХреЗ рд░реВрдк рдореЗрдВ рд▓реМрдЯрд╛рдПрдВ ( рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдирд┐рдХрд╛рдп рдореЗрдВ рдирд╣реАрдВ ):AuthController.kt @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String @Value("\${ksvg.app.isCookieSecure}") var isCookieSecure: Boolean = true @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser, response: HttpServletResponse): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (userCandidate.isPresent) { val user: User = userCandidate.get() val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)) SecurityContextHolder.getContext().setAuthentication(authentication) val jwt: String = jwtProvider.generateJwtToken(user.username!!) val cookie: Cookie = Cookie(authCookieName, jwt) cookie.maxAge = jwtProvider.jwtExpiration!! cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(SuccessfulSigninResponse(user.username, authorities)) } else { return ResponseEntity(ResponseMessage("User not found!"), HttpStatus.BAD_REQUEST) } }
рдорд╣рддреНрд╡рдкреВрд░реНрдг : рдХреГрдкрдпрд╛ рдзреНрдпрд╛рди рджреЗрдВ рдХрд┐ рдореИрдВ рд╡рд┐рдХрд▓реНрдк рд░рдЦрд╛ authCookieName
рдФрд░ isCookieSecure
рдореЗрдВ application.properties - рдзреНрд╡рдЬ рдХреЛ рдХреБрдХреА secure
рдХреЗрд╡рд▓ https рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдЬреЛ рдпрд╣ рдЕрддреНрдпрдВрдд рд╕реНрдерд╛рдиреАрдп рд╣реЛрд╕реНрдЯ рдХреЗ рд╕рд╛рде рдбрд┐рдмрдЧ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдореБрд╢реНрдХрд┐рд▓ рдмрдирд╛ рджреЗрддрд╛ рд╣реИред рдЙрддреНрдкрд╛рджрди рдореЗрдВ BUT, рдирд┐рд╢реНрдЪрд┐рдд рд░реВрдк рд╕реЗ, рдЗрд╕ рдзреНрд╡рдЬ рдХреЗ рд╕рд╛рде рдХреБрдХреАрдЬрд╝ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдирд╛ рдмреЗрд╣рддрд░ рд╣реИредрдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, JWT рдХреЗ рд▓рд┐рдП рд╡рд┐рд╢реЗрд╖ рдХреНрд╖реЗрддреНрд░ рдХреЗ рдмрд┐рдирд╛ рдЗрдХрд╛рдИ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдирд┐рдпрдВрддреНрд░рдХ рдХреА рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рдУрдВ рдХреЗ рд▓рд┐рдП рдпрд╣ рдЕрдм рдЙрдЪрд┐рдд рд╣реИред# 3 рдЕрджреНрдпрддрди JwtAuthTokenFilter
:рд╣рдо рдЕрдиреБрд░реЛрдз рд╣реЗрдбрд░ рд╕реЗ рдПрдХ рдЯреЛрдХрди рд▓реЗрддреЗ рдереЗ, рдЕрдм рд╣рдо рдЗрд╕реЗ рдХреБрдХреАрдЬрд╝ рд╕реЗ рд▓реЗрддреЗ рд╣реИрдВ:JwtAuthTokenFilter.kt @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String ... private fun getJwt(request: HttpServletRequest): String? { for (cookie in request.cookies) { if (cookie.name == authCookieName) { return cookie.value } } return null }
# 4 рдХреЛрд░ рдХреЛ рд╕рдХреНрд╖рдо рдХрд░рдирд╛рдЕрдЧрд░ рдореЗрд░реЗ рдкрд┐рдЫрд▓реЗ рд▓реЗрдЦ рдореЗрдВ рдЖрдк рдЕрднреА рднреА рдЪреБрдкрдЪрд╛рдк рдЗрд╕ рд╕рд╡рд╛рд▓ рдХреЛ рдЫреЛрдбрд╝ рд╕рдХрддреЗ рд╣реИрдВ, рддреЛ рдЕрдм рдмреИрдХрд╡рд░реНрдб рд╕рд╛рдЗрдб рдкрд░ рдХреЙрд░реНрд╕ рдХреЛ рд╕рдХреНрд╖рдо рдХрд┐рдП рдмрд┐рдирд╛ JWT рдЯреЛрдХрди рдХреА рд░рдХреНрд╖рд╛ рдХрд░рдирд╛ рдЕрдЬреАрдм рд╣реЛрдЧрд╛редрдЖрдк рдЗрд╕реЗ рд╕рдВрдкрд╛рджрд┐рдд рдХрд░рдХреЗ рдареАрдХ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ WebSecurityConfig.kt
:WebSecurityConfig.kt @Bean fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOrigins = Arrays.asList("http://localhost:8080", "http://localhost:8081", "https://kotlin-spring-vue-gradle-demo.herokuapp.com") configuration.allowedHeaders = Arrays.asList("*") configuration.allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowCredentials = true configuration.maxAge = 3600 val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", configuration) return source } @Throws(Exception::class) override fun configure(http: HttpSecurity) { http .cors().and() .csrf().disable().authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java) http.headers().cacheControl().disable() }
рдФрд░ рдЕрдм рдЖрдк @CrossOrigin
рдирд┐рдпрдВрддреНрд░рдХреЛрдВ рд╕реЗ рд╕рднреА рдПрдиреЛрдЯреЗрд╢рди рдирд┐рдХрд╛рд▓ рд╕рдХрддреЗ рд╣реИрдВ редрдорд╣рддреНрд╡рдкреВрд░реНрдг: рд╕реАрдорд╛рдВрдд рд╕реЗ рдЕрдиреБрд░реЛрдз рднреЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП AllowCredentials рдкреИрд░рд╛рдореАрдЯрд░ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИред рдЗрд╕рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдпрд╣рд╛рдБ рдФрд░ рдкрдврд╝реЗрдВ ред# 5 рд╕рд╛рдордиреЗ рдХреЗ рдХрд┐рдирд╛рд░реЗ рдкрд░ рд╣реЗрдбрд░ рдЕрдкрдбреЗрдЯ рдХрд░рдирд╛:http-commons.js export const AXIOS = axios.create({ baseURL: `/api`, headers: { 'Access-Control-Allow-Origin': ['http://localhost:8080', 'http://localhost:8081', 'https://kotlin-spring-vue-gradle-demo.herokuapp.com'], 'Access-Control-Allow-Methods': 'GET,POST,DELETE,PUT,OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Credentials': true } })
рдирд┐рд░реАрдХреНрд╖рдг
рдЖрдЗрдП рдПрдХ рд╣реЛрд╕реНрдЯ рд╕реЗ рд▓реЙрдЧ рдЗрди рдХрд░рдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд▓реЙрдЧ рдЗрди рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░реЗрдВ рдЬреЛ рдЕрдиреБрдордд рд╕реВрдЪреА рдореЗрдВ рдирд╣реАрдВ рд╣реИ WebSecurityConfig.kt
ред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдкреЛрд░реНрдЯ рдкрд░ рдмреИрдХрдПрдВрдб 8080
рдФрд░ рдлреНрд░рдВрдЯрдПрдВрдб рдХреЛ рдЪрд▓рд╛рдПрдВ , рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, 8082
рд▓реЙрдЧ рдЗрди рдХрд░рдиреЗ рдХрд╛ рдкреНрд░рдпрд╛рд╕ рдХрд░реЗрдВ:рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдХрд╛ рдЕрдиреБрд░реЛрдз CORS рдиреАрддрд┐ рджреНрд╡рд╛рд░рд╛ рдЦрд╛рд░рд┐рдЬ рдХрд░ рджрд┐рдпрд╛ рдЧрдпрд╛редрдЕрдм рджреЗрдЦрддреЗ рд╣реИрдВ рдХрд┐ рд╕рд╛рдорд╛рдиреНрдп рд░реВрдк рд╕реЗ рдзреНрд╡рдЬ рдХреБрдХреАрдЬрд╝ рдХреИрд╕реЗ рдХрд╛рдо рдХрд░рддреА рд╣реИрдВ httpOnly
ред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдЗрдП, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рд╕рд╛рдЗрдЯ https://kotlinlang.org рдкрд░ рдЬрд╛рдПрдВ рдФрд░ рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХрдВрд╕реЛрд▓ рдореЗрдВ рдЗрд╕реЗ рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд░реЗрдВ : document.cookie
httpOnly
рдЗрд╕ рд╕рд╛рдЗрдЯ рд╕реЗ рд╕рдВрдмрдВрдзрд┐рдд рдЧреИрд░- рдХреБрдХреАрдЬрд╝ рдХрдВрд╕реЛрд▓ рдореЗрдВ рджрд┐рдЦрд╛рдИ рджреЗрдВрдЧреЗ , рдЬреЛ рдХрд┐ рдЬреИрд╕рд╛ рдХрд┐ рд╣рдо рджреЗрдЦрддреЗ рд╣реИрдВ, рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╕реБрд▓рдн рд╣реИрдВредрдЕрдм рд╣рдорд╛рд░реЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдЪрд▓рддреЗ рд╣реИрдВ, рд▓реЙрдЧ рдЗрди рдХрд░реЗрдВ (рддрд╛рдХрд┐ рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреБрдХреА рдХреЛ JWT рд╕реЗ рдмрдЪрд╛рддрд╛ рд╣реИ ) рдФрд░ рд╡рд╣реА рдмрд╛рдд рджреЛрд╣рд░рд╛рдПрдВ:рдиреЛрдЯ: JWT рдЯреЛрдХрди рд╕реНрдЯреЛрд░ рдХрд░рдиреЗ рдХрд╛ рдпрд╣ рддрд░реАрдХрд╛ рд▓реЛрдХрд▓ рд╕реНрдЯреЛрд░реЗрдЬ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рд╕реЗ рдЕрдзрд┐рдХ рд╡рд┐рд╢реНрд╡рд╕рдиреАрдп рд╣реИ, рд▓реЗрдХрд┐рди рдЖрдкрдХреЛ рдпрд╣ рд╕рдордЭрдирд╛ рдЪрд╛рд╣рд┐рдП рдХрд┐ рдпрд╣ рд░рд╛рдордмрд╛рдг рдирд╣реАрдВ рд╣реИредрдИрдореЗрд▓ рдкрдВрдЬреАрдХрд░рдг рдХреА рдкреБрд╖реНрдЯрд┐
рдЗрд╕ рдХрд╛рд░реНрдп рдХреЛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рд╕рдВрдХреНрд╖рд┐рдкреНрдд рдПрд▓реНрдЧреЛрд░рд┐рдереНрдо рдЗрд╕ рдкреНрд░рдХрд╛рд░ рд╣реИ:- рд╕рднреА рдирдП рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛рдУрдВ рдХреЗ рд▓рд┐рдП,
isEnabled
рдбреЗрдЯрд╛рдмреЗрд╕ рдореЗрдВ рд╡рд┐рд╢реЗрд╖рддрд╛ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рд╣реИfalse
- рдПрдХ рд╕реНрдЯреНрд░рд┐рдВрдЧ рдЯреЛрдХрди рдордирдорд╛рдирд╛ рдкрд╛рддреНрд░реЛрдВ рд╕реЗ рдЙрддреНрдкрдиреНрди рд╣реЛрддрд╛ рд╣реИ, рдЬреЛ рдкрдВрдЬреАрдХрд░рдг рдХреА рдкреБрд╖реНрдЯрд┐ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдХреБрдВрдЬреА рдХреЗ рд░реВрдк рдореЗрдВ рдХрд╛рдо рдХрд░реЗрдЧрд╛
- рд▓рд┐рдВрдХ рдХреЗ рднрд╛рдЧ рдХреЗ рд░реВрдк рдореЗрдВ рдореЗрд▓ рдореЗрдВ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреЛ рдЯреЛрдХрди рднреЗрдЬрд╛ рдЬрд╛рддрд╛ рд╣реИ
- рдЧреБрдг
isEnabled
рдореВрд▓реНрдп рд▓реЗрддрд╛ рд╣реИ рд╕рдЪ рд╣реИ, рд╕рдордп рдХреА рдПрдХ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдЕрд╡рдзрд┐ рдХреЗ рднреАрддрд░ рдПрдХ рд▓рд┐рдВрдХ рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░рддрд╛ рд╣реИ рддреЛ
рдЕрдм рдЗрд╕ рдкреНрд░рдХреНрд░рд┐рдпрд╛ рдкрд░ рдЕрдзрд┐рдХ рд╡рд┐рд╕реНрддрд╛рд░ рд╕реЗ рд╡рд┐рдЪрд╛рд░ рдХрд░реЗрдВредрд╣рдореЗрдВ рдкрдВрдЬреАрдХрд░рдг рдХреА рдкреБрд╖реНрдЯрд┐ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЯреЛрдХрди рд╕реНрдЯреЛрд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдЯреЗрдмрд▓ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ: CREATE TABLE public.verification_token ( id serial NOT NULL, token character varying, expiry_date timestamp without time zone, user_id integer, PRIMARY KEY (id) ); ALTER TABLE public.verification_token ADD CONSTRAINT verification_token_users_fk FOREIGN KEY (user_id) REFERENCES public.users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;
рдФрд░, рддрджрдиреБрд╕рд╛рд░, рдСрдмреНрдЬреЗрдХреНрдЯ-рд░рд┐рд▓реЗрд╢рдирд▓ рдореИрдкрд┐рдВрдЧ рдХреЗ рд▓рд┐рдП рдПрдХ рдирдИ рдЗрдХрд╛рдИ ...:VerificationToken.kt package com.kotlinspringvue.backend.jpa import java.sql.* import javax.persistence.* import java.util.Calendar @Entity @Table(name = "verification_token") data class VerificationToken( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = 0, @Column(name = "token") var token: String? = null, @Column(name = "expiry_date") val expiryDate: Date, @OneToOne(targetEntity = User::class, fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST]) @JoinColumn(nullable = false, name = "user_id") val user: User ) { constructor(token: String?, user: User) : this(0, token, calculateExpiryDate(1440), user) } private fun calculateExpiryDate(expiryTimeInMinutes: Int): Date { val cal = Calendar.getInstance() cal.time = Timestamp(cal.time.time) cal.add(Calendar.MINUTE, expiryTimeInMinutes) return Date(cal.time.time) }
... рдФрд░ рднрдВрдбрд╛рд░:VerificationTokenRepository.kt package com.kotlinspringvue.backend.repository import com.kotlinspringvue.backend.jpa.VerificationToken import org.springframework.data.jpa.repository.JpaRepository import java.util.* interface VerificationTokenRepository : JpaRepository<VerificationToken, Long> { fun findByToken(token: String): Optional<VerificationToken> }
рдЕрдм рд╣рдореЗрдВ рдЯреЛрдХрди рдХреЗ рдкреНрд░рдмрдВрдзрди рдХреЗ рд▓рд┐рдП рдЙрдкрдХрд░рдгреЛрдВ рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ - рдИрдореЗрд▓ рджреНрд╡рд╛рд░рд╛ рдмрдирд╛рдирд╛, рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░рдирд╛ рдФрд░ рднреЗрдЬрдирд╛ред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдо UserDetailsServiceImpl
рдЯреЛрдХрди рдмрдирд╛рдиреЗ рдФрд░ рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░рдиреЗ рдХреЗ рддрд░реАрдХреЛрдВ рдХреЛ рдЬреЛрдбрд╝рдХрд░ рд╕рдВрд╢реЛрдзрд┐рдд рдХрд░рддреЗ рд╣реИрдВ:UserDetailsServiceImpl.kt override fun createVerificationTokenForUser(token: String, user: User) { tokenRepository.save(VerificationToken(token, user)) } override fun validateVerificationToken(token: String): String { val verificationToken: Optional<VerificationToken> = tokenRepository.findByToken(token) if (verificationToken.isPresent) { val user: User = verificationToken.get().user val cal: Calendar = Calendar.getInstance() if ((verificationToken.get().expiryDate.time - cal.time.time) <= 0) { tokenRepository.delete(verificationToken.get()) return TOKEN_EXPIRED } user.enabled = true tokenRepository.delete(verificationToken.get()) userRepository.save(user) return TOKEN_VALID } else { return TOKEN_INVALID } }
рдЕрдм рдПрдХ рдкреБрд╖реНрдЯрд┐рдХрд░рдг рд▓рд┐рдВрдХ рдХреЗ рд╕рд╛рде рдПрдХ рдИрдореЗрд▓ рднреЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рд╡рд┐рдзрд┐ рдЬреЛрдбрд╝реЗрдВ EmailServiceImpl
:EmailServiceImpl.kt @Value("\${host.url}") lateinit var hostUrl: String @Autowired lateinit var userDetailsService: UserDetailsServiceImpl ... override fun sendRegistrationConfirmationEmail(user: User) { val token = UUID.randomUUID().toString() userDetailsService.createVerificationTokenForUser(token, user) val link = "$hostUrl/?token=$token&confirmRegistration=true" val msg = "<p>Please, follow the link to complete your registration:</p><p><a href=\"$link\">$link</a></p>" user.email?.let{sendHtmlMessage(user.email!!, "KSVG APP: Registration Confirmation", msg)} }
рдиреЛрдЯ:- рдореИрдВ URL рд╣реЛрд╕реНрдЯ рд░рдЦрдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд┐рдлрд╛рд░рд┐рд╢ рдХрд░реЗрдВрдЧреЗ application.properties
- рд╣рдорд╛рд░реЗ рд▓рд┐рдВрдХ рдореЗрдВ, рд╣рдо рдЙрд╕ рдкрддреЗ рдкрд░ рджреЛ GET рдкреИрд░рд╛рдореАрдЯрд░ (
token
рдФрд░ confirmRegistration
) рдкрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВ рдЬрд╣рд╛рдВ рдЖрд╡реЗрджрди рддреИрдирд╛рдд рд╣реИред рдмрд╛рдж рдореЗрдВ рд╕рдордЭрд╛рдКрдВрдЧрд╛ рдХрд┐ рдХреНрдпреЛрдВред
рд╣рдо рдирд┐рдореНрдирд╛рдиреБрд╕рд╛рд░ рдкрдВрдЬреАрдХрд░рдг рдирд┐рдпрдВрддреНрд░рдХ рдХреЛ рд╕рдВрд╢реЛрдзрд┐рдд рдХрд░рддреЗ рд╣реИрдВ:- рд╕рднреА рдирдП рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛
false
рдлрд╝реАрд▓реНрдб рдХреЗ рд▓рд┐рдП рдорд╛рди рд╕реЗрдЯ рдХрд░реЗрдВрдЧреЗisEnabled
- рдирдпрд╛ рдЦрд╛рддрд╛ рдмрдирд╛рдиреЗ рдХреЗ рдмрд╛рдж, рд╣рдо рдкрдВрдЬреАрдХрд░рдг рдХреА рдкреБрд╖реНрдЯрд┐ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдИрдореЗрд▓ рднреЗрдЬреЗрдВрдЧреЗ
- рдПрдХ рдЕрд▓рдЧ рдЯреЛрдХрди рд╕рддреНрдпрд╛рдкрди рдирд┐рдпрдВрддреНрд░рдХ рдмрдирд╛рдПрдБ
- рдорд╣рддреНрд╡рдкреВрд░реНрдг: рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдХреЗ рджреМрд░рд╛рди рд╣рдо рдЬрд╛рдБрдЪреЗрдВрдЧреЗ рдХрд┐ рдХреНрдпрд╛ рдЦрд╛рддрд╛ рд╕рддреНрдпрд╛рдкрд┐рдд рд╣реИ:
AuthController.kt package com.kotlinspringvue.backend.controller import com.kotlinspringvue.backend.email.EmailService import javax.validation.Valid import java.util.* import java.util.stream.Collectors import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.ui.Model import com.kotlinspringvue.backend.model.LoginUser import com.kotlinspringvue.backend.model.NewUser import com.kotlinspringvue.backend.web.response.SuccessfulSigninResponse import com.kotlinspringvue.backend.web.response.ResponseMessage import com.kotlinspringvue.backend.jpa.User import com.kotlinspringvue.backend.repository.UserRepository import com.kotlinspringvue.backend.repository.RoleRepository import com.kotlinspringvue.backend.jwt.JwtProvider import com.kotlinspringvue.backend.service.ReCaptchaService import com.kotlinspringvue.backend.service.UserDetailsService import org.springframework.beans.factory.annotation.Value import org.springframework.context.ApplicationEventPublisher import org.springframework.web.bind.annotation.* import org.springframework.web.context.request.WebRequest import java.io.UnsupportedEncodingException import javax.servlet.http.Cookie import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_VALID import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_INVALID import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_EXPIRED @RestController @RequestMapping("/api/auth") class AuthController() { @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String @Value("\${ksvg.app.isCookieSecure}") var isCookieSecure: Boolean = true @Autowired lateinit var authenticationManager: AuthenticationManager @Autowired lateinit var userRepository: UserRepository @Autowired lateinit var roleRepository: RoleRepository @Autowired lateinit var encoder: PasswordEncoder @Autowired lateinit var jwtProvider: JwtProvider @Autowired lateinit var captchaService: ReCaptchaService @Autowired lateinit var userService: UserDetailsService @Autowired lateinit var emailService: EmailService @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser, response: HttpServletResponse): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (userCandidate.isPresent) { val user: User = userCandidate.get() if (!user.enabled) { return ResponseEntity(ResponseMessage("Account is not verified yet! Please, follow the link in the confirmation email."), HttpStatus.UNAUTHORIZED) } val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)) SecurityContextHolder.getContext().setAuthentication(authentication) val jwt: String = jwtProvider.generateJwtToken(user.username!!) val cookie: Cookie = Cookie(authCookieName, jwt) cookie.maxAge = jwtProvider.jwtExpiration!! cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(SuccessfulSigninResponse(user.username, authorities)) } else { return ResponseEntity(ResponseMessage("User not found!"), HttpStatus.BAD_REQUEST) } } @PostMapping("/signup") fun registerUser(@Valid @RequestBody newUser: NewUser): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(newUser.username!!) if (!captchaService.validateCaptcha(newUser.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (!userCandidate.isPresent) { if (usernameExists(newUser.username!!)) { return ResponseEntity(ResponseMessage("Username is already taken!"), HttpStatus.BAD_REQUEST) } else if (emailExists(newUser.email!!)) { return ResponseEntity(ResponseMessage("Email is already in use!"), HttpStatus.BAD_REQUEST) } try {
рдЕрдм рдЖрдЧреЗ рдХреЗ рднрд╛рдЧ рдкрд░ рдХрд╛рдо рдХрд░рддреЗ рд╣реИрдВ:# 1 рдШрдЯрдХ рдмрдирд╛рдПрдВ RegistrationConfirmPage.vue
# 2router.js
рдкреИрд░рд╛рдореАрдЯрд░ рдХреЗ рд╕рд╛рде рдПрдХ рдирдпрд╛ рдкрде рдЬреЛрдбрд╝реЗрдВ :token
: { path: '/registration-confirm/:token', name: 'RegistrationConfirmPage', component: RegistrationConfirmPage }
# 3 рдЕрдкрдбреЗрдЯ SignUp.vue
- рдкреНрд░рдкрддреНрд░реЛрдВ рд╕реЗ рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдбреЗрдЯрд╛ рднреЗрдЬрдиреЗ рдХреЗ рдмрд╛рдж, рд╣рдо рдЙрдиреНрд╣реЗрдВ рд╕реВрдЪрд┐рдд рдХрд░реЗрдВрдЧреЗ рдХрд┐ рдкрдВрдЬреАрдХрд░рдг рдкреВрд░рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдкрддреНрд░ рдореЗрдВ рджрд┐рдП рдЧрдП рд▓рд┐рдВрдХ рдХрд╛ рдкрд╛рд▓рди рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред# 4 рдорд╣рддреНрд╡рдкреВрд░реНрдг: рдЕрдлрд╕реЛрд╕, рд╣рдо рдПрдХ рдЕрд▓рдЧ рдШрдЯрдХ рдХреЛ рдПрдХ рдирд┐рд╢реНрдЪрд┐рдд рд▓рд┐рдВрдХ рдирд╣реАрдВ рджреЗ рд╕рдХрддреЗ рд╣реИрдВ рдЬреЛ рдЯреЛрдХрди рдХреЛ рдорд╛рдиреНрдп рдХрд░реЗрдЧрд╛ рдФрд░ рд╕рдлрд▓рддрд╛ рдпрд╛ рд╡рд┐рдлрд▓рддрд╛ рдХреА рд░рд┐рдкреЛрд░реНрдЯ рдХрд░реЗрдЧрд╛ред рд╕рднреА рд╕рдорд╛рди рд░рд╛рд╕реНрддреЛрдВ рдХреЗ рд▓рд┐рдВрдХ рд╣рдореЗрдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдореВрд▓ рдкреГрд╖реНрда рдкрд░ рд▓реЗ рдЬрд╛рдПрдВрдЧреЗред рд▓реЗрдХрд┐рди рд╣рдо рдЕрдкрдиреЗ рдЖрд╡реЗрджрди рдХреЛ рдкреНрд░реЗрд╖рд┐рдд рдЬреАрдИрдЯреА рдкреИрд░рд╛рдореАрдЯрд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдкрдВрдЬреАрдХрд░рдг рдХреА рдкреБрд╖реНрдЯрд┐ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдмрддрд╛ рд╕рдХрддреЗ рд╣реИрдВ confirmRegistration
: methods: { confirmRegistration() { if (this.$route.query.confirmRegistration === 'true' && this.$route.query.token != null) { this.$router.push({name: 'RegistrationConfirmPage', params: { token: this.$route.query.token}}); } }, ... mounted() { this.confirmRegistration(); }
# 5 рдЪрд▓рд┐рдП рдПрдХ рдШрдЯрдХ рдмрдирд╛рддреЗ рд╣реИрдВ рдЬреЛ рд╕рддреНрдпрд╛рдкрди рдкрд░ рдЯреЛрдХрди рд╕рддреНрдпрд╛рдкрди рдФрд░ рд░рд┐рдкреЛрд░реНрдЯ рдХрд░рддрд╛ рд╣реИ:RegistrationConfirmPage.vue <template> <div id="registration-confirm"> <div class="confirm-form"> <b-card title="Confirmation" tag="article" style="max-width: 20rem;" class="mb-2" > <div v-if="isSuccess"> <p class="success">Account is successfully verified!</p> <router-link to="/login"> <b-button variant="primary">Login</b-button> </router-link> </div> <div v-if="isError"> <p class="fail">Verification failed:</p> <p>{{ errorMessage }}</p> </div> </b-card> </div> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'RegistrationConfirmPage', data() { return { isSuccess: false, isError: false, errorMessage: '' } }, methods: { executeVerification() { AXIOS.post(`/auth/registrationConfirm`, null, {params: { 'token': this.$route.params.token}}) .then(response => { this.isSuccess = true; console.log(response); }, error => { this.isError = true; this.errorMessage = error.response.data.message; }) .catch(e => { console.log(e); this.errorMessage = 'Server error. Please, report this error website owners'; }) } }, mounted() { this.executeVerification(); } } </script> <style scoped> .confirm-form { margin-left: 38%; margin-top: 50px; } .success { color: green; } .fail { color: red; } </style>
рдПрдХ рдирд┐рд╖реНрдХрд░реНрд╖ рдХреЗ рдмрдЬрд╛рдп
рдЗрд╕ рд╕рд╛рдордЧреНрд░реА рдХреЗ рдЕрдВрдд рдореЗрдВ, рдореИрдВ рдПрдХ рд╡рд┐рд╖рдпрд╛рдВрддрд░ рдХрд░рдирд╛ рдЪрд╛рд╣реВрдВрдЧрд╛ рдФрд░ рдХрд╣рдирд╛ рдЪрд╛рд╣реВрдВрдЧрд╛ рдХрд┐ рдЗрд╕ рдкрд░ рдЪрд░реНрдЪрд╛ рдХреА рдЧрдИ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреА рдмрд╣реБрдд рдЕрд╡рдзрд╛рд░рдгрд╛ рдФрд░ рдкрд┐рдЫрд▓рд╛ рд▓реЗрдЦ рд▓реЗрдЦрди рдХреЗ рд╕рдордп рдирдпрд╛ рдирд╣реАрдВ рдерд╛ред рдЯрд╛рд╕реНрдХ рдЬрд▓реНрджреА рдЖрдзреБрдирд┐рдХ рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдЪреМрдЦрдЯреЗ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕реНрдкреНрд░рд┐рдВрдЧ рдмреВрдЯ рдкрд░ рдЖрд╡реЗрджрди рдХреА рдПрдХ рдкреВрд░реА рдвреЗрд░ рдХреЛрдгреАрдп / рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ / Vue.js рд╕реБрдВрджрд░ рдврдВрдЧ рд╕реЗ рд╣рд▓ рдХрд░рддреА рдмрдирд╛рдиреЗ Hipster редрд╣рд╛рд▓рд╛рдБрдХрд┐, рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ рд╡рд░реНрдгрд┐рдд рд╡рд┐рдЪрд╛рд░реЛрдВ рдХреЛ JHipster рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реБрдП рднреА рд▓рд╛рдЧреВ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдореБрдЭреЗ рдЖрд╢рд╛ рд╣реИ рдХрд┐ рдЗрд╕ рд╕реНрдерд╛рди рдкрд░ рдЖрдиреЗ рд╡рд╛рд▓реЗ рдкрд╛рдардХреЛрдВ рдХреЛ рд╡рд┐рдЪрд╛рд░ рдХреЗ рд▓рд┐рдП рднреЛрдЬрди рдХреЗ рд░реВрдк рдореЗрдВ рднреА рдпрд╣ рд╕рд╛рдордЧреНрд░реА рдЙрдкрдпреЛрдЧреА рд╣реЛрдЧреАредрдЙрдкрдпреЛрдЧреА рд▓рд┐рдВрдХ