版本
<spring-boot.version>3.0.2</spring-boot.version>
<jjwt.version>0.12.5</jjwt.version>
问题描述
题主在写 SpringSecurity6 + JWT 做登录认证开发。一路跟着教程叭叭的敲。等到接口验证的时候,发现我的登录接口虽然在SecurityConfig
中配置了免验证,但是访问登录接口的时候还是被拦截了。
这里先直接点题说明题主出错的原因:
因为我配置的URL是全路径的,也就是携带着server.servlet.context-path
的配置内容的,类似这样/bees/API/login/wechat
,其中/bees
就是我配置的context-path
。
我出问题的时候已经在网上找了很多解决方式。但是都没有解决我的问题。
当我在访问我的login
接口的时候,系统依然进入了我的自定义验证过滤器中。
后面在通义千问中,AI给出的建议,配置Security的日志输出级别到DEBUG
这样能有效地排查问题。
logging.level.org.springframework.security=DEBUG
这样将SpringSecurity的log级别调到DEBUG,在启动服务的时候发现了两行有用的日志
这是在遇到问题,根据各种文章修改完依然没能解决问题之后的唯一一点曙光。
解决:去掉/bees
之后,再次请求登录接口,就不会再过我们自定义的认证拦截器了。
这就是题主的遇到的问题症结所在。
总结
三点。
- 检查你自定义的认证拦截器是不是交给Spring管理了(如果是可能会有问题)。
- 检查你的SecurityConfig配置类中是否有配置
WebSecurityCustomizer
这个Bean(没有需要加上)。 - 最后检查你需要免验证的URL是否跟我一样加了
context-path
(或者使用了通配符,题主试过通配符但是不太行,可能是我使用方式有问题)。
最后贴一个比较完整的代码片段
包括 SecurityConfig
JwtAuthenticationFilter
JwtTokenUtil
代码片段如下:
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate UserServiceImpl userService;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {//关闭csrf和frameOptions,如果不关闭会影响前端请求接口http.csrf(AbstractHttpConfigurer::disable).headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));//开启跨域以便前端调用接口http.cors();//这里是配置的关键,决定哪些接口开启防护,哪些接口绕过防护// 配置访问控制规则http.authorizeHttpRequests(request ->// 指定特定接口无需验证即可访问,如微信登录request.requestMatchers(HttpMethod.POST, "/API/login/wechat").permitAll()// 其他所有以 "/bees/" 开头的接口需要认证才能访问.requestMatchers("/bees/**").authenticated());//禁用sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//将我们自定义的认证过滤器替换掉默认的认证过滤器,指定将自定义的Filter添加到某个指定的Filter之前或者之后http.addFilterBefore(new WeChatLoginFilter(jwtTokenUtil, userService), UsernamePasswordAuthenticationFilter.class);//指定认证错误处理器http.exceptionHandling().authenticationEntryPoint(new BeesAuthenticationFailEntryPoint()).accessDeniedHandler(new BeesDeniedHandler());return http.build();}/*** 指定加密器** @return BCryptPasswordEncoder*/@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();// 提供自定义loadUserByUsernameauthProvider.setUserDetailsService(userService);// 指定密码编辑器authProvider.setPasswordEncoder(passwordEncoder());return authProvider;}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic WebSecurityCustomizer ignoringCustomizer() {return (web) -> web.ignoring().requestMatchers(HttpMethod.POST, "/API/login/wechat");}
}
JwtAuthenticationFilter
/**
* 自定义的认证过滤器不交给Spring管理
*/
public class WeChatLoginFilter extends OncePerRequestFilter {private static final Logger log = LoggerFactory.getLogger(WeChatLoginFilter.class);private final JwtTokenUtil jwtTokenUtil;private final UserServiceImpl userService;public WeChatLoginFilter(JwtTokenUtil jwtTokenUtil, UserServiceImpl userService) {this.jwtTokenUtil = jwtTokenUtil;this.userService = userService;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {log.info("---WeChatLoginFilter---");String authorizationS = request.getHeader("Authorization");if (!StringUtils.hasLength(authorizationS)) {chain.doFilter(request, response);return;}Claims claims = jwtTokenUtil.getAllClaimsFromToken(authorizationS);//说明解析失败了if (claims == null) {throw new BadCredentialsException("token异常或已过期");}String authorization = claims.getSubject();TokenInfo tokenInfo = JSON.parseObject(authorization, TokenInfo.class);//通过loginType的区分,避免每次都查数据库if (LoginType.WX.getLoginType().equals(tokenInfo.getLoginType())) {User user = userService.loadUserByUserId(tokenInfo.getUserId());//用户不存在 todoBeesCommonAuthenticationToken authentication = new BeesCommonAuthenticationToken(JSON.toJSONString(user), "");//UserDetail user = userService.loadUserByUserId(tokenInfo.getUserId());
// BeesCommonAuthenticationToken authentication = new BeesCommonAuthenticationToken(user.getUser().getWxOpenId(), "", user.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authentication);}chain.doFilter(request, response);}
}
JwtTokenUtil
@Component
public class JwtTokenUtil {private static final long JWT_EXPIRATION_TIME_MS = 86400000; // 1 day/*** 加密用的盐*/@Value("${security.jwt.secretKey}")private String secretKey;// 生成JWT令牌public String generateToken(Integer loginType, Long userId) {TokenInfo tokenInfo = new TokenInfo();tokenInfo.setUserId(userId);tokenInfo.setLoginType(loginType);String subject = JSON.toJSONString(tokenInfo);Map<String, Object> claims = new HashMap<>();return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_TIME_MS)).signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();}// 解析JWT令牌并获取claimspublic Claims getAllClaimsFromToken(String token) {return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}// 从JWT令牌中提取用户名public String getUsernameFromToken(String token) {return getAllClaimsFromToken(token).getSubject();}// 验证JWT令牌是否有效public Boolean validateToken(String token, UserDetails userDetails) {final String username = getUsernameFromToken(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}// 检查JWT令牌是否过期private Boolean isTokenExpired(String token) {final Date expiration = getAllClaimsFromToken(token).getExpiration();return expiration.before(new Date());}// 生成签名密钥private SecretKey getSigningKey() {byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);return Keys.hmacShaKeyFor(keyBytes);}
}
——————————————以上————————————————————
希望对各位有帮助。
参考文章:
SpringSecurity6解决requestMatchers().permitAll()后依然执行自定义过滤器的问题