Initialize Spring Boot backend with JWT auth

This commit is contained in:
Tim
2025-06-30 17:17:54 +08:00
parent 911f908780
commit f53e3635d0
12 changed files with 464 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
package com.openisle;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OpenIsleApplication {
public static void main(String[] args) {
SpringApplication.run(OpenIsleApplication.class, args);
}
}

View File

@@ -0,0 +1,92 @@
package com.openisle.config;
import com.openisle.service.JwtService;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtService jwtService;
private final UserRepository userRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByUsername(username)
.<UserDetails>map(user -> org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities("USER")
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder)
.and()
.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public OncePerRequestFilter jwtAuthenticationFilter() {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
String username = jwtService.validateAndGetSubject(token);
UserDetails userDetails = userDetailsService().loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authToken);
} catch (Exception ignored) {
}
}
filterChain.doFilter(request, response);
}
};
}
}

View File

@@ -0,0 +1,52 @@
package com.openisle.controller;
import com.openisle.model.User;
import com.openisle.service.EmailService;
import com.openisle.service.JwtService;
import com.openisle.service.UserService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
private final JwtService jwtService;
private final EmailService emailService;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
User user = userService.register(req.getUsername(), req.getEmail(), req.getPassword());
emailService.sendEmail(user.getEmail(), "Welcome to OpenIsle", "Thank you for registering.");
String token = jwtService.generateToken(user.getUsername());
return ResponseEntity.ok(new JwtResponse(token));
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
return userService.authenticate(req.getUsername(), req.getPassword())
.map(user -> ResponseEntity.ok(new JwtResponse(jwtService.generateToken(user.getUsername()))))
.orElse(ResponseEntity.status(401).build());
}
@Data
private static class RegisterRequest {
private String username;
private String email;
private String password;
}
@Data
private static class LoginRequest {
private String username;
private String password;
}
@Data
private static class JwtResponse {
private final String token;
}
}

View File

@@ -0,0 +1,12 @@
package com.openisle.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/api/hello")
public String hello() {
return "Hello, Authenticated User";
}
}

View File

@@ -0,0 +1,26 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.openisle.model.User;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
}

View File

@@ -0,0 +1,38 @@
package com.openisle.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Service
public class EmailService {
@Value("${resend.api.key}")
private String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
public void sendEmail(String to, String subject, String text) {
String url = "https://api.resend.com/emails"; // hypothetical endpoint
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKey);
Map<String, String> body = new HashMap<>();
body.put("to", to);
body.put("subject", subject);
body.put("text", text);
body.put("from", "demo@openisle.example");
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
}
}

View File

@@ -0,0 +1,45 @@
package com.openisle.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration}")
private long expiration;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
public String generateToken(String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String validateAndGetSubject(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}

View File

@@ -0,0 +1,33 @@
package com.openisle.service;
import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public User register(String username, String email, String password) {
if (userRepository.findByUsername(username).isPresent() || userRepository.findByEmail(email).isPresent()) {
throw new RuntimeException("User already exists");
}
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
return userRepository.save(user);
}
public Optional<User> authenticate(String username, String password) {
return userRepository.findByUsername(username)
.filter(user -> passwordEncoder.matches(password, user.getPassword()));
}
}