You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 12:01:43