From 95fdbc2c9896b4071b3cb90bba2307bd68f65333 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 18 Apr 2024 18:16:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=20`JWT`=20?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=92=8C=E9=AA=8C=E8=AF=81=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=92=8C=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7=E5=90=8D=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E9=94=99=E8=AF=AF=E7=9A=84=E5=BC=82=E5=B8=B8=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/youlai/system/SystemApplication.java | 2 + .../constant/JwtClaimConstants.java | 2 +- .../common/constant/SecurityConstants.java | 12 ++++ .../system/common/util/ResponseUtils.java | 50 ++++++++-------- .../youlai/system/config/SecurityConfig.java | 34 +++++------ .../youlai/system/config/WebSocketConfig.java | 4 +- .../config/property/SecurityProperties.java | 44 ++++++++++++++ .../filter/CaptchaValidationFilter.java | 2 +- .../system/filter/JwtValidationFilter.java | 51 ++++++++++------ .../aspect/DuplicateSubmitAspect.java | 15 +++-- .../security/constant/SecurityConstants.java | 17 ------ .../system/security/model/SysUserDetails.java | 6 +- .../youlai/system/security/util/JwtUtils.java | 58 +++++-------------- .../system/service/impl/AuthServiceImpl.java | 27 ++++++--- src/main/resources/application-dev.yml | 21 +++++-- src/main/resources/application-prod.yml | 21 +++++-- 16 files changed, 212 insertions(+), 154 deletions(-) rename src/main/java/com/youlai/system/{security => common}/constant/JwtClaimConstants.java (92%) create mode 100644 src/main/java/com/youlai/system/config/property/SecurityProperties.java delete mode 100644 src/main/java/com/youlai/system/security/constant/SecurityConstants.java diff --git a/src/main/java/com/youlai/system/SystemApplication.java b/src/main/java/com/youlai/system/SystemApplication.java index c928f2aa..94bc3f42 100644 --- a/src/main/java/com/youlai/system/SystemApplication.java +++ b/src/main/java/com/youlai/system/SystemApplication.java @@ -2,8 +2,10 @@ package com.youlai.system; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication +@ConfigurationPropertiesScan public class SystemApplication { public static void main(String[] args) { SpringApplication.run(SystemApplication.class, args); diff --git a/src/main/java/com/youlai/system/security/constant/JwtClaimConstants.java b/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java similarity index 92% rename from src/main/java/com/youlai/system/security/constant/JwtClaimConstants.java rename to src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java index e662ddb9..c9efca82 100644 --- a/src/main/java/com/youlai/system/security/constant/JwtClaimConstants.java +++ b/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java @@ -1,4 +1,4 @@ -package com.youlai.system.security.constant; +package com.youlai.system.common.constant; /** * JWT Claims声明常量 diff --git a/src/main/java/com/youlai/system/common/constant/SecurityConstants.java b/src/main/java/com/youlai/system/common/constant/SecurityConstants.java index 96594d8b..11ea3f08 100644 --- a/src/main/java/com/youlai/system/common/constant/SecurityConstants.java +++ b/src/main/java/com/youlai/system/common/constant/SecurityConstants.java @@ -24,4 +24,16 @@ public interface SecurityConstants { String BLACKLIST_TOKEN_PREFIX = "token:blacklist:"; + /** + * 登录路径 + */ + String LOGIN_PATH = "/api/v1/auth/login"; + + + /** + * JWT Token 前缀 + */ + String JWT_TOKEN_PREFIX = "Bearer "; + + } diff --git a/src/main/java/com/youlai/system/common/util/ResponseUtils.java b/src/main/java/com/youlai/system/common/util/ResponseUtils.java index 2ed64175..cc46a6fc 100644 --- a/src/main/java/com/youlai/system/common/util/ResponseUtils.java +++ b/src/main/java/com/youlai/system/common/util/ResponseUtils.java @@ -1,49 +1,51 @@ package com.youlai.system.common.util; import cn.hutool.json.JSONUtil; -import com.youlai.system.common.result.IResultCode; import com.youlai.system.common.result.Result; import com.youlai.system.common.result.ResultCode; - +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; - -import static com.youlai.system.common.result.ResultCode.*; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; /** * 响应工具类 * - * @author haoxr + * @author Ray Hao * @since 2.0.0 */ +@Slf4j public class ResponseUtils { /** * 异常消息返回(适用过滤器中处理异常响应) * - * @param response - * @param resultCode + * @param response HttpServletResponse + * @param resultCode 响应结果码 */ - public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) throws IOException { - switch (resultCode) { - case ACCESS_UNAUTHORIZED: - case TOKEN_INVALID: - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - break; - case TOKEN_ACCESS_FORBIDDEN: - response.setStatus(HttpStatus.FORBIDDEN.value()); - break; - default: - response.setStatus(HttpStatus.BAD_REQUEST.value()); - break; - } + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + // 根据不同的结果码设置HTTP状态 + int status = switch (resultCode) { + case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + + response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding("UTF-8"); - response.getWriter().print(JSONUtil.toJsonStr(Result.failed(resultCode))); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } } - } diff --git a/src/main/java/com/youlai/system/config/SecurityConfig.java b/src/main/java/com/youlai/system/config/SecurityConfig.java index ed7455ce..1713e643 100644 --- a/src/main/java/com/youlai/system/config/SecurityConfig.java +++ b/src/main/java/com/youlai/system/config/SecurityConfig.java @@ -1,7 +1,9 @@ package com.youlai.system.config; import cn.hutool.captcha.generator.CodeGenerator; -import com.youlai.system.security.constant.SecurityConstants; +import cn.hutool.core.collection.CollectionUtil; +import com.youlai.system.common.constant.SecurityConstants; +import com.youlai.system.config.property.SecurityProperties; import com.youlai.system.security.exception.MyAccessDeniedHandler; import com.youlai.system.security.exception.MyAuthenticationEntryPoint; import com.youlai.system.filter.JwtValidationFilter; @@ -17,6 +19,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -26,7 +29,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic /** * Spring Security 权限配置 * - * @author haoxr + * @author Ray Hao * @since 2023/2/17 */ @Configuration @@ -39,29 +42,33 @@ public class SecurityConfig { private final MyAccessDeniedHandler accessDeniedHandler; private final RedisTemplate redisTemplate; private final CodeGenerator codeGenerator; + private final SecurityProperties securityProperties; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + .authorizeHttpRequests(requestMatcherRegistry -> requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll() .anyRequest().authenticated() ) - .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ) + .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(AbstractHttpConfigurer::disable) + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) ; // 验证码校验过滤器 - http.addFilterBefore(new CaptchaValidationFilter(redisTemplate,codeGenerator), UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class); // JWT 校验过滤器 - http.addFilterBefore(new JwtValidationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new JwtValidationFilter(redisTemplate,securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class); return http.build(); } @@ -71,18 +78,11 @@ public class SecurityConfig { */ @Bean public WebSecurityCustomizer webSecurityCustomizer() { - return (web) -> web.ignoring() - .requestMatchers( - "/api/v1/auth/captcha", - "/webjars/**", - "/doc.html", - "/swagger-resources/**", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/ws/**", - "/ws-app/**" - ); + return (web) -> { + if (CollectionUtil.isNotEmpty(securityProperties.getIgnoreUrls())) { + web.ignoring().requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0])); + } + }; } /** diff --git a/src/main/java/com/youlai/system/config/WebSocketConfig.java b/src/main/java/com/youlai/system/config/WebSocketConfig.java index fdf86e20..69245d7c 100644 --- a/src/main/java/com/youlai/system/config/WebSocketConfig.java +++ b/src/main/java/com/youlai/system/config/WebSocketConfig.java @@ -2,7 +2,7 @@ package com.youlai.system.config; import cn.hutool.core.util.StrUtil; import cn.hutool.jwt.JWTPayload; -import com.youlai.system.security.util.JwtUtils; +import cn.hutool.jwt.JWTUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -83,7 +83,7 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 这里不应该用"name" // String username = JwtUtils.parseToken(bearerToken).get("name").toString(); - String username = JwtUtils.parseToken(bearerToken).get(JWTPayload.SUBJECT).toString(); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); if (StrUtil.isNotBlank(username)) { accessor.setUser(() -> username); diff --git a/src/main/java/com/youlai/system/config/property/SecurityProperties.java b/src/main/java/com/youlai/system/config/property/SecurityProperties.java new file mode 100644 index 00000000..1aacffbb --- /dev/null +++ b/src/main/java/com/youlai/system/config/property/SecurityProperties.java @@ -0,0 +1,44 @@ +package com.youlai.system.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + * @author haoxr + * @since 2024/4/18 + */ +@Data +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 白名单 URL 集合 + */ + private List ignoreUrls; + + /** + * JWT 配置 + */ + private JwtProperty jwt; + + + /** + * JWT 配置 + */ + @Data + public static class JwtProperty { + + /** + * JWT 秘钥 + */ + private String key; + + /** + * JWT 过期时间 + */ + private Long ttl; + + } +} diff --git a/src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java index 83c467c0..310c59b3 100644 --- a/src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java +++ b/src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java @@ -24,7 +24,7 @@ import java.io.IOException; */ public class CaptchaValidationFilter extends OncePerRequestFilter { - private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(com.youlai.system.security.constant.SecurityConstants.LOGIN_PATH, "POST"); + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST"); public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; diff --git a/src/main/java/com/youlai/system/filter/JwtValidationFilter.java b/src/main/java/com/youlai/system/filter/JwtValidationFilter.java index 110ca1e4..64da1135 100644 --- a/src/main/java/com/youlai/system/filter/JwtValidationFilter.java +++ b/src/main/java/com/youlai/system/filter/JwtValidationFilter.java @@ -1,13 +1,14 @@ package com.youlai.system.filter; -import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; import com.youlai.system.common.constant.SecurityConstants; import com.youlai.system.common.result.ResultCode; import com.youlai.system.security.util.JwtUtils; import com.youlai.system.common.util.ResponseUtils; -import com.youlai.system.common.exception.BusinessException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -19,22 +20,25 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.Map; /** * JWT token 校验过滤器 * - * @author haoxr + * @author Ray Hao * @since 2023/9/13 */ public class JwtValidationFilter extends OncePerRequestFilter { private final RedisTemplate redisTemplate; - public JwtValidationFilter(RedisTemplate redisTemplate) { + private final byte[] secretKey; + + public JwtValidationFilter(RedisTemplate redisTemplate, String secretKey) { this.redisTemplate = redisTemplate; + this.secretKey = secretKey.getBytes(); } + /** * 从请求中获取 JWT Token,校验 JWT Token 是否合法 *

@@ -44,27 +48,38 @@ public class JwtValidationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader(HttpHeaders.AUTHORIZATION); - try { - if (StrUtil.isNotBlank(token)) { - Map payload = JwtUtils.parseToken(token); - String jti = Convert.toStr(payload.get(JWTPayload.JWT_ID)); - Boolean isTokenBlacklisted = redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti); - if (Boolean.TRUE.equals(isTokenBlacklisted)) { + try { + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); // 去除 Bearer 前缀 + // 校验 Token 是否有效 + if (JWTUtil.verify(token, secretKey)) { + // 解析 Token 获取有效载荷 + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + // 检查 Token 是否已被加入黑名单 + String jti = payloads.getStr(JWTPayload.JWT_ID); + boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti)); + if (isTokenBlacklisted) { + ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + return; + } + // Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中 + Authentication authentication = JwtUtils.getAuthentication(payloads); + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + // Token 无效,直接返回响应 ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); return; } - - Authentication authentication = JwtUtils.getAuthentication(payload); - SecurityContextHolder.getContext().setAuthentication(authentication); } - } catch (BusinessException ex) { - //this is very important, since it guarantees the user is not authenticated at all + } catch (Exception e) { SecurityContextHolder.clearContext(); - ResponseUtils.writeErrMsg(response, (ResultCode) ex.getResultCode()); + ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); return; } - + // Token有效或无Token时继续执行过滤链 filterChain.doFilter(request, response); } } diff --git a/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java b/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java index aabc64ec..c88766bc 100644 --- a/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java +++ b/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java @@ -1,11 +1,12 @@ package com.youlai.system.plugin.dupsubmit.aspect; -import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; -import com.youlai.system.plugin.dupsubmit.annotation.PreventDuplicateSubmit; -import com.youlai.system.common.result.ResultCode; +import cn.hutool.jwt.JWTUtil; +import cn.hutool.jwt.RegisteredPayload; +import com.youlai.system.common.constant.SecurityConstants; import com.youlai.system.common.exception.BusinessException; -import com.youlai.system.security.util.JwtUtils; +import com.youlai.system.common.result.ResultCode; +import com.youlai.system.plugin.dupsubmit.annotation.PreventDuplicateSubmit; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -69,8 +70,10 @@ public class DuplicateSubmitAspect { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String token = request.getHeader(HttpHeaders.AUTHORIZATION); - if (StrUtil.isNotBlank(token)) { - String jti = Convert.toStr(JwtUtils.parseToken(token).get("jti"), null); + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + // 从 JWT Token 中获取 jti + String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID); resubmitLockKey = RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI(); } return resubmitLockKey; diff --git a/src/main/java/com/youlai/system/security/constant/SecurityConstants.java b/src/main/java/com/youlai/system/security/constant/SecurityConstants.java deleted file mode 100644 index 689fcfcd..00000000 --- a/src/main/java/com/youlai/system/security/constant/SecurityConstants.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.youlai.system.security.constant; - -/** - * Security 常量 - * - * @author haoxr - * @since 2.0.0 - */ -public interface SecurityConstants { - - /** - * 登录接口路径 - */ - String LOGIN_PATH = "/api/v1/auth/login"; - - -} diff --git a/src/main/java/com/youlai/system/security/model/SysUserDetails.java b/src/main/java/com/youlai/system/security/model/SysUserDetails.java index e6de8032..e6b36825 100644 --- a/src/main/java/com/youlai/system/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/system/security/model/SysUserDetails.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import com.youlai.system.model.dto.UserAuthInfo; import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -24,6 +25,7 @@ import java.util.stream.Collectors; @NoArgsConstructor public class SysUserDetails implements UserDetails { + @Getter private Long userId; private String username; @@ -60,10 +62,6 @@ public class SysUserDetails implements UserDetails { this.dataScope = user.getDataScope(); } - public Long getUserId() { - return this.userId; - } - @Override public Collection getAuthorities() { diff --git a/src/main/java/com/youlai/system/security/util/JwtUtils.java b/src/main/java/com/youlai/system/security/util/JwtUtils.java index 447e1dc3..49cc845f 100644 --- a/src/main/java/com/youlai/system/security/util/JwtUtils.java +++ b/src/main/java/com/youlai/system/security/util/JwtUtils.java @@ -3,12 +3,10 @@ package com.youlai.system.security.util; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONArray; -import cn.hutool.jwt.JWT; +import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWTPayload; import cn.hutool.jwt.JWTUtil; -import com.youlai.system.security.constant.JwtClaimConstants; +import com.youlai.system.common.constant.JwtClaimConstants; import com.youlai.system.security.model.SysUserDetails; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -21,9 +19,9 @@ import java.util.*; import java.util.stream.Collectors; /** - * JWT 工具类 + * JWT Token 工具类 * - * @author haoxr + * @author Ray Hao * @since 2.6.0 */ @Component @@ -41,12 +39,12 @@ public class JwtUtils { private static int ttl; - @Value("${jwt.key}") + @Value("${security.jwt.key}") public void setKey(String key) { JwtUtils.key = key.getBytes(); } - @Value("${jwt.ttl}") + @Value("${security.jwt.ttl}") public void setTtl(Integer ttl) { JwtUtils.ttl = ttl; } @@ -57,7 +55,7 @@ public class JwtUtils { * @param authentication 用户认证信息 * @return Token 字符串 */ - public static String generateToken(Authentication authentication) { + public static String createToken(Authentication authentication) { SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); @@ -80,25 +78,25 @@ public class JwtUtils { payload.put(JWTPayload.SUBJECT, authentication.getName()); payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); - return JWTUtil.createToken(payload, JwtUtils.key); + return JWTUtil.createToken(payload, key); } /** * 从 JWT Token 中解析 Authentication 用户认证信息 * - * @param payload JWT 载体 + * @param payloads JWT 载体 * @return 用户认证信息 */ - public static UsernamePasswordAuthenticationToken getAuthentication(Map payload) { + public static UsernamePasswordAuthenticationToken getAuthentication(JSONObject payloads) { SysUserDetails userDetails = new SysUserDetails(); - userDetails.setUserId(Convert.toLong(payload.get(JwtClaimConstants.USER_ID))); // 用户ID - userDetails.setDeptId(Convert.toLong(payload.get(JwtClaimConstants.DEPT_ID))); // 部门ID - userDetails.setDataScope(Convert.toInt(payload.get(JwtClaimConstants.DATA_SCOPE))); // 数据权限范围 + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 - userDetails.setUsername(Convert.toStr(payload.get(JWTPayload.SUBJECT))); // 用户名 + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 // 角色集合 - Set authorities = ((JSONArray) payload.get(JwtClaimConstants.AUTHORITIES)) + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) .stream() .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) .collect(Collectors.toSet()); @@ -107,30 +105,4 @@ public class JwtUtils { } - /** - * 解析 JWT Token 获取载体信息 - * - * @param token JWT Token - * @return 载体信息 - */ - public static Map parseToken(String token) { - try { - if (StrUtil.isBlank(token)) { - return null; - } - - if (token.startsWith("Bearer ")) { - token = token.substring(7); - } - - JWT jwt = JWTUtil.parseToken(token); - if (jwt.setKey(JwtUtils.key).validate(0)) { - return jwt.getPayloads(); - } - } catch (Exception ignored) { - } - return null; - } - - } diff --git a/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java index bd38b2e3..c7f6dd4d 100644 --- a/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java @@ -3,10 +3,11 @@ package com.youlai.system.service.impl; import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.generator.CodeGenerator; -import cn.hutool.core.convert.Convert; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; import com.youlai.system.common.constant.SecurityConstants; import com.youlai.system.common.enums.CaptchaTypeEnum; import com.youlai.system.model.dto.CaptchaResult; @@ -28,7 +29,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.awt.*; -import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit; public class AuthServiceImpl implements AuthService { private final AuthenticationManager authenticationManager; - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; private final CodeGenerator codeGenerator; private final Font captchaFont; private final CaptchaProperties captchaProperties; @@ -57,10 +57,13 @@ public class AuthServiceImpl implements AuthService { */ @Override public LoginResult login(String username, String password) { + // 认证用户信息 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password); + // 认证 Authentication authentication = authenticationManager.authenticate(authenticationToken); - String accessToken = JwtUtils.generateToken(authentication); + // 认证成功,生成Token + String accessToken = JwtUtils.createToken(authentication); return LoginResult.builder() .tokenType("Bearer") .accessToken(accessToken) @@ -74,18 +77,24 @@ public class AuthServiceImpl implements AuthService { public void logout() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String token = request.getHeader(HttpHeaders.AUTHORIZATION); - if (StrUtil.isNotBlank(token)) { - - Map claims = JwtUtils.parseToken(token); - String jti = Convert.toStr(claims.get(JWTPayload.JWT_ID)); - Long expiration = Convert.toLong(claims.get(JWTPayload.EXPIRES_AT)); + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + // 解析Token以获取有效载荷(payload) + JSONObject payloads = JWTUtil.parseToken(token).getPayloads(); + // 解析 Token 获取 jti(JWT ID) 和 exp(过期时间) + String jti = payloads.getStr(JWTPayload.JWT_ID); + Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); + // 如果exp存在,则计算Token剩余有效时间 if (expiration != null) { + // 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除 long ttl = expiration - System.currentTimeMillis() / 1000; redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS); } else { + // 如果exp不存在,说明Token永不过期,则永久加入黑名单 redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null); } } + // 清空Spring Security上下文 SecurityContextHolder.clearContext(); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c91408be..cc7ea990 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -57,12 +57,21 @@ mybatis-plus: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl -# 认证配置 -jwt: - # 密钥 - key: SecretKey012345678901234567890123456789012345678901234567890123456789 - # token 过期时间(单位:秒) - ttl: 7200 +# 安全配置 +security: + jwt: + key: SecretKey012345678901234567890123456789012345678901234567890123456789 + ttl: 7200 + ignore-urls: + - /v3/api-docs/** + - /doc.html + - /swagger-resources/** + - /webjars/** + - /doc.html + - /swagger-ui/** + - /swagger-ui.html + - /api/v1/auth/captcha + oss: # OSS 类型 (目前支持aliyun、minio) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 45c32fb8..553268ea 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -57,13 +57,22 @@ mybatis-plus: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl -# 认证配置 -jwt: - # 密钥 - key: SecretKey012345678901234567890123456789012345678901234567890123456789 - # token 过期时间(单位:秒) - ttl: 7200 +# 安全配置 +security: + jwt: + key: SecretKey012345678901234567890123456789012345678901234567890123456789 + ttl: 7200 + ignore-urls: + - /v3/api-docs/** + - /doc.html + - /swagger-resources/** + - /webjars/** + - /doc.html + - /swagger-ui/** + - /swagger-ui.html + - /api/v1/auth/captcha +# 文件上传配置 oss: # OSS 类型 (目前支持aliyun、minio) type: minio