diff --git a/src/main/java/com/example/springdemo/security/SecurityFilterChainConfig.java b/src/main/java/com/example/springdemo/security/config/SecurityFilterChainConfig.java similarity index 59% rename from src/main/java/com/example/springdemo/security/SecurityFilterChainConfig.java rename to src/main/java/com/example/springdemo/security/config/SecurityFilterChainConfig.java index 00010e9..183f8e6 100644 --- a/src/main/java/com/example/springdemo/security/SecurityFilterChainConfig.java +++ b/src/main/java/com/example/springdemo/security/config/SecurityFilterChainConfig.java @@ -1,11 +1,13 @@ -package com.example.springdemo.security; +package com.example.springdemo.security.config; import com.example.springdemo.security.jwt.JwtAuthenticationFilter; -import jakarta.annotation.Resource; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -14,32 +16,41 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; -import org.springframework.security.web.authentication.AuthenticationFilter; @Configuration @EnableWebSecurity // Enable Spring Security @EnableGlobalAuthentication // Enable Spring Security's global authentication configuration -@EnableMethodSecurity(prePostEnabled = true) // Enable Spring Security's method security -public class SecurityFilterChainConfig { - @Resource - AuthenticationFilter authenticationFilter; - +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = false) // Enable Spring Security's method security +public class SecurityFilterChainConfig implements InitializingBean { @Bean - public SecurityFilterChain SecurityFilterChain(@NotNull HttpSecurity http) throws Exception { - var ignoreUrls = new String[]{"/login", "/logout", "/error"}; + public SecurityFilterChain SecurityFilterChain(@NotNull HttpSecurity http, + JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { + var ignoreUrls = new String[]{"/auth/**"}; var authedUrls = new String[]{"/users/*/**"}; http .authorizeHttpRequests( (request) -> request .requestMatchers(authedUrls).authenticated() // authenticate all requests to authedUrls .requestMatchers(ignoreUrls).permitAll() // permit all requests to ignoreUrls + .anyRequest().authenticated() // authenticate all other requests ) .httpBasic(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) - .sessionManagement(a -> a.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) - .addFilterBefore(authenticationFilter, AnonymousAuthenticationFilter.class); // jwt filter; + .addFilterBefore(jwtAuthenticationFilter, AnonymousAuthenticationFilter.class); // jwt filter; return http.build(); } + + @Bean + public AuthenticationManager authenticationManager + (@NotNull AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Override + public void afterPropertiesSet() throws Exception { + + } } diff --git a/src/main/java/com/example/springdemo/security/dto/JwtAuthResponse.java b/src/main/java/com/example/springdemo/security/dto/JwtAuthResponse.java new file mode 100644 index 0000000..44aaffc --- /dev/null +++ b/src/main/java/com/example/springdemo/security/dto/JwtAuthResponse.java @@ -0,0 +1,20 @@ +package com.example.springdemo.security.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@NoArgsConstructor +public class JwtAuthResponse { + private String accessToken; + private String tokenType; + + public JwtAuthResponse(String accessToken) { + this.accessToken = accessToken; + this.tokenType = "Bearer "; + } + +} + diff --git a/src/main/java/com/example/springdemo/security/dto/LoginDto.java b/src/main/java/com/example/springdemo/security/dto/LoginDto.java new file mode 100644 index 0000000..1436e2f --- /dev/null +++ b/src/main/java/com/example/springdemo/security/dto/LoginDto.java @@ -0,0 +1,11 @@ +package com.example.springdemo.security.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginDto { + private String username; + private String password; +} diff --git a/src/main/java/com/example/springdemo/security/events/AuthSuccess.java b/src/main/java/com/example/springdemo/security/events/AuthSuccess.java new file mode 100644 index 0000000..34ea509 --- /dev/null +++ b/src/main/java/com/example/springdemo/security/events/AuthSuccess.java @@ -0,0 +1,41 @@ +package com.example.springdemo.security.events; + +import com.example.springdemo.security.utils.JwtTokenProvider; +import com.example.springdemo.utils.verificationAnnotation.Result; +import jakarta.annotation.Resource; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class AuthSuccess { + @Resource + private JwtTokenProvider JwtTokenProvider; + + public void onAuthSuccess(HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull Authentication authentication) throws IOException, ServletException { + Result result = Result.of(200, "Login success"); + + log.info("User {} login success", authentication.getName()); + + String token = JwtTokenProvider.generateToken(authentication); + + result.setData(token); + + String responseJson = result.toString(); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + response.getWriter().println(responseJson); + response.getWriter().flush(); + + } +} diff --git a/src/main/java/com/example/springdemo/security/events/LoginSuccess.java b/src/main/java/com/example/springdemo/security/events/LoginSuccess.java deleted file mode 100644 index 84b7be2..0000000 --- a/src/main/java/com/example/springdemo/security/events/LoginSuccess.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.springdemo.security.events; - -import com.example.springdemo.entities.Users; -import org.jetbrains.annotations.NotNull; -import org.springframework.context.PayloadApplicationEvent; -import org.springframework.core.ResolvableType; - -public class LoginSuccess extends PayloadApplicationEvent { - public LoginSuccess(Object source, Users payload) { - super(source, payload); - } - - @Override - public ResolvableType getResolvableType() { - return ResolvableType.forRawClass(LoginSuccess.class); - } - - @Override - public @NotNull Users getPayload() { - return super.getPayload(); - } -} diff --git a/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..373ffc6 --- /dev/null +++ b/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,21 @@ +package com.example.springdemo.security.jwt; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component("JwtAuthenticationEntryPoint") +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull AuthenticationException authException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); + } +} diff --git a/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationFilter.java index d35736e..3d7f08b 100644 --- a/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/springdemo/security/jwt/JwtAuthenticationFilter.java @@ -1,23 +1,66 @@ package com.example.springdemo.security.jwt; +import com.example.springdemo.security.utils.JwtTokenProvider; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -@Slf4j +@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + private final UserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) { + this.jwtTokenProvider = jwtTokenProvider; + this.userDetailsService = userDetailsService; + } + @Override protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, - @NotNull FilterChain filterChain) - throws ServletException, IOException { - + @NotNull FilterChain filterChain) throws ServletException, IOException { + // 从 request 获取 JWT token + String token = getTokenFromRequest(request); + // 校验 token + if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + // 从 token 获取 username + String username = jwtTokenProvider.getUsername(token); + // 加载与令 token 关联的用户 + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // 获取安全上下文 + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + filterChain.doFilter(request, response); } -} + + private @Nullable String getTokenFromRequest(@NotNull HttpServletRequest request) { + + String bearerToken = request.getHeader("Authorization"); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/springdemo/security/jwt/token/RequestAuthToken.java b/src/main/java/com/example/springdemo/security/jwt/token/RequestAuthToken.java deleted file mode 100644 index 1b40a8c..0000000 --- a/src/main/java/com/example/springdemo/security/jwt/token/RequestAuthToken.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.springdemo.security.jwt.token; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; - -@Getter -@Setter -@EqualsAndHashCode(callSuper = false) -public class RequestAuthToken extends UsernamePasswordAuthenticationToken { - - private String userId; - - private String secret; - - private TokenType tokenType; - - public RequestAuthToken(Object principal, Object credentials, TokenType tokenType) { - this(principal, credentials, null, null, tokenType); - } - - public RequestAuthToken(Object principal, Object credentials, final String userId, String secret) { - this(principal, credentials, userId, secret, null); - } - - public RequestAuthToken(Object principal, Object credentials, final String userId, String secret, TokenType tokenType) { - super(principal, credentials); - } -} diff --git a/src/main/java/com/example/springdemo/security/jwt/token/TokenType.java b/src/main/java/com/example/springdemo/security/jwt/token/TokenType.java deleted file mode 100644 index 75b72ea..0000000 --- a/src/main/java/com/example/springdemo/security/jwt/token/TokenType.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.springdemo.security.jwt.token; - -import lombok.Getter; - -@Getter -public enum TokenType { - - STRING(1, "String"),//string - INFO(3, "INFO");//json - - private final Integer id; - private final String name; - - TokenType(Integer id, String name) { - this.id = id; - this.name = name; - } - -} diff --git a/src/main/java/com/example/springdemo/security/utils/JwtTokenProvider.java b/src/main/java/com/example/springdemo/security/utils/JwtTokenProvider.java new file mode 100644 index 0000000..9f4f23c --- /dev/null +++ b/src/main/java/com/example/springdemo/security/utils/JwtTokenProvider.java @@ -0,0 +1,88 @@ +package com.example.springdemo.security.utils; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +@Slf4j +public class JwtTokenProvider { + private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class); + + private String jwtSecret; + + @Value("${app.jwt-expiration-milliseconds}") + private long jwtExpirationDate; + + private void setJwtSecret() { + this.jwtSecret = Keys.secretKeyFor(SignatureAlgorithm.HS512).toString(); + } + + // 生成 JWT token + public String generateToken(@NotNull Authentication authentication) { + // 用户名 + String username = authentication.getName(); + + // 当前时间 + Date currentDate = new Date(); + + // 过期时间 + Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate); + + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .signWith(key()) + .compact(); + } + + @Contract(" -> new") + private @NotNull Key key() { + if (jwtSecret == null) { + this.setJwtSecret(); + } + return Keys.hmacShaKeyFor(jwtSecret.getBytes()); + } + + // 从 Jwt token 获取用户名 + public String getUsername(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(key()) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getSubject(); + } + + // 验证 Jwt token + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(key()) + .build() + .parse(token); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + return false; + } +}