diff --git a/pom.xml b/pom.xml
index fc5bfeac..8112404d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,23 @@
spring-security-test
test
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.5
+ runtime
+
diff --git a/src/main/java/com/example/bankapp/config/JwtAuthenticationFilter.java b/src/main/java/com/example/bankapp/config/JwtAuthenticationFilter.java
new file mode 100644
index 00000000..12ed3515
--- /dev/null
+++ b/src/main/java/com/example/bankapp/config/JwtAuthenticationFilter.java
@@ -0,0 +1,55 @@
+package com.example.bankapp.config;
+
+import com.example.bankapp.service.AccountService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtUtil jwtUtil;
+ private final AccountService accountService;
+
+ public JwtAuthenticationFilter(JwtUtil jwtUtil, AccountService accountService) {
+ this.jwtUtil = jwtUtil;
+ this.accountService = accountService;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ String token = null;
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if ("jwt".equals(cookie.getName())) {
+ token = cookie.getValue();
+ break;
+ }
+ }
+ }
+
+ if (token != null && jwtUtil.validateToken(token)) {
+ String username = jwtUtil.extractUsername(token);
+ UserDetails userDetails = accountService.loadUserByUsername(username);
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/com/example/bankapp/config/JwtUtil.java b/src/main/java/com/example/bankapp/config/JwtUtil.java
new file mode 100644
index 00000000..3a3c4668
--- /dev/null
+++ b/src/main/java/com/example/bankapp/config/JwtUtil.java
@@ -0,0 +1,59 @@
+package com.example.bankapp.config;
+
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Base64;
+import java.util.Date;
+
+@Component
+public class JwtUtil {
+
+ private final SecretKey key;
+ private final long expirationMs;
+
+ public JwtUtil(@Value("${jwt.secret}") String secret,
+ @Value("${jwt.expiration-ms}") long expirationMs) {
+ this.key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret));
+ this.expirationMs = expirationMs;
+ }
+
+ public String generateToken(String username) {
+ Date now = new Date();
+ Date expiry = new Date(now.getTime() + expirationMs);
+ return Jwts.builder()
+ .subject(username)
+ .issuedAt(now)
+ .expiration(expiry)
+ .signWith(key)
+ .compact();
+ }
+
+ public String extractUsername(String token) {
+ return Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload()
+ .getSubject();
+ }
+
+ public boolean validateToken(String token) {
+ try {
+ Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public long getExpirationMs() {
+ return expirationMs;
+ }
+}
diff --git a/src/main/java/com/example/bankapp/config/SecurityConfig.java b/src/main/java/com/example/bankapp/config/SecurityConfig.java
index 4dbd1572..1f93cac7 100644
--- a/src/main/java/com/example/bankapp/config/SecurityConfig.java
+++ b/src/main/java/com/example/bankapp/config/SecurityConfig.java
@@ -4,13 +4,16 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
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.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@@ -19,31 +22,33 @@ public class SecurityConfig {
@Autowired
AccountService accountService;
+ @Autowired
+ JwtAuthenticationFilter jwtAuthenticationFilter;
+
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
+ return authConfig.getAuthenticationManager();
+ }
+
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
- .requestMatchers("/register").permitAll()
+ .requestMatchers("/login", "/register", "/logout", "/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated()
)
- .formLogin(form -> form
- .loginPage("/login")
- .loginProcessingUrl("/login")
- .defaultSuccessUrl("/dashboard", true)
- .permitAll()
- )
- .logout(logout -> logout
- .invalidateHttpSession(true)
- .clearAuthentication(true)
- .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
- .logoutSuccessUrl("/login?logout")
- .permitAll()
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+ .logout(logout -> logout.disable())
+ .exceptionHandling(ex -> ex
+ .authenticationEntryPoint((request, response, authException) ->
+ response.sendRedirect("/login"))
)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.sameOrigin())
@@ -55,6 +60,5 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(accountService).passwordEncoder(passwordEncoder());
-
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/example/bankapp/controller/AuthController.java b/src/main/java/com/example/bankapp/controller/AuthController.java
new file mode 100644
index 00000000..bd1cbc10
--- /dev/null
+++ b/src/main/java/com/example/bankapp/controller/AuthController.java
@@ -0,0 +1,69 @@
+package com.example.bankapp.controller;
+
+import com.example.bankapp.config.JwtUtil;
+import com.example.bankapp.service.AccountService;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@Controller
+public class AuthController {
+
+ private final AuthenticationManager authenticationManager;
+ private final JwtUtil jwtUtil;
+
+ public AuthController(AuthenticationManager authenticationManager,
+ JwtUtil jwtUtil) {
+ this.authenticationManager = authenticationManager;
+ this.jwtUtil = jwtUtil;
+ }
+
+ @GetMapping("/login")
+ public String loginPage() {
+ return "login";
+ }
+
+ @PostMapping("/login")
+ public String login(@RequestParam String username,
+ @RequestParam String password,
+ HttpServletResponse response) {
+ try {
+ authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(username, password));
+ String token = jwtUtil.generateToken(username);
+ Cookie cookie = new Cookie("jwt", token);
+ cookie.setHttpOnly(true);
+ cookie.setPath("/");
+ cookie.setMaxAge((int) (jwtUtil.getExpirationMs() / 1000));
+ response.addCookie(cookie);
+ return "redirect:/dashboard";
+ } catch (AuthenticationException e) {
+ return "redirect:/login?error";
+ }
+ }
+
+ @GetMapping("/logout")
+ public ResponseEntity logout() {
+ SecurityContextHolder.clearContext();
+ ResponseCookie cookie = ResponseCookie.from("jwt", "")
+ .httpOnly(true)
+ .path("/")
+ .maxAge(0)
+ .build();
+ return ResponseEntity.status(HttpStatus.FOUND)
+ .header(HttpHeaders.LOCATION, "/login?logout")
+ .header(HttpHeaders.SET_COOKIE, cookie.toString())
+ .build();
+ }
+}
diff --git a/src/main/java/com/example/bankapp/controller/BankController.java b/src/main/java/com/example/bankapp/controller/BankController.java
index 19fcded7..3ea671e3 100644
--- a/src/main/java/com/example/bankapp/controller/BankController.java
+++ b/src/main/java/com/example/bankapp/controller/BankController.java
@@ -42,11 +42,6 @@ public String registerAccount(@RequestParam String username, @RequestParam Strin
}
}
- @GetMapping("/login")
- public String login() {
- return "login";
- }
-
@PostMapping("/deposit")
public String deposit(@RequestParam BigDecimal amount) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 08663a63..75663d66 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -9,3 +9,7 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=true
+
+# JWT configuration
+jwt.secret=${JWT_SECRET}
+jwt.expiration-ms=3600000