سیستم ورود با رمز JWT و رمز عبور بازنشانی ایمیل

Summarize this content to 400 words in Persian Lang
مقدمه
این برنامه ورود بهار یک سیستم مدیریت کاربر امن و قوی است که با استفاده از آن ساخته شده است چکمه بهاره. این پروژه رویکردهای مدرن برای اجرای احراز هویت، مجوز، و عملکردهای حساب کاربری را نشان می دهد. ویژگی های کلیدی شامل ثبت نام کاربر، مدیریت رمز عبور ایمن با BCrypt، بازنشانی رمز عبور مبتنی بر ایمیل، و احراز هویت JWT (JSON Web Token) است. این برنامه با در نظر گرفتن توسعه پذیری و مقیاس پذیری طراحی شده است، این برنامه به عنوان یک پایه عالی برای پروژه هایی که نیاز به مدیریت کاربر و کنترل دسترسی مبتنی بر نقش دارند، عمل می کند.
با استفاده از ابزارهای قدرتمند اسپرینگ مانند امنیت بهار، بهار داده JPA، و JavaMailSender، این پروژه بهترین شیوه ها را در زمینه امنیت، قابلیت نگهداری و سهولت ادغام تضمین می کند. چه در حال ساخت یک برنامه وب کوچک یا یک سیستم سازمانی بزرگ باشید، این پروژه نقطه شروع عملی و ساختار یافته ای را برای مدیریت ایمن حساب های کاربری فراهم می کند.
پیکربندی
وابستگی های Pom.xml
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-oauth2-resource-server
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-mail
org.springframework.boot
spring-boot-devtools
runtime
true
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
داکر
برای اجرای پایگاه داده PostgreSQL، a ایجاد کنید docker-compose.yaml فایل:
services:
postgres:
image: postgres:latest
ports:
– “5432:5432”
environment:
– POSTGRES_DB=database
– POSTGRES_USER=admin
– POSTGRES_PASSWORD=admin
volumes:
– postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اجرا کنید:
docker compose up -d
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
کاربرد.خواص
spring.application.name=login_app
spring.datasource.url=jdbc:postgresql://localhost:5432/database
spring.datasource.username=admin
spring.datasource.password=admin
spring.mail.host=sandbox.smtp.mailtrap.io
spring.mail.port=2525
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.default-encoding=UTF-8
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.config.import=classpath:env.properties
jwt.public.key=classpath:public.key
jwt.private.key=classpath:private.key
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
env.properties
spring.mail.username=
spring.mail.password=
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
چگونه یک کلید نامتقارن ایجاد کنیم؟
در این پست نحوه تولید کلیدهای نامتقارن را ببینید
ساختار پروژه
login_app/
├── .mvn/ # Maven folder (Maven configurations)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── dev/
│ │ │ └── mspilari/
│ │ │ └── login_app/
│ │ │ ├── configs/ # Security, authentication, and other configurations
│ │ │ ├── domains/ # Main application domains
│ │ │ │ ├── email/ # Email-related logic
│ │ │ │ └── user/ # User-related logic
│ │ │ ├── exceptions/ # Custom exceptions and error handling
│ │ │ └── utils/ # Utilities and helpers
│ │ └── resources/ # Resources (e.g., configuration files)
│ └── test/ # Application tests
├── target/ # Build folder generated by Maven
├── .gitattributes # Git attributes configuration
├── .gitignore # Git ignore file
├── docker-compose.yaml # Docker Compose configuration
├── HELP.md # Project help documentation
├── mvnw # Maven Wrapper script for Linux
├── mvnw.cmd # Maven Wrapper script for Windows
└── pom.xml # Maven configuration file
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ویژگی ها
ثبت نام کاربر با تایید ایمیل و رمز عبور
با احراز هویت JWT وارد شوید
بازیابی رمز عبور با تحویل لینک ایمیل
بازنشانی رمز عبور از طریق پیوند با رمز موقت
اعتبار سنجی میدانی و مدیریت خطا
کد
دایرکتوری پیکربندی
BCryptPasswordConfig.java
package dev.mspilari.login_app.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class BCryptPasswordConfig {
@Bean
public BCryptPasswordEncoder bPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
شکست کد
@Configuration
این حاشیه به Spring می گوید که کلاس شامل تعاریف bean است.
کلاس های حاشیه نویسی شده با @Configuration در حین راه اندازی برنامه پردازش می شوند و هر روشی با آن حاشیه نویسی می شود @Bean مقادیر بازگشتی آنها به عنوان لوبیاهای مدیریت شده به زمینه برنامه Spring اضافه می شود.
@Bean
این @Bean حاشیه نویسی بر روی bPasswordEncoder() متد نشان می دهد که این متد یک شی را برمی گرداند که باید به عنوان bean در زمینه برنامه Spring ثبت شود.
این اجازه می دهد تا BCryptPasswordEncoder شیء در هر کجا که در برنامه مورد نیاز است تزریق شود.
BCryptPasswordEncoder
این یک کلاس کاربردی است که توسط Spring Security برای رمزگذاری رمزهای عبور ارائه شده است.
از آن استفاده می کند الگوریتم هش BCryptکه راهی قوی و امن برای هش رمزهای عبور به حساب می آید. این الگوریتم قبل از هش کردن رمز عبور به طور خودکار یک “salt” اضافه می کند و آن را در برابر حملات فرهنگ لغت و حملات جدول رنگین کمان مقاوم می کند.
روش bPasswordEncoder()
هنگامی که این متد توسط فریم ورک Spring فراخوانی می شود، یک نمونه جدید از ایجاد می کند BCryptPasswordEncoder و آن را در زمینه برنامه در دسترس قرار می دهد.
سپس سایر کلاسهای این برنامه میتوانند این bean را برای رمزگذاری یا مطابقت رمزهای عبور به صورت خودکار سیمکشی کنند.
JwtConfig.java
package dev.mspilari.login_app.configs;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
@Configuration
public class JwtConfig {
@Value(“${jwt.public.key}”)
private RSAPublicKey publicKey;
@Value(“${jwt.private.key}”)
private RSAPrivateKey privateKey;
@Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.publicKey).build();
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
شکست کد
1. حاشیه نویسی در سطح کلاس
@Configuration
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
نشان می دهد که این یک کلاس پیکربندی Spring است که در آن beans (کامپوننت های مدیریت شده توسط Spring) تعریف شده است.
لوبیاهای تعریف شده در اینجا برای تزریق وابستگی در قسمت Spring Application Context در دسترس خواهند بود.
2. تزریق کلیدهای RSA از پیکربندی
@Value(“${jwt.public.key}”)
private RSAPublicKey publicKey;
@Value(“${jwt.private.key}”)
private RSAPrivateKey privateKey;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
@Value برای تزریق استفاده می شود کلید عمومی و کلید خصوصی از فایل ویژگی های برنامه (به عنوان مثال، application.yml یا application.properties).
انتظار می رود این کلیدها در ویژگی های زیر باشند:
jwt.public.key=
jwt.private.key=
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
3. JWT Encoder Bean
@Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
هدف: یک Bean برای رمزگذاری (تولید) توکن های JWT ایجاد می کند.
مراحل:
کلید RSA را بسازید:
RSAKey.Builder یک نمایش JWK (JSON Web Key) از جفت کلید RSA عمومی/خصوصی ایجاد می کند.
مجموعه JWK را ایجاد کنید:
ImmutableJWKSet کلید را در یک مجموعه ذخیره می کند. این مجموعه توسط کتابخانه های Nimbus JOSE برای امضای توکن ها استفاده می شود.
NimbusJwtEncoder:
این رمزگذار از ImmutableJWKSet برای رمزگذاری و امضای نشانه ها با استفاده از کلید خصوصی.
4. JWT Decoder Bean
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.publicKey).build();
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
هدف: یک Bean برای رمزگشایی و تأیید توکن های JWT ایجاد می کند.
مراحل:
تأیید کلید عمومی:
NimbusJwtDecoder.withPublicKey() با کلید عمومی RSA پیکربندی شده است. امضای توکن ها را تأیید می کند.
ساخت رمزگشا:
این build() متد نمونه رمزگشا را ایجاد می کند.
نحوه کار رمزگذاری و رمزگشایی JWT
رمزگذاری JWT (تولید توکن):
این JwtEncoder bean برای ایجاد یک توکن JWT امضا شده استفاده می شود. این نشانه معمولاً حاوی اطلاعات کاربر (به عنوان مثال، نام کاربری، نقشها و غیره) به عنوان ادعا است و با استفاده از کلید خصوصی RSA امضا میشود.
مثال:
String token = jwtEncoder.encode(JwtClaimsSet.builder().subject(“user”).build()).getTokenValue();
رمزگشایی JWT (تأیید رمز):
این JwtDecoder bean برای رمزگشایی و تأیید توکن با استفاده از کلید عمومی RSA استفاده می شود. این توکن را تضمین می کند:
توسط سرور صادر شد (تأیید امضا).
دستکاری نشده است.
مثال:
Jwt jwt = jwtDecoder.decode(token);
SecurityConfig.java
package dev.mspilari.login_app.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private JwtConfig jwtConfig;
public SecurityConfig(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.POST, “/user/register”).permitAll()
.requestMatchers(HttpMethod.POST, “/user/login”).permitAll()
.requestMatchers(HttpMethod.POST, “/user/redeem-password”).permitAll()
.requestMatchers(HttpMethod.POST, “/user/reset-password”).permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(config -> config.jwt(jwt -> jwt.decoder(jwtConfig.jwtDecoder())));
return http.build();
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
1. حاشیه نویسی در سطح کلاس
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
@Configuration: این کلاس را به عنوان یک پیکربندی Spring که beans را تعریف می کند، علامت گذاری می کند.
@EnableWebSecurity: ویژگی های امنیتی وب Spring Security را فعال می کند.
@EnableMethodSecurity: حاشیه نویسی های امنیتی در سطح روش مانند را فعال می کند @PreAuthorize یا @Secured. این به شما امکان می دهد دسترسی به روش های خاص در برنامه خود را بر اساس نقش ها، مجوزها یا شرایط کنترل کنید.
2. SecurityFilterChain لوبیا
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
را تعریف می کند زنجیره فیلتر امنیتی برای برنامه زنجیره فیلتر دنباله ای از فیلترهای امنیتی است که برای درخواست های HTTP ورودی اعمال می شود.
3. حفاظت CSRF
.csrf(csrf -> csrf.disable())
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
CSRF (جعل درخواست بین سایتی) حفاظت غیرفعال است
حفاظت از CSRF اغلب برای API های بدون حالت غیر ضروری است، زیرا توکن ها (مانند JWT) از قبل راهی برای جلوگیری از درخواست های غیرمجاز ارائه می دهند.
غیرفعال کردن آن پیکربندی امنیتی این API مبتنی بر JWT را ساده می کند.
4. قوانین مجوز
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, “/user/register”).permitAll()
.requestMatchers(HttpMethod.POST, “/user/login”).permitAll()
.requestMatchers(HttpMethod.POST, “/user/redeem-password”).permitAll()
.requestMatchers(HttpMethod.POST, “/user/reset-password”).permitAll()
.anyRequest().authenticated())
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پیکربندی می کند که کدام نقاط پایانی به احراز هویت نیاز دارند:
مجوز همه:
درخواست های POST به نقاط پایانی مانند /user/register، /user/login، /user/redeem-password، و /user/reset-password برای همه باز است (بدون نیاز به احراز هویت).
این نقاط پایانی احتمالاً برای ثبت نام کاربر، ورود به سیستم و بازیابی/تنظیم رمز عبور استفاده می شوند که معمولاً بدون ورود به سیستم قابل دسترسی هستند.
سایر درخواست ها را تأیید کنید:
تمام نقاط پایانی دیگر (anyRequest) احراز هویت نیاز دارد.
5. اعتبارسنجی JWT
.oauth2ResourceServer(config -> config
.jwt(jwt -> jwt.decoder(jwtConfig.jwtDecoder())));
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
برنامه را به صورت یک پیکربندی می کند سرور منبع OAuth 2.0 که درخواست ها را با استفاده از توکن های JWT تایید می کند.
رسیور JWT:
این JwtDecoder لوبیا (ارائه شده توسط JwtConfig) برای تأیید توکن های JWT ورودی برای درخواست برای ایمن سازی نقاط پایانی استفاده می شود.
چگونه این کار می کند
CSRF غیرفعال است: از آنجایی که این یک API است که بر احراز هویت JWT بدون حالت تکیه دارد، غیرفعال کردن CSRF یک روش معمول است.
قوانین مجوز:
کاربران احراز هویت نشده فقط می توانند به نقاط پایانی که صریحاً مجاز هستند دسترسی داشته باشند (به عنوان مثال، /user/register یا /user/login).
هر درخواست دیگری نیاز به یک توکن معتبر JWT دارد.
اعتبار سنجی JWT:
Spring Security به طور خودکار استخراج می کند Authorization سربرگ درخواست های دریافتی
اگر هدر حاوی یک نشانه JWT معتبر باشد، درخواست احراز هویت می شود و زمینه کاربر ایجاد می شود.
اگر رمز نامعتبر یا مفقود باشد، درخواست رد می شود.
دایرکتوری دامنه ها
دایرکتوری ایمیل
دایرکتوری خدمات
package dev.mspilari.login_app.domains.email.services;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private JavaMailSender mailSender;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendEmail(String email, String subject, String body) {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(email);
mail.setFrom(“app_login@email.com”);
mail.setSubject(subject);
mail.setText(body);
mailSender.send(mail);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دایرکتوری کاربر
دایرکتوری کنترلرها
package dev.mspilari.login_app.domains.user.controllers;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.mspilari.login_app.domains.user.dto.UserDto;
import dev.mspilari.login_app.domains.user.dto.UserRedeemPasswordDto;
import dev.mspilari.login_app.domains.user.dto.UserResetPasswordDto;
import dev.mspilari.login_app.domains.user.services.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping(“user”)
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping(“/login”)
public ResponseEntity<Map<String, String>> login(@RequestBody @Valid UserDto userDto) {
var token = userService.login(userDto.email(), userDto.password());
return ResponseEntity.ok().body(Map.of(“token”, token));
}
@PostMapping(“/register”)
public ResponseEntity<Map<String, String>> register(@RequestBody @Valid UserDto userDto) {
userService.createUser(userDto.email(), userDto.password());
return ResponseEntity.ok().body(Map.of(“message”, “User created successfully”));
}
@PostMapping(“/redeem-password”)
public ResponseEntity<Map<String, String>> redeemPassword(@RequestBody @Valid UserRedeemPasswordDto userDto) {
userService.redeemPassword(userDto.email());
return ResponseEntity.ok().body(Map.of(“message”, “Send the redeem password link to your email”));
}
@PostMapping(“/reset-password”)
public ResponseEntity<Map<String, String>> resetPassword(@RequestBody @Valid UserResetPasswordDto userDto) {
userService.resetPassword(userDto.token(), userDto.password());
return ResponseEntity.ok().body(Map.of(“message”, “Credentials updated”));
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دایرکتوری DTO
UserDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record UserDto(@Email @NotBlank String email, @Min(4) @NotBlank String password) {
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
UserRedeemPasswordDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Email;
public record UserRedeemPasswordDto(@Email String email) {
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
UserResetPasswordDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record UserResetPasswordDto(@NotBlank String token, @NotBlank @Min(4) String password) {
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
فهرست نهاد
UserEntity.java
package dev.mspilari.login_app.domains.user.entity;
import java.time.Instant;
import java.util.UUID;
import dev.mspilari.login_app.domains.user.enums.Role;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = “tb_users”)
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String email;
private String password;
private String resetToken;
private Instant resetTokenExpiration;
@Enumerated(EnumType.STRING)
private Role role;
public UserEntity(String email, String password, Role role) {
this.email = email;
this.password = password;
this.role = role;
}
public UserEntity() {
}
public UserEntity withResetToken(String resetToken, Instant resetTokenAdditionalTime) {
this.resetToken = resetToken;
this.resetTokenExpiration = resetTokenAdditionalTime;
return this;
}
public UUID getId() {
return id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public Instant getResetTokenExpiration() {
return resetTokenExpiration;
}
public void setResetTokenExpiration(Instant resetTokenExpiration) {
this.resetTokenExpiration = resetTokenExpiration;
}
public String getResetToken() {
return resetToken;
}
public void setResetToken(String resetToken) {
this.resetToken = resetToken;
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دایرکتوری Enums
Role.java
package dev.mspilari.login_app.domains.user.enums;
public enum Role {
ADMIN,
CLIENT
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دایرکتوری مخازن
UserRepository.java
package dev.mspilari.login_app.domains.user.repositories;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import dev.mspilari.login_app.domains.user.entity.UserEntity;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByResetToken(String resetToken);
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دایرکتوری خدمات
UserService.java
package dev.mspilari.login_app.domains.user.services;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import dev.mspilari.login_app.domains.email.services.EmailService;
import dev.mspilari.login_app.domains.user.entity.UserEntity;
import dev.mspilari.login_app.domains.user.enums.Role;
import dev.mspilari.login_app.domains.user.repositories.UserRepository;
import dev.mspilari.login_app.utils.JwtActions;
@Service
public class UserService {
@Value(“${token.expiration.seconds:300}”)
private Long tokenExpirationSeconds;
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtActions jwtActions;
private final EmailService emailService;
public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, JwtActions jwtActions,
EmailService emailService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtActions = jwtActions;
this.emailService = emailService;
}
private Optional<UserEntity> findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
private boolean verifyPassword(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
private void sendPasswordResetEmail(String email, String token) {
String subject = “Password Reset Request”;
String resetUrl = “https://seusite.com/reset?token=” + token;
String body = “Click the link to reset your password: ” + resetUrl;
// Implemente o serviço de e-mail conforme necessário
emailService.sendEmail(email, subject, body);
}
public void createUser(String email, String password) {
if (findUserByEmail(email).isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, “Email already exists !”);
}
var encodedPassword = passwordEncoder.encode(password);
var newUser = new UserEntity(email, encodedPassword, Role.CLIENT);
userRepository.save(newUser);
}
public String login(String email, String password) {
var user = findUserByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
“Invalid login credentials”));
if (!verifyPassword(password, user.getPassword())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, “Invalid login credentials”);
}
return jwtActions.jwtCreate(user.getEmail(), user.getRole().toString());
}
public void redeemPassword(String email) {
var user = findUserByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
“Invalid email”));
var token = UUID.randomUUID().toString();
user.withResetToken(token, Instant.now().plusSeconds(this.tokenExpirationSeconds));
userRepository.save(user);
sendPasswordResetEmail(user.getEmail(), token);
}
public void resetPassword(String token, String password) {
var user = userRepository.findByResetToken(token)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
“User not found”));
if (user.getResetTokenExpiration().isBefore(Instant.now())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, “Token expired”);
}
user.setPassword(passwordEncoder.encode(password));
user.withResetToken(null, null);
userRepository.save(user);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
فهرست استثنائات
GlobalException.java
package dev.mspilari.login_app.exceptions;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> defaultExceptionHandler(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(“error”, e.getMessage()));
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, String>> responseStatusExceptionHandler(ResponseStatusException e) {
var error = Map.of(“errorMessage”, e.getReason(), “errorStatusCode”, e.getStatusCode().toString());
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> validationErrorHandler(MethodArgumentNotValidException e) {
var errors = new HashMap<String, String>();
for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.status(e.getStatusCode()).body(errors);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دایرکتوری Utils
JwtActions.java
package dev.mspilari.login_app.utils;
import java.time.Instant;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import dev.mspilari.login_app.configs.JwtConfig;
@Service
public class JwtActions {
@Value(“${jwt.expiration:300}”)
private Long jwtExpiration;
private final JwtConfig jwtConfig;
public JwtActions(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
public String jwtCreate(String email, String role) {
var now = Instant.now();
var claims = JwtClaimsSet.builder()
.issuer(“login_app”)
.subject(email)
.issuedAt(now)
.expiresAt(now.plusSeconds(jwtExpiration))
.claim(“scope”, role)
.build();
return jwtConfig.jwtEncoder().encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
نتیجه گیری
در این پروژه، ما با استفاده از Spring Boot یک سیستم احراز هویت کاربر امن و غنی از ویژگی ها را با موفقیت پیاده سازی کردیم. فراتر از عملکردهای اصلی مانند ثبت نام کاربر، ورود به سیستم و احراز هویت مبتنی بر JWT، برنامه همچنین دارای یک سیستم بازیابی رمز عبور است. کاربران می توانند رمزهای عبور خود را از طریق پیوند ایمیل بازنشانی کنند و از روند بازیابی روان و ایمن اطمینان حاصل کنند.
برای تسهیل بازیابی رمز عبور مبتنی بر ایمیل، ما ادغام کردیم ایمیل بهار با تله پستی، یک سرویس تست ایمیل ایمن و کارآمد. این به برنامه اجازه میدهد تا پیوندهای بازنشانی رمز عبور را با توکنهای موقت ارسال کند و در عین حال اطمینان حاصل کند که ایمیلها به صورت ایمن ارسال شده و در یک محیط کنترلشده آزمایش میشوند. این راهاندازی نشان میدهد که چگونه میتوان جریانهای کاری حساس مانند بازیابی رمز عبور را بدون قرار دادن کاربران واقعی در معرض مشکلات احتمالی در طول توسعه و آزمایش، مدیریت کرد.
ترکیبی از روشهای احراز هویت امن، مدیریت رمز عبور قوی، و یکپارچهسازی یکپارچه ایمیل، این برنامه را به یک پایه قابل اعتماد برای هر سیستم وب مدرن تبدیل میکند. توسعه دهندگان می توانند این شیوه ها را مطابق با نیازهای خاص خود تطبیق دهند و از مقیاس پذیری و اعتماد کاربر اطمینان حاصل کنند. با استفاده از بهترین روشها و ابزارهایی مانند Spring Security و Mailtrap، ما نشان دادهایم که چگونه برنامههای کاربردی ایمن و متمرکز بر کاربر را به راحتی بسازیم.
📍 مرجع
💻 مخزن پروژه
👋 با من صحبت کن
مقدمه
این برنامه ورود بهار یک سیستم مدیریت کاربر امن و قوی است که با استفاده از آن ساخته شده است چکمه بهاره. این پروژه رویکردهای مدرن برای اجرای احراز هویت، مجوز، و عملکردهای حساب کاربری را نشان می دهد. ویژگی های کلیدی شامل ثبت نام کاربر، مدیریت رمز عبور ایمن با BCrypt، بازنشانی رمز عبور مبتنی بر ایمیل، و احراز هویت JWT (JSON Web Token) است. این برنامه با در نظر گرفتن توسعه پذیری و مقیاس پذیری طراحی شده است، این برنامه به عنوان یک پایه عالی برای پروژه هایی که نیاز به مدیریت کاربر و کنترل دسترسی مبتنی بر نقش دارند، عمل می کند.
با استفاده از ابزارهای قدرتمند اسپرینگ مانند امنیت بهار، بهار داده JPA، و JavaMailSender، این پروژه بهترین شیوه ها را در زمینه امنیت، قابلیت نگهداری و سهولت ادغام تضمین می کند. چه در حال ساخت یک برنامه وب کوچک یا یک سیستم سازمانی بزرگ باشید، این پروژه نقطه شروع عملی و ساختار یافته ای را برای مدیریت ایمن حساب های کاربری فراهم می کند.
پیکربندی
وابستگی های Pom.xml
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-oauth2-resource-server
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-mail
org.springframework.boot
spring-boot-devtools
runtime
true
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
داکر
برای اجرای پایگاه داده PostgreSQL، a ایجاد کنید docker-compose.yaml
فایل:
services:
postgres:
image: postgres:latest
ports:
- "5432:5432"
environment:
- POSTGRES_DB=database
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
اجرا کنید:
docker compose up -d
کاربرد.خواص
spring.application.name=login_app
spring.datasource.url=jdbc:postgresql://localhost:5432/database
spring.datasource.username=admin
spring.datasource.password=admin
spring.mail.host=sandbox.smtp.mailtrap.io
spring.mail.port=2525
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.default-encoding=UTF-8
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.config.import=classpath:env.properties
jwt.public.key=classpath:public.key
jwt.private.key=classpath:private.key
env.properties
spring.mail.username=
spring.mail.password=
چگونه یک کلید نامتقارن ایجاد کنیم؟
در این پست نحوه تولید کلیدهای نامتقارن را ببینید
ساختار پروژه
login_app/
├── .mvn/ # Maven folder (Maven configurations)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── dev/
│ │ │ └── mspilari/
│ │ │ └── login_app/
│ │ │ ├── configs/ # Security, authentication, and other configurations
│ │ │ ├── domains/ # Main application domains
│ │ │ │ ├── email/ # Email-related logic
│ │ │ │ └── user/ # User-related logic
│ │ │ ├── exceptions/ # Custom exceptions and error handling
│ │ │ └── utils/ # Utilities and helpers
│ │ └── resources/ # Resources (e.g., configuration files)
│ └── test/ # Application tests
├── target/ # Build folder generated by Maven
├── .gitattributes # Git attributes configuration
├── .gitignore # Git ignore file
├── docker-compose.yaml # Docker Compose configuration
├── HELP.md # Project help documentation
├── mvnw # Maven Wrapper script for Linux
├── mvnw.cmd # Maven Wrapper script for Windows
└── pom.xml # Maven configuration file
ویژگی ها
- ثبت نام کاربر با تایید ایمیل و رمز عبور
- با احراز هویت JWT وارد شوید
- بازیابی رمز عبور با تحویل لینک ایمیل
- بازنشانی رمز عبور از طریق پیوند با رمز موقت
- اعتبار سنجی میدانی و مدیریت خطا
کد
دایرکتوری پیکربندی
BCryptPasswordConfig.java
package dev.mspilari.login_app.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class BCryptPasswordConfig {
@Bean
public BCryptPasswordEncoder bPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
شکست کد
-
@Configuration
- این حاشیه به Spring می گوید که کلاس شامل تعاریف bean است.
- کلاس های حاشیه نویسی شده با
@Configuration
در حین راه اندازی برنامه پردازش می شوند و هر روشی با آن حاشیه نویسی می شود@Bean
مقادیر بازگشتی آنها به عنوان لوبیاهای مدیریت شده به زمینه برنامه Spring اضافه می شود.
-
@Bean
- این
@Bean
حاشیه نویسی بر رویbPasswordEncoder()
متد نشان می دهد که این متد یک شی را برمی گرداند که باید به عنوان bean در زمینه برنامه Spring ثبت شود. - این اجازه می دهد تا BCryptPasswordEncoder شیء در هر کجا که در برنامه مورد نیاز است تزریق شود.
- این
-
BCryptPasswordEncoder
- این یک کلاس کاربردی است که توسط Spring Security برای رمزگذاری رمزهای عبور ارائه شده است.
- از آن استفاده می کند الگوریتم هش BCryptکه راهی قوی و امن برای هش رمزهای عبور به حساب می آید. این الگوریتم قبل از هش کردن رمز عبور به طور خودکار یک “salt” اضافه می کند و آن را در برابر حملات فرهنگ لغت و حملات جدول رنگین کمان مقاوم می کند.
-
روش
bPasswordEncoder()
- هنگامی که این متد توسط فریم ورک Spring فراخوانی می شود، یک نمونه جدید از ایجاد می کند
BCryptPasswordEncoder
و آن را در زمینه برنامه در دسترس قرار می دهد. - سپس سایر کلاسهای این برنامه میتوانند این bean را برای رمزگذاری یا مطابقت رمزهای عبور به صورت خودکار سیمکشی کنند.
- هنگامی که این متد توسط فریم ورک Spring فراخوانی می شود، یک نمونه جدید از ایجاد می کند
JwtConfig.java
package dev.mspilari.login_app.configs;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
@Configuration
public class JwtConfig {
@Value("${jwt.public.key}")
private RSAPublicKey publicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;
@Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.publicKey).build();
}
}
شکست کد
1. حاشیه نویسی در سطح کلاس
@Configuration
- نشان می دهد که این یک کلاس پیکربندی Spring است که در آن beans (کامپوننت های مدیریت شده توسط Spring) تعریف شده است.
- لوبیاهای تعریف شده در اینجا برای تزریق وابستگی در قسمت Spring Application Context در دسترس خواهند بود.
2. تزریق کلیدهای RSA از پیکربندی
@Value("${jwt.public.key}")
private RSAPublicKey publicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;
-
@Value
برای تزریق استفاده می شود کلید عمومی و کلید خصوصی از فایل ویژگی های برنامه (به عنوان مثال،application.yml
یاapplication.properties
). - انتظار می رود این کلیدها در ویژگی های زیر باشند:
jwt.public.key=
jwt.private.key=
3. JWT Encoder Bean
@Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
- هدف: یک Bean برای رمزگذاری (تولید) توکن های JWT ایجاد می کند.
-
مراحل:
-
کلید RSA را بسازید:
-
RSAKey.Builder
یک نمایش JWK (JSON Web Key) از جفت کلید RSA عمومی/خصوصی ایجاد می کند.
-
-
مجموعه JWK را ایجاد کنید:
-
ImmutableJWKSet
کلید را در یک مجموعه ذخیره می کند. این مجموعه توسط کتابخانه های Nimbus JOSE برای امضای توکن ها استفاده می شود.
-
-
NimbusJwtEncoder:
- این رمزگذار از
ImmutableJWKSet
برای رمزگذاری و امضای نشانه ها با استفاده از کلید خصوصی.
- این رمزگذار از
-
کلید RSA را بسازید:
4. JWT Decoder Bean
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.publicKey).build();
}
- هدف: یک Bean برای رمزگشایی و تأیید توکن های JWT ایجاد می کند.
-
مراحل:
-
تأیید کلید عمومی:
-
NimbusJwtDecoder.withPublicKey()
با کلید عمومی RSA پیکربندی شده است. امضای توکن ها را تأیید می کند.
-
-
ساخت رمزگشا:
- این
build()
متد نمونه رمزگشا را ایجاد می کند.
- این
-
تأیید کلید عمومی:
نحوه کار رمزگذاری و رمزگشایی JWT
-
رمزگذاری JWT (تولید توکن):
- این
JwtEncoder
bean برای ایجاد یک توکن JWT امضا شده استفاده می شود. این نشانه معمولاً حاوی اطلاعات کاربر (به عنوان مثال، نام کاربری، نقشها و غیره) به عنوان ادعا است و با استفاده از کلید خصوصی RSA امضا میشود. - مثال:
String token = jwtEncoder.encode(JwtClaimsSet.builder().subject("user").build()).getTokenValue();
- این
-
رمزگشایی JWT (تأیید رمز):
- این
JwtDecoder
bean برای رمزگشایی و تأیید توکن با استفاده از کلید عمومی RSA استفاده می شود. این توکن را تضمین می کند:- توسط سرور صادر شد (تأیید امضا).
- دستکاری نشده است.
- مثال:
Jwt jwt = jwtDecoder.decode(token);
- این
SecurityConfig.java
package dev.mspilari.login_app.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private JwtConfig jwtConfig;
public SecurityConfig(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.POST, "/user/register").permitAll()
.requestMatchers(HttpMethod.POST, "/user/login").permitAll()
.requestMatchers(HttpMethod.POST, "/user/redeem-password").permitAll()
.requestMatchers(HttpMethod.POST, "/user/reset-password").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(config -> config.jwt(jwt -> jwt.decoder(jwtConfig.jwtDecoder())));
return http.build();
}
}
1. حاشیه نویسی در سطح کلاس
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
-
@Configuration
: این کلاس را به عنوان یک پیکربندی Spring که beans را تعریف می کند، علامت گذاری می کند. -
@EnableWebSecurity
: ویژگی های امنیتی وب Spring Security را فعال می کند. -
@EnableMethodSecurity
: حاشیه نویسی های امنیتی در سطح روش مانند را فعال می کند@PreAuthorize
یا@Secured
. این به شما امکان می دهد دسترسی به روش های خاص در برنامه خود را بر اساس نقش ها، مجوزها یا شرایط کنترل کنید.
2. SecurityFilterChain
لوبیا
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- را تعریف می کند زنجیره فیلتر امنیتی برای برنامه زنجیره فیلتر دنباله ای از فیلترهای امنیتی است که برای درخواست های HTTP ورودی اعمال می شود.
3. حفاظت CSRF
.csrf(csrf -> csrf.disable())
-
CSRF (جعل درخواست بین سایتی) حفاظت غیرفعال است
- حفاظت از CSRF اغلب برای API های بدون حالت غیر ضروری است، زیرا توکن ها (مانند JWT) از قبل راهی برای جلوگیری از درخواست های غیرمجاز ارائه می دهند.
- غیرفعال کردن آن پیکربندی امنیتی این API مبتنی بر JWT را ساده می کند.
4. قوانین مجوز
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/user/register").permitAll()
.requestMatchers(HttpMethod.POST, "/user/login").permitAll()
.requestMatchers(HttpMethod.POST, "/user/redeem-password").permitAll()
.requestMatchers(HttpMethod.POST, "/user/reset-password").permitAll()
.anyRequest().authenticated())
- پیکربندی می کند که کدام نقاط پایانی به احراز هویت نیاز دارند:
- مجوز همه:
- درخواست های POST به نقاط پایانی مانند
/user/register
،/user/login
،/user/redeem-password
، و/user/reset-password
برای همه باز است (بدون نیاز به احراز هویت). - این نقاط پایانی احتمالاً برای ثبت نام کاربر، ورود به سیستم و بازیابی/تنظیم رمز عبور استفاده می شوند که معمولاً بدون ورود به سیستم قابل دسترسی هستند.
- سایر درخواست ها را تأیید کنید:
- تمام نقاط پایانی دیگر (
anyRequest
) احراز هویت نیاز دارد.
5. اعتبارسنجی JWT
.oauth2ResourceServer(config -> config
.jwt(jwt -> jwt.decoder(jwtConfig.jwtDecoder())));
- برنامه را به صورت یک پیکربندی می کند سرور منبع OAuth 2.0 که درخواست ها را با استفاده از توکن های JWT تایید می کند.
-
رسیور JWT:
- این
JwtDecoder
لوبیا (ارائه شده توسطJwtConfig
) برای تأیید توکن های JWT ورودی برای درخواست برای ایمن سازی نقاط پایانی استفاده می شود.
- این
چگونه این کار می کند
- CSRF غیرفعال است: از آنجایی که این یک API است که بر احراز هویت JWT بدون حالت تکیه دارد، غیرفعال کردن CSRF یک روش معمول است.
-
قوانین مجوز:
- کاربران احراز هویت نشده فقط می توانند به نقاط پایانی که صریحاً مجاز هستند دسترسی داشته باشند (به عنوان مثال،
/user/register
یا/user/login
). - هر درخواست دیگری نیاز به یک توکن معتبر JWT دارد.
- کاربران احراز هویت نشده فقط می توانند به نقاط پایانی که صریحاً مجاز هستند دسترسی داشته باشند (به عنوان مثال،
-
اعتبار سنجی JWT:
- Spring Security به طور خودکار استخراج می کند
Authorization
سربرگ درخواست های دریافتی - اگر هدر حاوی یک نشانه JWT معتبر باشد، درخواست احراز هویت می شود و زمینه کاربر ایجاد می شود.
- اگر رمز نامعتبر یا مفقود باشد، درخواست رد می شود.
- Spring Security به طور خودکار استخراج می کند
دایرکتوری دامنه ها
دایرکتوری ایمیل
دایرکتوری خدمات
package dev.mspilari.login_app.domains.email.services;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private JavaMailSender mailSender;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendEmail(String email, String subject, String body) {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(email);
mail.setFrom("app_login@email.com");
mail.setSubject(subject);
mail.setText(body);
mailSender.send(mail);
}
}
دایرکتوری کاربر
دایرکتوری کنترلرها
package dev.mspilari.login_app.domains.user.controllers;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.mspilari.login_app.domains.user.dto.UserDto;
import dev.mspilari.login_app.domains.user.dto.UserRedeemPasswordDto;
import dev.mspilari.login_app.domains.user.dto.UserResetPasswordDto;
import dev.mspilari.login_app.domains.user.services.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("user")
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody @Valid UserDto userDto) {
var token = userService.login(userDto.email(), userDto.password());
return ResponseEntity.ok().body(Map.of("token", token));
}
@PostMapping("/register")
public ResponseEntity<Map<String, String>> register(@RequestBody @Valid UserDto userDto) {
userService.createUser(userDto.email(), userDto.password());
return ResponseEntity.ok().body(Map.of("message", "User created successfully"));
}
@PostMapping("/redeem-password")
public ResponseEntity<Map<String, String>> redeemPassword(@RequestBody @Valid UserRedeemPasswordDto userDto) {
userService.redeemPassword(userDto.email());
return ResponseEntity.ok().body(Map.of("message", "Send the redeem password link to your email"));
}
@PostMapping("/reset-password")
public ResponseEntity<Map<String, String>> resetPassword(@RequestBody @Valid UserResetPasswordDto userDto) {
userService.resetPassword(userDto.token(), userDto.password());
return ResponseEntity.ok().body(Map.of("message", "Credentials updated"));
}
}
دایرکتوری DTO
UserDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record UserDto(@Email @NotBlank String email, @Min(4) @NotBlank String password) {
}
UserRedeemPasswordDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Email;
public record UserRedeemPasswordDto(@Email String email) {
}
UserResetPasswordDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record UserResetPasswordDto(@NotBlank String token, @NotBlank @Min(4) String password) {
}
فهرست نهاد
UserEntity.java
package dev.mspilari.login_app.domains.user.entity;
import java.time.Instant;
import java.util.UUID;
import dev.mspilari.login_app.domains.user.enums.Role;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "tb_users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String email;
private String password;
private String resetToken;
private Instant resetTokenExpiration;
@Enumerated(EnumType.STRING)
private Role role;
public UserEntity(String email, String password, Role role) {
this.email = email;
this.password = password;
this.role = role;
}
public UserEntity() {
}
public UserEntity withResetToken(String resetToken, Instant resetTokenAdditionalTime) {
this.resetToken = resetToken;
this.resetTokenExpiration = resetTokenAdditionalTime;
return this;
}
public UUID getId() {
return id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public Instant getResetTokenExpiration() {
return resetTokenExpiration;
}
public void setResetTokenExpiration(Instant resetTokenExpiration) {
this.resetTokenExpiration = resetTokenExpiration;
}
public String getResetToken() {
return resetToken;
}
public void setResetToken(String resetToken) {
this.resetToken = resetToken;
}
}
دایرکتوری Enums
Role.java
package dev.mspilari.login_app.domains.user.enums;
public enum Role {
ADMIN,
CLIENT
}
دایرکتوری مخازن
UserRepository.java
package dev.mspilari.login_app.domains.user.repositories;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import dev.mspilari.login_app.domains.user.entity.UserEntity;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByResetToken(String resetToken);
}
دایرکتوری خدمات
UserService.java
package dev.mspilari.login_app.domains.user.services;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import dev.mspilari.login_app.domains.email.services.EmailService;
import dev.mspilari.login_app.domains.user.entity.UserEntity;
import dev.mspilari.login_app.domains.user.enums.Role;
import dev.mspilari.login_app.domains.user.repositories.UserRepository;
import dev.mspilari.login_app.utils.JwtActions;
@Service
public class UserService {
@Value("${token.expiration.seconds:300}")
private Long tokenExpirationSeconds;
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtActions jwtActions;
private final EmailService emailService;
public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, JwtActions jwtActions,
EmailService emailService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtActions = jwtActions;
this.emailService = emailService;
}
private Optional<UserEntity> findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
private boolean verifyPassword(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
private void sendPasswordResetEmail(String email, String token) {
String subject = "Password Reset Request";
String resetUrl = "https://seusite.com/reset?token=" + token;
String body = "Click the link to reset your password: " + resetUrl;
// Implemente o serviço de e-mail conforme necessário
emailService.sendEmail(email, subject, body);
}
public void createUser(String email, String password) {
if (findUserByEmail(email).isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email already exists !");
}
var encodedPassword = passwordEncoder.encode(password);
var newUser = new UserEntity(email, encodedPassword, Role.CLIENT);
userRepository.save(newUser);
}
public String login(String email, String password) {
var user = findUserByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid login credentials"));
if (!verifyPassword(password, user.getPassword())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid login credentials");
}
return jwtActions.jwtCreate(user.getEmail(), user.getRole().toString());
}
public void redeemPassword(String email) {
var user = findUserByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid email"));
var token = UUID.randomUUID().toString();
user.withResetToken(token, Instant.now().plusSeconds(this.tokenExpirationSeconds));
userRepository.save(user);
sendPasswordResetEmail(user.getEmail(), token);
}
public void resetPassword(String token, String password) {
var user = userRepository.findByResetToken(token)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
"User not found"));
if (user.getResetTokenExpiration().isBefore(Instant.now())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Token expired");
}
user.setPassword(passwordEncoder.encode(password));
user.withResetToken(null, null);
userRepository.save(user);
}
}
فهرست استثنائات
GlobalException.java
package dev.mspilari.login_app.exceptions;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> defaultExceptionHandler(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage()));
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, String>> responseStatusExceptionHandler(ResponseStatusException e) {
var error = Map.of("errorMessage", e.getReason(), "errorStatusCode", e.getStatusCode().toString());
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> validationErrorHandler(MethodArgumentNotValidException e) {
var errors = new HashMap<String, String>();
for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.status(e.getStatusCode()).body(errors);
}
}
دایرکتوری Utils
JwtActions.java
package dev.mspilari.login_app.utils;
import java.time.Instant;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import dev.mspilari.login_app.configs.JwtConfig;
@Service
public class JwtActions {
@Value("${jwt.expiration:300}")
private Long jwtExpiration;
private final JwtConfig jwtConfig;
public JwtActions(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
public String jwtCreate(String email, String role) {
var now = Instant.now();
var claims = JwtClaimsSet.builder()
.issuer("login_app")
.subject(email)
.issuedAt(now)
.expiresAt(now.plusSeconds(jwtExpiration))
.claim("scope", role)
.build();
return jwtConfig.jwtEncoder().encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
نتیجه گیری
در این پروژه، ما با استفاده از Spring Boot یک سیستم احراز هویت کاربر امن و غنی از ویژگی ها را با موفقیت پیاده سازی کردیم. فراتر از عملکردهای اصلی مانند ثبت نام کاربر، ورود به سیستم و احراز هویت مبتنی بر JWT، برنامه همچنین دارای یک سیستم بازیابی رمز عبور است. کاربران می توانند رمزهای عبور خود را از طریق پیوند ایمیل بازنشانی کنند و از روند بازیابی روان و ایمن اطمینان حاصل کنند.
برای تسهیل بازیابی رمز عبور مبتنی بر ایمیل، ما ادغام کردیم ایمیل بهار با تله پستی، یک سرویس تست ایمیل ایمن و کارآمد. این به برنامه اجازه میدهد تا پیوندهای بازنشانی رمز عبور را با توکنهای موقت ارسال کند و در عین حال اطمینان حاصل کند که ایمیلها به صورت ایمن ارسال شده و در یک محیط کنترلشده آزمایش میشوند. این راهاندازی نشان میدهد که چگونه میتوان جریانهای کاری حساس مانند بازیابی رمز عبور را بدون قرار دادن کاربران واقعی در معرض مشکلات احتمالی در طول توسعه و آزمایش، مدیریت کرد.
ترکیبی از روشهای احراز هویت امن، مدیریت رمز عبور قوی، و یکپارچهسازی یکپارچه ایمیل، این برنامه را به یک پایه قابل اعتماد برای هر سیستم وب مدرن تبدیل میکند. توسعه دهندگان می توانند این شیوه ها را مطابق با نیازهای خاص خود تطبیق دهند و از مقیاس پذیری و اعتماد کاربر اطمینان حاصل کنند. با استفاده از بهترین روشها و ابزارهایی مانند Spring Security و Mailtrap، ما نشان دادهایم که چگونه برنامههای کاربردی ایمن و متمرکز بر کاربر را به راحتی بسازیم.
📍 مرجع
💻 مخزن پروژه
👋 با من صحبت کن