Spring Security JWT登录失败跳转至带error参数登录页求助
登录失败排查请求
我认为项目的安全配置已正确设置,但无法登录主页面。尝试登录时,系统未跳转至主页面,而是返回登录页,地址从http://localhost:8080/api/login/变为http://localhost:8080/api/login/?error。以下是项目相关配置代码,恳请协助排查问题:
WebSecurityConfig
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${authkey}") private String authKey; private final JwtTokenFilter jwtTokenFilter; private final JwtTokenConfig jwtTokenConfig; @Autowired public WebSecurityConfig(JwtTokenFilter jwtTokenFilter, JwtTokenConfig jwtTokenConfig) { this.jwtTokenFilter = jwtTokenFilter; this.jwtTokenConfig = jwtTokenConfig; } @Bean public PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); //return new BCryptPasswordEncoder(12); } @Bean protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers( "/api/login/", "/api/login/registration").permitAll() .anyRequest().authenticated() .and() .apply(jwtTokenConfig) .and() .logout() .logoutRequestMatcher(new RegexRequestMatcher("/api/logout", "GET")) .deleteCookies(authKey).logoutSuccessUrl("/") .and() .formLogin() .loginPage("/api/login/"); } }
JwtTokenFilter
@Component public class JwtTokenFilter extends GenericFilterBean { private final JwtTokenAdapter adapter; @Value("${authkey}") private String secretUrlKey; @Autowired public JwtTokenFilter(JwtTokenAdapter adapter){ this.adapter = adapter; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { authByCookies((HttpServletRequest) servletRequest); } catch (AuthenticationException ex){ SecurityContextHolder.clearContext(); } catch (Exception ex){ SecurityContextHolder.clearContext(); ex.printStackTrace(); } filterChain.doFilter(servletRequest, servletResponse); } private void authByCookies(HttpServletRequest servletRequest){ Cookie[] cookies = servletRequest.getCookies(); if (cookies != null){ if (cookies.length > 0){ Optional<Cookie> authorization = Arrays .stream(cookies) .filter(cookie -> cookie.getName().equals(secretUrlKey)).findFirst(); authorization .map(Cookie::getValue) .ifPresent(token -> SecurityContextHolder .getContext() .setAuthentication(adapter.authentication(token))); } } } }
JwtTokenConfig
@Component public class JwtTokenConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private final JwtTokenFilter jwtTokenFilter; @Autowired public JwtTokenConfig(JwtTokenFilter jwtTokenFilter) { this.jwtTokenFilter = jwtTokenFilter; } @Override public void configure(HttpSecurity builder) throws Exception{ builder.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
JwtTokenAdapter
@Component public class JwtTokenAdapter { private final EmployeeRepository employeeRepository; private String secret = "KEMZ"; @Value("${authkey}") private String secretUrlKey; @Autowired public JwtTokenAdapter(EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; } public String createToken (String username){ Date date = new Date(); return Jwts.builder() .setSubject(username) .setIssuedAt(date) .setExpiration(new Date(date.getTime() * 100_000)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Authentication authentication (String token){ Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); if (!claims.getExpiration().before(new Date())){ Employee employee = employeeRepository.findByUsername(claims.getSubject()).orElseThrow(() -> new UsernameNotFoundException("not")); UserDetail detail= UserDetail.getInstance(employee); return new UsernamePasswordAuthenticationToken(detail, null, detail.getAuthorities()); } else throw new UsernameNotFoundException("ТЫ НЕ ПРОЙДЕШЬ!!!... попробуй-ка еще разок"); } }
UserDetailService
@Component("userDetailService") public class UserDetailService implements UserDetailsService { private final EmployeeRepository employeeRepository; @Autowired public UserDetailService(EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Employee employee = employeeRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("not")); return UserDetail.getInstance(employee); } }
UserDetail
@AllArgsConstructor public class UserDetail implements UserDetails { private String username; private String password; private Set<SimpleGrantedAuthority> roles; private boolean isBlocked; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles; } @Override public String getPassword() { return username; } @Override public String getUsername() { return password; } @Override public boolean isAccountNonExpired() { return !isBlocked; } @Override public boolean isAccountNonLocked() { return !isBlocked; } @Override public boolean isCredentialsNonExpired() { return !isBlocked; } @Override public boolean isEnabled() { return !isBlocked; } public static UserDetail getInstance(Employee employee){ return new UserDetail( employee.getUsername(), employee.getPassword(), Stream.of(new SimpleGrantedAuthority(employee.getPosition().name())) .collect(Collectors.toSet()), employee.getIsBlocked() ); } }
RegistrationControllerImpl
@Controller @Slf4j public class RegistrationControllerImpl implements RegistrationController { private EmployeeService employeeService; @Autowired public RegistrationControllerImpl(EmployeeService employeeService){ this.employeeService = employeeService; } @GetMapping ("/") public String loginForm(){ return "login"; } @GetMapping ("/registration") public String registrationForm() { return "registration"; } @PostMapping("/") public String authenticate(@RequestParam String username, @RequestParam String password, ServletResponse response){ log.info("мы должны сюда попасть"); try{ employeeService.authenticate(new AuthUserDto(username, password), response); return "redirect:/api/product/"; } catch (AuthenticationException e){ log.info(e.getMessage()); e.printStackTrace(); return "redirect:/api/login/"; } } @PostMapping ("/registration") public String processRegistration (EmployeeDto employeeDto){ employeeService.addEmployee(employeeDto); return "redirect:/api/product/"; } }
RegistrationController
@RequestMapping ("/api/login") public interface RegistrationController { @GetMapping("/") String loginForm(); @GetMapping ("/registration") String registrationForm(); @PostMapping ("/") String authenticate(String username, String password, ServletResponse response); @PostMapping ("/registration") String processRegistration(EmployeeDto employeeDto); }
login.html
<!DOCTYPE html> <html lang="ru" xmlns:th="www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"> <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/> </head> <body> <h1>Login</h1> <!--<div th:if="${error}">--> <!--Unable to login. Check your username and password.--> <!--</div>--> <p>New here? Click <a th:method="get" th:href="@{/api/login/registration}">here</a> to register.</p> <form method="post" th:action="@{/api/login/}" id="loginForm"> <label for="username">ЛОГИН: </label> <input type="text" name="username" id="username" /><br/> <label for="password">ПАРОЛЬ: </label> <input type="password" name="password" id="password" /><br/> <!--<form th:method="post" th:action="@{api/login/authenticate}">--> <input type="submit" value="ВОЙТИ"/> <!--</form>--> </form> </body> </html>
registration.html
<!DOCTYPE html> <html lang="ru" xmlns:th="www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Registration</title> </head> <body> <h1>Register</h1> <form th:method="post" th:action="@{registration}" id="registerForm"> <input type="text" name="username" placeholder="Логин"/><br/> <!--<label for="password">ПАРОЛЬ: </label>--> <input type="password" name="password" placeholder="Пароль"/><br/> <!--<label for="confirm">ПОДТВЕРДИТЕ ПАРОЛЬ: </label>--> <input type="password" name="confirm" placeholder="Повторить пароль"/><br/> <!--<label for="name">ИМЯ: </label>--> <input type="text" name="name" placeholder="Имя"/><br/> <!--<label for="surname">ФАМИЛИЯ: </label>--> <input type="text" name="surname" placeholder="Фамилия"/><br/> <!--<label for="position">ДОЛЖНОСТЬ: </label>--> <input type="text" name="position" placeholder="Должность"/><br/> <input type="submit" value="Зарегистрироваться"/> <!--<form th:method="get" th:action="@{/}">--> <!--<input type="submit" value="Зарегистрироваться"/>--> <!--</form>--> </form> </body> </html>
app.properties
spring.datasource.url=jdbc:mysql://localhost:3306/company?createDatabaseIfNotExist=true spring.datasource.username=root spring.datasource.password=00400040 spring.jpa.show-sql=true spring.mvc.hiddenmethod.filter.enabled=true server.servlet.session.tracking-modes=cookie #spring.jpa.hibernate.ddl-auto=create #spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect authkey=authenticated
我已为此问题困扰许久,但仍无法找到原因。
核心问题排查
- UserDetail类字段完全颠倒:
getPassword()返回username,getUsername()返回password,Spring Security验证时会用表单提交的用户名匹配UserDetails的username,密码匹配UserDetails的password,此逻辑直接导致验证失败。 - 认证模式冲突:同时启用JWT无状态认证和formLogin表单登录,formLogin默认依赖会话,但配置了
SessionCreationPolicy.STATELESS,导致表单登录后无法维持会话,跳转时因无JWT Cookie被拦截回登录页。 - JWT过期时间计算错误:
new Date(date.getTime() * 100_000)会导致时间戳溢出,生成的JWT瞬间过期,后续请求携带该Token直接认证失败。 - 登录请求处理冲突:自定义
authenticate方法和Spring Security的formLogin都拦截/api/login/的POST请求,两者逻辑重叠导致认证流程混乱。
内容的提问来源于stack exchange,提问作者Dinislam
相关产品推荐
相关产品推荐

