java springboot3.x jwt+spring security6.x实现用户登录认证

springboot3.x jwt+spring security6.x实现用户登录认证

什么是JWT

JWT(JSON Web Token)是一种开放标准(RFC 7519),它用于在网络应用环境中传递声明。通常,JWT用于身份验证和信息交换。JWT的一个典型用法是在Web应用中实现基于Token的身份验证。

JWT 的特点

自包含(Self-contained):JWT 包含了用户的认证信息和其他相关的数据,因此无需在服务器端存储会话信息。它是通过加密或签名的方式来保证数据的完整性和防止篡改。

JWT的结构

JWT由三部分组成:

Header(头部):包含令牌的类型(通常是JWT)和加密算法(如HMAC SHA256或RSA)。
Payload(负载):包含声明(Claims)。这些声明可以是关于实体(通常是用户)和附加数据的断言。例如,可以包含用户的ID、角色、权限等。
Signature(签名):用来验证消息的完整性以及发送者的身份。通过Header中的算法和密钥生成签名。

什么是spring Security

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,通常用于保护 Spring 应用程序。它提供了各种安全功能,如:

认证(Authentication):用于确认用户的身份。例如,检查用户名和密码。
授权(Authorization):用于控制用户是否有权限访问某些资源。通常通过角色或权限进行授权。
防护功能:提供 CSRF 防护、会话管理、跨站点请求伪造防护等安全功能。

Spring Security 是一个全面的安全框架,可以与多种身份验证机制集成,如基于表单的登录、OAuth2、LDAP、JWT 等。

有了spring Security为什么还需要JWT

Spring Security 本身并不负责管理用户登录后如何保持会话的状态,这部分通常是通过 会话Session(以前的方式) 或 JWT 来完成的。

JWT 和 spring Security关系

Spring Security 和 JWT 之间并没有直接的关系,但是它们可以很好地结合使用。在一个典型的现代 Web 应用中,Spring Security 负责保护应用的安全性,而 JWT 通常用于实现基于 token 的无状态认证

Spring Boot 中的安全验证和存储用户信息

在Spring Boot应用中,常用的安全验证方式是结合 Spring Security 和 JWT。Spring Security提供了多种身份验证机制,而JWT则用于实现无状态的认证方案。JWT通常在用户登录成功后生成,并作为后续请求的认证凭证。

一般流程

  1. 用户登录:用户通过用户名和密码发送请求给后端,后端验证用户身份。
  2. 生成JWT:身份验证通过后,后端生成JWT,并将其发送给前端。JWT通常在HTTP响应的Authorization头中返回。
  3. 存储JWT:前端将JWT存储在本地(例如localStorage或sessionStorage)或Cookie中。
  4. 后续请求:每次前端向后端发起请求时,JWT都会包含在请求头的Authorization字段中,后端通过JWT验证用户身份。
  5. 验证JWT:后端通过Spring Security中的过滤器解码JWT,并验证签名。如果JWT有效,用户身份就会被确认。

为什么说JWT是无状态登录

是因为它的使用方式不需要在服务器端存储会话信息,也就是说,服务器不需要保存任何关于客户端的状态信息

有状态和无状态

有状态认证(Stateful Authentication)

在传统的认证机制中,服务器会维护一个会话(session)来记录客户端的身份信息。例如,在基于会话的认证中,当用户成功登录时,服务器会创建一个会话并生成一个唯一的会话 ID。这个会话 ID 会存储在服务器端的内存或数据库中,且会在客户端的浏览器中以 Cookie 或其他方式保存(例如,JSESSIONID)。每次客户端发起请求时,都会携带会话 ID,服务器根据会话 ID 查找会话信息来确认用户身份。

问题:服务器需要维护会话状态。每当用户发送请求时,服务器需要查询存储的会话数据。这会增加服务器的负担,尤其是在分布式系统中,需要将会话数据共享或同步到不同的服务器节点。

无状态认证(Stateless Authentication)

与会话认证不同,无状态认证不需要在服务器上存储任何客户端的会话信息。服务器验证身份时,通过客户端携带的信息(如 JWT)来验证请求,而不依赖于服务器端存储的会话状态。JWT 本身包含了足够的信息来完成身份验证和授权,服务器只需解析和验证这个 token,而无需查询任何存储的信息。

为什么不手动封装json存储

数据完整性与安全性

JWT: JWT 的一个核心特性是 签名,它确保了数据的 完整性和安全性。JWT 中的签名是由服务器的私钥生成的,任何人如果修改了 JWT 的内容,签名就会变得无效,服务器就能发现数据被篡改。
例如,如果你将用户信息存储为 JSON,前端将其发送给服务器,那么攻击者可能会篡改这个 JSON 数据(如修改用户角色、权限等),而服务器无法判断数据是否被篡改,除非你做额外的安全处理(比如加密)。
自定义 JSON: 如果你让前端封装 JSON 并发送到服务器,服务器将无法知道数据是否被篡改。没有类似 JWT 签名的机制,前端发送的 JSON 数据就很容易被篡改。为了解决这个问题,你需要自己实现一些安全机制(如加密或自定义签名),但这会增加实现的复杂度

无状态性与有效期管理

JWT:JWT 是自包含的,它不仅仅包含用户信息,还可以包含 过期时间(exp)等信息。这样,服务器可以非常容易地判断一个 JWT 是否过期,无需查询数据库或任何状态。

自定义 JSON:如果前端仅仅发送一个 JSON 数据包,服务器就无法知道该数据是否已经过期。为了实现这一点,你需要自己维护一个机制,比如在 JSON 中加入时间戳,或者在服务器端保存用户的会话过期时间。这会导致服务器变得有状态,因为服务器需要管理每个用户的会话生命周期。

服务器性能与分布式架构支持

JWT:由于 JWT 是自包含的,所有的信息都在 token 中,服务器不需要存储任何会话信息,这意味着它是 无状态的。无状态性在 分布式架构 中特别有优势,因为多个微服务可以独立地验证 JWT 而无需共享会话数据。

自定义 JSON:如果你选择自己封装 JSON,并由前端存储和发送,每次请求时,服务器就需要检查和验证这个 JSON 信息。尤其是在分布式环境中,你可能需要通过共享会话存储(如 Redis)来确保各个服务都能正确验证用户身份,增加了管理的复杂性。

可扩展性

JWT:由于 JWT 是标准化的,支持的库和工具非常多,你可以很容易地实现不同平台(如前端、后端、移动端等)之间的认证,而且不需要太多的额外工作。JWT 自带的灵活性和规范使得它可以适应多种需求。

自定义 JSON:如果你手动封装 JSON 并且实现自己的认证机制,虽然在小范围内可以工作,但这种方法缺乏标准化和统一性,扩展性差。在系统规模扩大或需求变化时,可能会遇到很多维护和兼容性问题。

实现

1.引入依赖

引入JWT依赖
        <!-- JWT library -->

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.11.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>


        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
引入spring Security依赖
        <!-- Spring Security for Authentication -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.实现 UserDetailsService接口

UserDetailsService 是 Spring Security 中一个非常重要的接口,负责从数据库或其他存储中加载用户的详细信息,通常用于身份验证和授权过程。

在 Spring Security 中,用户信息通常存储在数据库或其他外部系统中。为了进行认证和授权,Spring Security 需要从这些存储中获取用户的详细信息,如用户名、密码、角色、权限等。为了实现这一点,Spring Security 提供了 UserDetailsService 接口,它定义了如何加载这些用户信息。

package com.example.demonew.service.loginService;

import com.example.demonew.entity.UserInfo;
import com.example.demonew.mapper.loginMapper.LoginMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private LoginMapper loginMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo user = loginMapper.getUserInfo(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
		//将信息添加到User也可以说是UserDetails 对象中
        return User.withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();
    }

}

总结

实现 UserDetailsService 接口的主要目的是将用户的认证信息(包括用户名、密码、角色等)从数据库等存储中提取出来,并转化为 Spring Security 能够理解并使用的 UserDetails 对象。在 Spring Security 的认证和授权流程中,UserDetailsService 是用来提供用户信息的核心组件。

当用户尝试登录时,Spring Security 会调用 loadUserByUsername 方法来查找用户,并使用返回的 UserDetails 来进行身份验证和授权。因此,实现 UserDetailsService 是将自定义的用户数据源(如数据库)与 Spring Security 进行集成的关键步骤。

3.实现JWT工具类

这里工具类实现了,生成jwt token,解密jwt, 以及通过jwt获取用户名,验证令牌等功能,方便后续调用

package com.example.demonew.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Component
@Slf4j
public class JwtTokenProviderUtil {


    @Value("${app.jwt-secret}")
    private String jwtSecret;

    @Value("${app.jwt-expiration-milliseconds}")
    private long jwtExpirationDate;


    private Key key;

    // 使用懒加载方式获取密钥
    private Key getKey() {
        if (key == null) {
            synchronized (this) {
                if (key == null) {
                    key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
                }
            }
        }
        return key;
    }

    // 生成 JWT token
    public String generateToken(Authentication authentication){
        String username = authentication.getName();

        Date currentDate = new Date();

        Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);

        String token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(getKey(),SignatureAlgorithm.HS256)
                .compact();
        return token;
    }

    private Key key(){
        return Keys.hmacShaKeyFor(
                Decoders.BASE64.decode(jwtSecret)
        );
    }

    //解密token
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }



    // 解析 JWT 令牌并返回其主体
    private Claims getClaimsFromToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(getKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            log.error("Error parsing JWT token: {}", e.getMessage());
            throw new RuntimeException("JWT token parsing failed", e);
        }
    }

    // 验证 JWT 令牌
    public boolean validateToken(String token) {
        try{
            Jwts.parserBuilder()
                    .setSigningKey(getKey())
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            log.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            log.error("JWT claims string is empty: {}", e.getMessage());
        }
        return false;

    }

    // 获取用户认证信息
    public Authentication getAuthentication(String token) {
        String username = getUsername(token);
        List<GrantedAuthority> authorities = getAuthorities(token);
        return new UsernamePasswordAuthenticationToken(username, "", authorities);
    }

    //通过jwt信息获取用户名
    public String getUsername(String token) {
        Claims claims = getClaimsFromToken(token);
        String username = claims.getSubject();
        return username;

    }

    public List<GrantedAuthority> getAuthorities(String token) {
        Claims claims = getClaimsFromToken(token);
        List<String> roles = (List<String>)claims.get("roles");

        return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }


}


设置密钥和过期时间
app:
  jwt-secret: 60fosjWhlsy6bxLjSv5P/8fvmvanEEAjUQm3KLkSuMc= # 密钥 不唯一自己设置
  jwt-expiration-milliseconds: 604800000
随机生成密钥
    @Test
    void jwtSecretGenerator (){
        SecureRandom random = new SecureRandom();
        byte[] secret = new byte[32];  // 256 位
        random.nextBytes(secret);
        String jwtSecret = Base64.getEncoder().encodeToString(secret);  // 将字节数组转换为 Base64 编码字符串
        System.out.println(jwtSecret);
    }

4.spring Security配置类

Spring Security 配置类主要用于设置安全策略,过滤器链以及禁用默认的 CSRF 等。
spring boot3.0废弃了extends WebSecurityConfigurerAdapter的方式,所以这里采用添加@Bean新方式

SecurityConfig 是 Spring Security 的配置类,作用是配置 Spring Security 的安全策略,进行身份认证和授权管理

可以看出spring Security就是通过http来做跨域,权限等控制

package com.example.demonew.config;


import com.example.demonew.service.loginService.CustomUserDetailsService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity  // 确保添加这个注解来启用 Spring Security 配置
public class SecurityConfig {

    @Resource
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private  JwtAuthenticationFilter jwtAuthenticationFilter;



    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 配置 HTTP 安全策略
        http
                .csrf(csrf -> csrf.disable()) // 由于你使用的是前后端分离的架构,CSRF 防护通常是不需要的。禁用 CSRF 防护可以避免 Spring Security 对跨站请求伪造攻击的保护,从而简化 API 调用。
                //授权配置
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/login").permitAll() // 允许所有用户访问 /user/login 路径下的资源(通常是登录接口)。其他接口需要认证后访问。
                        .requestMatchers("/test/**").hasAuthority("ROLE_ADMIN") // 仅允许具有 ADMIN 角色的用户访问 /test/** 路径下的资源。
                        .anyRequest().authenticated() //所有其他请求都需要进行身份认证才能访问。
                )
                //  jwtAuthenticationFilter是一个自定义的过滤器,它负责处理请求中的 JWT 认证。此过滤器被配置为在 Spring Security 自带的 UsernamePasswordAuthenticationFilter 之前执行,这意味着在 Spring Security 处理用户名和密码认证之前,会先进行 JWT 校验。
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .userDetailsService(userDetailsService) //配置自定义的 UserDetailsService 用于加载用户信息
                .formLogin(form -> form.disable()) // Spring Security 默认启用基于表单的登录认证。如果你使用的是前后端分离的方式,通常不会使用传统的表单登录,因此禁用它。
                .logout(logout -> logout.permitAll()); // 配置登出功能,允许所有用户进行登出。



        return http.build();  // 返回配置好的 SecurityFilterChain
    }


    /*
     这个方法配置了 Spring Security 的 AuthenticationManager,它是进行用户认证的核心组件。配置了一个 DaoAuthenticationProvider,它使用 CustomUserDetailsService 来加载用户信息,使用 BCryptPasswordEncoder 进行密码验证。
    DaoAuthenticationProvider:是 Spring Security 用于基于数据库的用户认证提供程序,它会从 UserDetailsService 中加载用户信息,并使用密码编码器对用户密码进行校验。
    UserDetailsService:CustomUserDetailsService 实现了 UserDetailsService 接口,用于加载用户信息(例如,用户名、密码、角色等)。它通常会从数据库中查询用户信息。
    PasswordEncoder:BCryptPasswordEncoder 用于对用户输入的密码进行加密,然后与存储在数据库中的加密密码进行对比。BCrypt 是一种常见的密码加密算法。
    返回 AuthenticationManager:该管理器负责执行认证流程,返回经过认证的 Authentication 对象。
    */
    @Bean
    public AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService,
            PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService); // 配置 UserDetailsService,用于加载用户信息
        authenticationProvider.setPasswordEncoder(passwordEncoder); // 配置密码编码器,用于密码校验

        return new ProviderManager(authenticationProvider); // 返回一个 ProviderManager,管理多个认证提供者
    }

    /*
    该方法返回一个 BCryptPasswordEncoder 实例,用于对用户的密码进行加密和验证。
    BCryptPasswordEncoder 是一种安全的密码加密算法,Spring Security 默认推荐使用这种加密方式。
    */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

//    @Bean
//    public SecurityContextRepository securityContextRepository() {
//        return new HttpSessionSecurityContextRepository();
//    }


}



5.创建JWT 过滤器

创建一个过滤器来拦截每个请求,提取 JWT,并验证它。过滤器会检查请求头是否带有 JWT,如果有,它会验证 JWT 并通过 Spring Security 认证上下文设置用户身份。

JwtAuthenticationFilter 是一个用于处理 JWT(JSON Web Token)身份验证的过滤器类,继承自 Spring Security 的 OncePerRequestFilter。它的作用是在每次请求时,检查请求头中的 JWT token,并进行校验,如果 token 验证通过,则根据 token 中的信息设置 Spring Security 的 Authentication,确保用户能够通过 JWT 认证来访问受保护的资源。

package com.example.demonew.config;

import com.example.demonew.service.loginService.LoginService;
import com.example.demonew.util.JwtTokenProviderUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProviderUtil jwtTokenProvider;

    @Autowired
    private UserDetailsService userDetailsService;


/*这是过滤器的核心方法,它会在每次 HTTP 请求时被调用。方法中的处理流程如下:*/
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 通过 getTokenFromRequest(request) 方法,尝试从 HTTP 请求的 Authorization 头中提取 JWT。如果 token 存在且以 Bearer 开头,则提取出 token 部分。
        String token = getTokenFromRequest(request);

        /* 校验 token
        使用 jwtTokenProvider.validateToken(token) 校验 JWT 的合法性。
        例如,检查 token 是否过期、是否篡改等。如果 token 无效或过期,认证过程会被跳过,后续请求会被拒绝。
        */
        if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){
            try {
                // 从 token 获取 username
                String username = jwtTokenProvider.getUsername(token);

                // 加载与 token 关联的用户
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                /*
                 * 创建一个 UsernamePasswordAuthenticationToken 实例,传入 userDetails 和该用户的权限(userDetails.getAuthorities())。
                 * UsernamePasswordAuthenticationToken 是 Spring Security 用来封装用户身份信息的一个对象,它包含了用户的身份信息和权限。
                 * */
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                /*
                 * 使用 SecurityContextHolder.getContext().setAuthentication(authenticationToken) 将身份认证信息(即 authenticationToken)存储在 Spring Security 的上下文中。
                 * 这样,Spring Security 在处理后续请求时,就能基于这个认证信息对请求进行授权控制。
                 * */
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }catch (Exception e){
                // 记录日志并清除认证上下文
                log.warn("JWT Token validation failed: " + e.getMessage());
                SecurityContextHolder.clearContext();
            }


        }else {
            // 无 token 或 token 无效,清除认证上下文
            logger.warn("JWT Token is missing or invalid");
            SecurityContextHolder.clearContext();
        }

        /*最后,调用 filterChain.doFilter(request, response) 让请求继续传递给下一个过滤器或最终的目标(如 Controller)。
        这一步是确保请求能够正常进入应用的下一阶段*/
        filterChain.doFilter(request, response);
    }




    // 从请求头获取 JWT 格式为:Authorization: Bearer <token>
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7); // 去掉 "Bearer " 前缀
        }
        return null;
    }
}


为什么使用 OncePerRequestFilter

OncePerRequestFilter 是 Spring 提供的一个过滤器基类,确保每次请求只会调用一次过滤器的 doFilterInternal 方法,而不是每个过滤器链都执行一次。这可以有效避免重复执行相同的逻辑,保证 JWT 校验只执行一次。

创建登录接口

这样用户先通过用户名,密码进行登录,然后校验信息,返回userDetail对象

package com.example.demonew.controller.login;

import com.example.demonew.common.Result;
import com.example.demonew.config.SecurityConfig;
import com.example.demonew.service.loginService.LoginService;
import com.example.demonew.util.JwtTokenProviderUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/user")
public class LoginController {
    @Autowired
    private LoginService loginService;

    @Autowired
    private JwtTokenProviderUtil jwtTokenProviderUtil;

    @Autowired
    private AuthenticationManager authenticationManager;



    @PostMapping("/login")
    public Result Login(@RequestBody LoginRequest loginRequest){
        loginService.login(loginRequest.getUsername(),loginRequest.getPassword());
        // 登录授权
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        // 将登录用户信息交给SpringSecurity管理
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 利用用户授权信息生成JWT令牌
        String token = jwtTokenProviderUtil.generateToken(authentication);

        return Result.success(token);

    };

    @PostMapping("/register")
    public Result registerUser(@RequestBody LoginRequest loginRequest) {
        BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
        String encodedPassword = bCryptPasswordEncoder.encode(loginRequest.password);
        String password=encodedPassword;
        loginService.register(loginRequest.getUsername(),password);
        return new Result();
    }


    // 登录请求体的封装类
    public static class LoginRequest {
        private String username;
        private String password;

        // getters and setters
        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }
    }

}

准备数据

在这里插入图片描述
密码这里先手动生成一个

    @Test
    void testBCryptPasswordEncoder(){
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encode = encoder.encode("123456");
        System.out.println(encode);
    }

postman测试

登录

登录不需要带Authorization信息,登录后jwt会生成token返回,后续请求需要带上
在这里插入图片描述

其他接口

没权限 未带认证
在这里插入图片描述

带上认证成功访问
在这里插入图片描述

错误总结

执行filterChain.doFilter(request, response);后无法进入controller

解决

检查securityFilterChain方法中的requestMatchers方法,url是否正确
requestMatchers(“/user/login”).permitAll() // 开放登录接口

Caused by: java.lang.IllegalArgumentException: A UserDetailsService must be set

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'securityFilterChain' defined in class path resource [com/example/demonew/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProvider
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:896)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:849)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:509)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1441)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:336)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:289)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:334)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1122)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1093)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1030)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:987)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:627)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:318)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350)
	at com.example.demonew.DemoNewApplication.main(DemoNewApplication.java:16)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'securityFilterChain' defined in class path resource [com/example/demonew/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProvider
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:657)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:645)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1357)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1187)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:563)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:336)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:289)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:334)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.addCandidateEntry(DefaultListableBeanFactory.java:1883)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1847)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeanCollection(DefaultListableBeanFactory.java:1737)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeans(DefaultListableBeanFactory.java:1705)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1580)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1519)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:888)
	... 22 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProvider
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.lambda$instantiate$0(SimpleInstantiationStrategy.java:199)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiateWithFactoryMethod(SimpleInstantiationStrategy.java:88)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:168)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)
	... 39 common frames omitted
Caused by: java.lang.RuntimeException: Could not postProcess org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a of type class org.springframework.security.authentication.dao.DaoAuthenticationProvider
	at org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor.postProcess(AutowireBeanFactoryObjectPostProcessor.java:71)
	at org.springframework.security.config.annotation.SecurityConfigurerAdapter$CompositeObjectPostProcessor.postProcess(SecurityConfigurerAdapter.java:130)
	at org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess(SecurityConfigurerAdapter.java:82)
	at org.springframework.security.config.annotation.authentication.configurers.userdetails.AbstractDaoAuthenticationConfigurer.configure(AbstractDaoAuthenticationConfigurer.java:96)
	at org.springframework.security.config.annotation.authentication.configurers.userdetails.AbstractDaoAuthenticationConfigurer.configure(AbstractDaoAuthenticationConfigurer.java:36)
	at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure(AbstractConfiguredSecurityBuilder.java:398)
	at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:352)
	at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38)
	at org.springframework.security.config.annotation.web.builders.HttpSecurity.beforeConfigure(HttpSecurity.java:3301)
	at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:351)
	at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38)
	at com.example.demonew.config.SecurityConfig.securityFilterChain(SecurityConfig.java:49)
	at com.example.demonew.config.SecurityConfig$$SpringCGLIB$$0.CGLIB$securityFilterChain$2(<generated>)
	at com.example.demonew.config.SecurityConfig$$SpringCGLIB$$FastClass$$1.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258)
	at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:348)
	at com.example.demonew.config.SecurityConfig$$SpringCGLIB$$0.securityFilterChain(<generated>)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.lambda$instantiate$0(SimpleInstantiationStrategy.java:171)
	... 42 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.authentication.dao.DaoAuthenticationProvider@2a4e939a': A UserDetailsService must be set
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:413)
	at org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor.initializeBeanIfNeeded(AutowireBeanFactoryObjectPostProcessor.java:98)
	at org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor.postProcess(AutowireBeanFactoryObjectPostProcessor.java:67)
	... 61 common frames omitted
Caused by: java.lang.IllegalArgumentException: A UserDetailsService must be set
	at org.springframework.util.Assert.notNull(Assert.java:181)
	at org.springframework.security.authentication.dao.DaoAuthenticationProvider.doAfterPropertiesSet(DaoAuthenticationProvider.java:99)
	at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.afterPropertiesSet(AbstractUserDetailsAuthenticationProvider.java:119)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1855)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804)
	... 64 common frames omitted
Disconnected from the target VM, address: '127.0.0.1:59351', transport: 'socket'

Process finished with exit code 1

解决

2025-01-14 11:59:16.898 WARN — [nio-8080-exec-2] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt

解决

密码格式不正确,确保数据库中的密码使用 BCrypt 加密

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encode = encoder.encode("123456");

TemplateInputException

详细信息如下

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateInputException: Error resolving template [user/login], template might not exist or might not be accessible by any of the configured Template Resolvers] with root cause
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [user/login], template might not exist or might not be accessible by any of the configured Template Resolvers

在返回return时候报错

解决

在 pom.xml 中删除 Thymeleaf 依赖(如果不使用 Thymeleaf)

原因

Spring Boot 默认渲染视图:

当你使用 Spring Boot 启动项目时,如果没有明确声明返回类型,Spring Boot 会默认尝试将响应返回为视图(view)页面,而不是直接返回 JSON 或其他格式的数据。
在你的代码中,login 方法返回的是 Result.success(token),这个 token 是你生成的 JWT,应该作为 API 的响应数据返回,而不是试图通过 Thymeleaf 渲染 user/login 模板。
返回的是数据,而非视图:

如果你的 login 是一个 RESTful 风格的 API,应该返回 JSON 数据而不是试图渲染页面。Spring 默认会将没有指定视图的请求作为视图解析,导致它去找 user/login.html。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953932.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录刷题day07|(数组篇)58.区间和

目录 一、数组理论基础 二、前缀和 三、相关算法题目 四、总结 五、待解决问题 一、数组理论基础 数组是存放在连续内存空间上的相同类型数据的集合。 代码随想录 (programmercarl.com) 特点&#xff1a; 1.下标从0开始&#xff0c;内存中地址空间是连续的 2.查询快&…

专用小软件,完全免费,非常丝滑

今天给大家介绍一个专门将PDF数电发票合并打印的软件&#xff0c;这个软件可以批量操作&#xff0c;完全免费没有任何的广告。 电子发票专用批量打印工具 免费批量使用 软件无需安装&#xff0c;解压之后双击这个图标就能直接使用了。 点击右上角的加号&#xff0c;选中需要打…

安装虚拟机VMware遇到的问题

问题1&#xff1a;进入如下界面&#xff0c;不知道如何操作 解决办法 键盘⬇️&#xff0c;选择“Reset the system”回车 问题2&#xff1a;系统存放位置我给放在了VMware安装目录&#xff0c;具体D:\software\VMware\Windows安装不行 解决办法&#xff1a;D:\software\virt…

Matlab 具有周期性分布的死角孔的饱和空气多孔材料的声学特性

本文对直主孔含侧空腔&#xff08;死角&#xff09;的饱和空气多孔介质中的声传播进行了理论和数值研究。侧腔位于沿每个主孔周期性间隔的“节点”上。研究了侧向空腔分布中周期性的影响&#xff0c;并单独考虑了紧间隔死角的低频极限。结果表明&#xff0c;吸附系数和透射损失…

Vue如何构建项目

目录 1.安装Node.js 2.换源(建议) 3.选择一个目录 4.创建一个vue项目 5.验证是否成功 1.安装Node.js 安装18.3或更⾼版本的 Nodejs 点击下载->Node.Js中文网 node -v npm -v 安装好后在windows的cmd窗口下运行 如果能运行出结果就说明安装好了。 2.换源(建议) //…

网络层协议-----IP协议

目录 1.认识IP地址 2.IP地址的分类 3.子网划分 4.公网IP和私网IP 5.IP协议 6.如何解决IP地址不够用 1.认识IP地址 IP 地址&#xff08;Internet Protocol Address&#xff09;是指互联网协议地址。 它是分配给连接到互联网的设备&#xff08;如计算机、服务器、智能手机…

MacOS 下 Memory Analyzer 启动报错

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的全栈工程师 欢迎分享 / 收藏 / 赞 / 在看…

sql模糊关联匹配

需求目标&#xff1a; 建立临时表 drop table grafana_bi.zbj_gift_2024;USE grafana_bi; CREATE TABLE zbj_gift_2024 (id INT AUTO_INCREMENT PRIMARY KEY,userName VARCHAR(255),giftName VARCHAR(255),giftNum INT,points INT,teacher VARCHAR(255),sendDate DATETIME,…

automake error: version mismatch

automake error: version mismatch REF:automake 编译提示版本报错 解决高版本不兼容低版本

C++----STL(string)

引言&#xff1a;STL简介 什么是STL STL(standard template libaray-标准模板库)&#xff1a; 是 C标准库的重要组成部分&#xff08;注意&#xff1a;STL只是C标准库里的一部分&#xff0c;cin和cout也是属于C标准库的&#xff09;&#xff0c;不仅是一个可复用的组件库&…

如何选择视频文件

文章目录 1. 概念介绍2. 方法与细节2.1 实现方法2.2 具体细节 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何选择多个图片文件"相关的内容&#xff0c;本章回中将介绍如何选择视频文件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在前…

C++中的STL

STL&#xff08;标准模板库&#xff09;在广义上分为&#xff1a;容器&#xff0c;算法&#xff0c;迭代器 容器和算法之间通过迭代器进行无缝衔接 STL大体上分为六大组件:分别为容器&#xff0c;算法&#xff0c;迭代器&#xff0c;仿函数&#xff0c;适配器&#xff0c;空间…

Windows下安装和配置Go开发环境

文章目录 1. 介绍了SDK2. 下载 SDK工具包3. windows 下配置 Golang 环境变量 1. 介绍了SDK SDK 的全称(Software Development Kit 软件开发工具包)SDK是提供给开发人员使用的&#xff0c;其中包含了对应开发语言的工具包 2. 下载 SDK工具包 Go语言的官网为&#xff1a;https…

riscv架构下linux4.15实现early打印

在高版本linux6.12.7源码中&#xff0c;early console介绍&#xff0c;可参考《riscv架构下linux6.12.7实现early打印》文章。 1 什么是early打印 适配内核到新的平台&#xff0c;基本环境搭建好之后&#xff0c;首要的就是要调通串口&#xff0c;方便后面的信息打印。 正常流…

HarmonyOS 鸿蒙 ArkTs(5.0.1 13)实现Scroll下拉到顶刷新/上拉触底加载,Scroll滚动到顶部

HarmonyOS 鸿蒙 ArkTs(5.0.1 13)实现Scroll下拉到顶刷新/上拉触底加载 效果展示 使用方法 import LoadingText from "../components/LoadingText" import PageToRefresh from "../components/PageToRefresh" import FooterBar from "../components/…

《自动驾驶与机器人中的SLAM技术》ch9:自动驾驶车辆的离线地图构建

目录 1 点云建图的流程 2 前端实现 2.1 前端流程 2.2 前端结果 3 后端位姿图优化与异常值剔除 3.1 两阶段优化流程 3.2 优化结果 ① 第一阶段优化结果 ② 第二阶段优化结果 4 回环检测 4.1 回环检测流程 ① 遍历第一阶段优化轨迹中的关键帧。 ② 并发计算候选回环对…

鸿蒙面试 2025-01-10

写了鉴权工具&#xff0c;你在项目中申请了那些权限&#xff1f;&#xff08;常用权限&#xff09; 位置权限 &#xff1a; ohos.permission.LOCATION_IN_BACKGROUND&#xff1a;允许应用在后台访问位置信息。 ohos.permission.LOCATION&#xff1a;允许应用访问精确的位置信息…

Windows图形界面(GUI)-QT-C/C++ - QT控件创建管理初始化

公开视频 -> 链接点击跳转公开课程博客首页 -> ​​​链接点击跳转博客主页 目录 控件创建 包含对应控件类型头文件 实例化控件类对象 控件设置 设置父控件 设置窗口标题 设置控件大小 设置控件坐标 设置文本颜色和背景颜色 控件排版 垂直布局 QVBoxLayout …

Unreal Engine 5 C++ Advanced Action RPG 七章笔记

第七章 Ranged Enemy 2-Ranged Enemy Starting Weapon 制作新敌人的流程准备 新敌人的武器起始的状态数据自己的战斗能力投射能力自己的行为树 创建角色,添加武器,添加数据,就是继承之前的基类敌人的 运行结果 3-Glacer Starting Stats 看看就行,就是复制曲线表格更改数…

funcaptcha手势指向验证码识别

注意&#xff0c;本文只提供学习的思路&#xff0c;严禁违反法律以及破坏信息系统等行为&#xff0c;本文只提供思路 如有侵犯&#xff0c;请联系作者下架 本文滑块识别已同步上线至OCR识别网站&#xff1a; http://yxlocr.nat300.top/ocr/other/21 该验证码会给出某物品所有的…