From 5a6402f7fdfb09a96899f639ad7991c844e0b365 Mon Sep 17 00:00:00 2001 From: yulinling <2712495353@qq.com> Date: Sun, 15 Jun 2025 20:08:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20-=20=E5=AE=8C=E5=96=84Jwt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/config/MyBatisPlusConfig.java | 2 +- .../workflow/config/SecurityConfig.java | 18 +++++-- .../yulinling/workflow/constant/Status.java | 10 ++-- .../yulinling/workflow/model/ApiResponse.java | 49 ++++++++++--------- .../security/JwtAccessDeniedHandler.java | 45 +++++++++++++++++ .../security/JwtAuthenticationEntryPoint.java | 21 +++++++- .../security/JwtAuthenticationFilter.java | 22 ++------- .../security/JwtUserDetailsService.java | 14 ++++++ .../yulinling/workflow/utils/JwtUtil.java | 42 ++++++++++++++-- .../yulinling/workflow/utils/JwtUtilTest.java | 3 -- 10 files changed, 165 insertions(+), 61 deletions(-) create mode 100644 src/main/java/asia/yulinling/workflow/security/JwtAccessDeniedHandler.java diff --git a/src/main/java/asia/yulinling/workflow/config/MyBatisPlusConfig.java b/src/main/java/asia/yulinling/workflow/config/MyBatisPlusConfig.java index 21f57ea..f3c9203 100644 --- a/src/main/java/asia/yulinling/workflow/config/MyBatisPlusConfig.java +++ b/src/main/java/asia/yulinling/workflow/config/MyBatisPlusConfig.java @@ -9,7 +9,7 @@ import org.springframework.context.annotation.Configuration; /** *

- * MybatisPlus配置类 + * MybatisPlus配置类 *

* * @author YLL diff --git a/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java b/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java index 60ac3e9..21411fe 100644 --- a/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java +++ b/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java @@ -1,5 +1,6 @@ package asia.yulinling.workflow.config; +import asia.yulinling.workflow.security.JwtAccessDeniedHandler; import asia.yulinling.workflow.security.JwtAuthenticationEntryPoint; import asia.yulinling.workflow.security.JwtAuthenticationFilter; import asia.yulinling.workflow.security.JwtUserDetailsService; @@ -28,20 +29,29 @@ public class SecurityConfig { private final JwtUserDetailsService jwtUserDetailsService; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil, + JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, + JwtAccessDeniedHandler jwtAccessDeniedHandler) throws Exception { http + // 关闭CSRF .csrf(AbstractHttpConfigurer::disable) + // 关闭session .sessionManagement(AbstractHttpConfigurer::disable) + // 登录登出自定义实现 + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + // 异常处理 .exceptionHandling(ex -> ex .authenticationEntryPoint(jwtAuthenticationEntryPoint) - .accessDeniedHandler((request, response, accessDeniedException) -> - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied")) + .accessDeniedHandler(jwtAccessDeniedHandler) ) + // 认证请求 .authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() - .requestMatchers("/users", "/users/**").authenticated() + .requestMatchers("/users", "/users/**").hasRole("ADMIN") .anyRequest().authenticated() ) + // JWT过滤器 .addFilterBefore( new JwtAuthenticationFilter(jwtUtil, jwtUserDetailsService), UsernamePasswordAuthenticationFilter.class diff --git a/src/main/java/asia/yulinling/workflow/constant/Status.java b/src/main/java/asia/yulinling/workflow/constant/Status.java index c1dd399..ac290e5 100644 --- a/src/main/java/asia/yulinling/workflow/constant/Status.java +++ b/src/main/java/asia/yulinling/workflow/constant/Status.java @@ -4,7 +4,7 @@ import lombok.Getter; /** *

- * 状态码封装 + * 状态码封装 *

* * @author yulinling @@ -18,14 +18,10 @@ public enum Status { /** 未知异常 */ UNKNOWN_ERROR(500, "Unknown error"); - /** - * 状态码 - */ + /** 状态码 */ private final Integer code; - /** - * 内容 - */ + /** 内容 */ private final String message; Status(Integer code, String message) { diff --git a/src/main/java/asia/yulinling/workflow/model/ApiResponse.java b/src/main/java/asia/yulinling/workflow/model/ApiResponse.java index 598d5d3..519a958 100644 --- a/src/main/java/asia/yulinling/workflow/model/ApiResponse.java +++ b/src/main/java/asia/yulinling/workflow/model/ApiResponse.java @@ -32,14 +32,15 @@ public class ApiResponse { /** * 无参构造器 */ - private ApiResponse() {} + private ApiResponse() { + } /** * 全参构造器 * - * @param code 状态码 - * @param message 返回内容 - * @param data 返回数据 + * @param code 状态码 + * @param message 返回内容 + * @param data 返回数据 */ private ApiResponse(Integer code, String message, Object data) { this.code = code; @@ -50,9 +51,9 @@ public class ApiResponse { /** * 构造一个自定义的Api返回 * - * @param code 状态码 - * @param message 返回内容 - * @param data 返回数据 + * @param code 状态码 + * @param message 返回内容 + * @param data 返回数据 * @return ApiResponse */ public static ApiResponse of(Integer code, String message, T data) { @@ -62,8 +63,8 @@ public class ApiResponse { /** * 构造一个成功且带数据的Api返回 * - * @param data 返回数据 - * @return ApiResponse + * @param data 返回数据 + * @return ApiResponse */ public static ApiResponse ofSuccess(T data) { return ofStatus(Status.OK, data); @@ -72,8 +73,8 @@ public class ApiResponse { /** * 构造一个成功且自定义数据的Api返回 * - * @param message 返回内容 - * @return ApiResponse + * @param message 返回内容 + * @return ApiResponse */ public static ApiResponse ofMessage(String message) { return of(Status.OK.getCode(), message, null); @@ -82,18 +83,19 @@ public class ApiResponse { /** * 构造一个有状态不带数据的Api返回 * - * @param status 状态{@link Status} - * @return ApiResponse + * @param status 状态{@link Status} + * @return ApiResponse */ public static ApiResponse ofStatus(Status status) { return ofStatus(status, null); } + /** * 构造一个有状态且带数据的Api返回 * - * @param status 状态{@link Status} - * @param data 返回数据 - * @return ApiResponse + * @param status 状态{@link Status} + * @param data 返回数据 + * @return ApiResponse */ public static ApiResponse ofStatus(Status status, T data) { return of(status.getCode(), status.getMessage(), data); @@ -102,20 +104,21 @@ public class ApiResponse { /** * 构造一个异常且带数据的Api返回 * - * @param t 异常 - * @param data 返回数据 - * @return ApiResponse - * @param {@link BaseException} 子类 + * @param t 异常 + * @param data 返回数据 + * @param {@link BaseException} 子类 + * @return ApiResponse */ public static ApiResponse ofException(T t, T data) { return of(t.getCode(), t.getMessage(), data); } + /** * 构造一个异常且不带数据的Api返回 * - * @param t 异常 - * @return ApiResponse - * @param {@link BaseException} 子类 + * @param t 异常 + * @param {@link BaseException} 子类 + * @return ApiResponse */ public static ApiResponse ofException(T t) { return ofException(t, null); diff --git a/src/main/java/asia/yulinling/workflow/security/JwtAccessDeniedHandler.java b/src/main/java/asia/yulinling/workflow/security/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..02a1d3d --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/security/JwtAccessDeniedHandler.java @@ -0,0 +1,45 @@ +package asia.yulinling.workflow.security; + +import asia.yulinling.workflow.exception.BaseException; +import asia.yulinling.workflow.model.ApiResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + *

+ * JWT权限不足处理器 + *

+ * + * @author YLL + * @since 2025/6/15 + */ +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + private final ObjectMapper objectMapper; + + public JwtAccessDeniedHandler(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + + // 1. 设置res 403 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + + // 2. 构建通用response + BaseException baseException = new BaseException(HttpServletResponse.SC_FORBIDDEN, "权限不足"); + ApiResponse apiResponse = ApiResponse.ofException(baseException, null); + + // 3. 将通用res写入res + response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); + } +} diff --git a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationEntryPoint.java b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationEntryPoint.java index 5e70813..9f9063a 100644 --- a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationEntryPoint.java @@ -1,5 +1,8 @@ package asia.yulinling.workflow.security; +import asia.yulinling.workflow.exception.BaseException; +import asia.yulinling.workflow.model.ApiResponse; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; @@ -18,9 +21,25 @@ import java.io.IOException; */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper; + + public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Missing or invalid token"); + + // 1. 设置res 401 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + // 2. 构建通用response + BaseException baseException = new BaseException(401, "账号未登录"); + ApiResponse apiResponse = ApiResponse.ofException(baseException, null); + + // 3. 将通用res写入res + response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); } } diff --git a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java index 9800f4a..e528a0f 100644 --- a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java +++ b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java @@ -38,16 +38,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { - log.info("request: {}", request.getHeader("Authorization")); - String token = getTokenFromRequest(request); - log.info("token: {}", token); + // 1. 拿取Token + String token = jwtUtil.getTokenFromRequest(request); if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) { - // 解析token获取username + // 2. 解析token获取username String username = jwtUtil.parseToken(token).getSubject(); - log.info("username: {}", username); - // 根据token获取的username,加载当前登录中userDetails + // 3. 根据username验证password UserDetails userDetails = userDetailsService.loadUserByUsername(username); log.info("userDetails: {}", userDetails); @@ -61,16 +59,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } - - private @Nullable String getTokenFromRequest(HttpServletRequest request) { -// eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTc0OTk2MzA1OSwiZXhwIjoxNzQ5OTYzNjU5fQ.QxiZmycBGxfVfooh_T_lo9SibugLZ2bFt752UChHdtpNb6u__iXodQDK_s6hcz0R -// eyJhbGciOiJIUzI1NiJ9.e30.7QzwIJVh2WpbwTF5ce4crYy3kK2-4GOs0eYJqrGD8FU - String bearerToken = request.getHeader("Authorization"); - - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - - return null; - } } diff --git a/src/main/java/asia/yulinling/workflow/security/JwtUserDetailsService.java b/src/main/java/asia/yulinling/workflow/security/JwtUserDetailsService.java index a9fee01..c32006a 100644 --- a/src/main/java/asia/yulinling/workflow/security/JwtUserDetailsService.java +++ b/src/main/java/asia/yulinling/workflow/security/JwtUserDetailsService.java @@ -37,6 +37,7 @@ public class JwtUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 1. 构建查找sql LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(User.class) .eq(User::getUsername, username) .or() @@ -44,12 +45,25 @@ public class JwtUserDetailsService implements UserDetailsService { .or() .eq(User::getPhone, username); + // 2. 查找用户 User user = userMapper.selectOne(queryWrapper); + + if (user == null) { + throw new UsernameNotFoundException("未找到用户信息:" + username); + } + + // 3. 查找用户对应的角色 List roles = roleMapper.selectByIds(Collections.singleton(user.getId())); + if (roles.isEmpty()) { + throw new UsernameNotFoundException("未找到角色信息" + roles); + } + Set authorities = roles.stream() .map((role) -> new SimpleGrantedAuthority(role.getName())) .collect(Collectors.toSet()); + + // 4. 返回User return new org.springframework.security.core.userdetails.User( username, user.getPassword(), diff --git a/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java b/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java index 3abfe49..42a7f35 100644 --- a/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java +++ b/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java @@ -2,8 +2,10 @@ package asia.yulinling.workflow.utils; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import io.jsonwebtoken.*; @@ -52,7 +54,7 @@ public class JwtUtil { public String generateToken(Authentication authentication, boolean isRememberMe) { // 1. 构建签名密钥 Key key = key(); - log.info("key: {}", key); + // 2. 当前时间 Date now = new Date(); @@ -80,8 +82,10 @@ public class JwtUtil { */ public Claims parseToken(String token) { try { + // 1. 获取签名密钥 Key key = key(); - log.info("key: {}", key); + + // 2. 解析Token return Jwts.parserBuilder() .setSigningKey(key) .build() @@ -106,21 +110,51 @@ public class JwtUtil { * @return true-token正确 false-token错误 */ public boolean validateToken(String token) { + // 1. 获取签名密钥 Key key = key(); - log.info("token: {}; key: {}", token, key); + // 2. 根据密钥解析token try { Jwts.parserBuilder() .setSigningKey(key) .build() .parse(token); + // 3. 解析正确 return true; } catch (JwtException e) { - log.error("Token : {}", token, e); + log.error("Token: {}, Error: {}", token, e.getMessage()); + // 4. 解析错误 return false; } } + /** + * 设置Token国企 + * + * @param request 请求 + */ + public void invalidateToken(HttpServletRequest request) { + String token = getTokenFromRequest(request); + String username = parseToken(token).getSubject(); + } + + /** + * 解析请求头的Authorization,拿取Token + * + * @param request 请求 + * @return Token + */ + public @Nullable String getTokenFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + // 返回Token + return bearerToken.substring(7); + } + + return null; + } + /** * 获取签名密钥 * diff --git a/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java b/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java index 5bdbb9e..c42ce88 100644 --- a/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java +++ b/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java @@ -3,13 +3,11 @@ package asia.yulinling.workflow.utils; import io.jsonwebtoken.Claims; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.util.List; @@ -25,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*; */ class JwtUtilTest { private JwtUtil jwtUtil; - private AuthenticationManager authenticationManager; @BeforeEach void setUp() {