3.1 Лабораторийн зорилго
Энэ лабораторид та:
- Spring Security ашиглан JWT authentication хэрэгжүүлэх
- Role-based authorization тохируулах
- Input validation нэмэх
- Password bcrypt-ээр хадгалах
- CORS, Security Headers тохируулах
Хэл: Java 17+ / Spring Boot 3.x | Framework: Spring Security | Dependency: jjwt, bcrypt
3.2 Лаб 1: Spring Security + JWT Authentication
Алхам 1: Dependency нэмэх (pom.xml)
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Алхам 2: User Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password; // bcrypt hash хадгална
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role; // ADMIN, TEACHER, STUDENT
// Getters, Setters
}
public enum Role {
ADMIN, TEACHER, STUDENT
}
Алхам 3: JWT Service
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expiration; // 86400000 = 24 цаг
// Token үүсгэх
public String generateToken(User user) {
return Jwts.builder()
.subject(user.getEmail())
.claim("role", user.getRole().name())
.claim("name", user.getName())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
// Token-аас email авах
public String extractEmail(String token) {
return extractClaim(token, Claims::getSubject);
}
// Token хүчинтэй эсэх
public boolean isTokenValid(String token, UserDetails userDetails) {
String email = extractEmail(token);
return email.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> resolver) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return resolver.apply(claims);
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
Алхам 4: JWT Authentication Filter
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. Authorization header авах
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 2. Token задлах
String token = authHeader.substring(7);
String email = jwtService.extractEmail(token);
// 3. Authentication тохируулах
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Алхам 5: Auth Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
return ResponseEntity.ok(authService.login(request));
}
}
// Request / Response DTO
public record RegisterRequest(
@NotBlank String name,
@NotBlank @Email String email,
@NotBlank @Size(min = 8) String password
) {}
public record LoginRequest(
@NotBlank @Email String email,
@NotBlank String password
) {}
public record AuthResponse(String token, String email, String role) {}
Алхам 6: Auth Service
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
// Constructor injection...
public AuthResponse register(RegisterRequest request) {
// Email давтагдаагүй эсэх
if (userRepository.existsByEmail(request.email())) {
throw new RuntimeException("Email бүртгэлтэй байна");
}
// User үүсгэх
User user = new User();
user.setName(request.name());
user.setEmail(request.email());
user.setPassword(passwordEncoder.encode(request.password())); // bcrypt!
user.setRole(Role.STUDENT);
userRepository.save(user);
// JWT token үүсгэх
String token = jwtService.generateToken(user);
return new AuthResponse(token, user.getEmail(), user.getRole().name());
}
public AuthResponse login(LoginRequest request) {
// Authentication шалгах
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password())
);
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new RuntimeException("Хэрэглэгч олдсонгүй"));
String token = jwtService.generateToken(user);
return new AuthResponse(token, user.getEmail(), user.getRole().name());
}
}
3.3 Лаб 2: Role-Based Authorization
Алхам 1: Security Config
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
// Constructor injection...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
Алхам 2: Method-Level Security
@RestController
@RequestMapping("/api/students")
public class StudentController {
// Бүгд харах боломжтой
@GetMapping
@PreAuthorize("hasAnyRole('ADMIN', 'TEACHER', 'STUDENT')")
public List<StudentResponse> getAll() { ... }
// Зөвхөн ADMIN, TEACHER нэмж чадна
@PostMapping
@PreAuthorize("hasAnyRole('ADMIN', 'TEACHER')")
public StudentResponse create(@Valid @RequestBody CreateStudentRequest request) { ... }
// Зөвхөн ADMIN устгаж чадна
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void delete(@PathVariable Long id) { ... }
// Өөрийнхөө мэдээлэл эсвэл ADMIN
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @securityService.isOwner(#id, authentication)")
public StudentResponse getById(@PathVariable Long id) { ... }
}
3.4 Лаб 3: Input Validation + Error Handling
Алхам 1: Validation DTO
public class CreateStudentRequest {
@NotBlank(message = "Нэр заавал оруулна")
@Size(min = 2, max = 100, message = "Нэр 2-100 тэмдэгт байна")
private String name;
@NotBlank(message = "Email заавал оруулна")
@Email(message = "Email формат буруу")
private String email;
@NotNull(message = "GPA заавал оруулна")
@DecimalMin(value = "0.0", message = "GPA 0-оос бага байж болохгүй")
@DecimalMax(value = "4.0", message = "GPA 4.0-оос их байж болохгүй")
private BigDecimal gpa;
@NotNull(message = "Тэнхим заавал сонгоно")
private Long departmentId;
// Getters, Setters
}
Алхам 2: Global Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
// Validation алдаа
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "Оролтын алдаа", errors));
}
// Authentication алдаа
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
return ResponseEntity.status(403)
.body(new ErrorResponse(403, "Хандах эрхгүй", null));
}
// Ерөнхий алдаа — Дэлгэрэнгүй мэдээлэл БУЦААХГҮЙ (аюулгүй)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
// Production: Дотоод алдааг хэрэглэгчид ХАРУУЛАХГҮЙ
log.error("Internal error: ", ex);
return ResponseEntity.status(500)
.body(new ErrorResponse(500, "Серверийн алдаа", null));
}
}
public record ErrorResponse(int status, String message, Object errors) {}
⚠️ Чухал: Production-д stack trace, DB алдаа зэрэг дотоод мэдээллийг хэрэглэгчид ХЭЗЭЭ Ч буцаахгүй! Халдагчид ашиглана.
3.5 Лаб 4: CORS + Security Headers + application.yml
Алхам 1: application.yml тохиргоо
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/studentdb
username: ${DB_USERNAME} # ENV variable → Кодонд password байхгүй!
password: ${DB_PASSWORD}
jwt:
secret: ${JWT_SECRET} # ENV variable
expiration: 86400000 # 24 цаг (ms)
server:
error:
include-message: never # Алдааны дэлгэрэнгүй НУУХ
include-stacktrace: never # Stack trace НУУХ
Алхам 2: CORS тохиргоо
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://myapp.com") // Зөвхөн энэ domain!
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(3600);
}
}
Алхам 3: Security Headers
// SecurityConfig дотор
http.headers(headers -> headers
.contentSecurityPolicy(csp ->
csp.policyDirectives("default-src 'self'; script-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts ->
hsts.maxAgeInSeconds(31536000).includeSubDomains(true))
.contentTypeOptions(Customizer.withDefaults())
);
Тест хийх:
# 1. Бүртгүүлэх
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Бат","email":"bat@test.com","password":"Password123!"}'
# Хариу: {"token":"eyJ...","email":"bat@test.com","role":"STUDENT"}
# 2. Нэвтрэх
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"bat@test.com","password":"Password123!"}'
# 3. JWT token-тэй хүсэлт
curl -X GET http://localhost:8080/api/students \
-H "Authorization: Bearer eyJ..."
# 4. Эрхгүй хүсэлт (STUDENT → DELETE)
curl -X DELETE http://localhost:8080/api/students/1 \
-H "Authorization: Bearer eyJ..."
# → 403 Forbidden