SpringBoot使用JWT校验token实现登录验证

1.添加token依赖

在pom.xml中添加以下依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.3</version>
        </dependency>


        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

2.密钥配置文件

在resources目录下新建jwt.properties文件用来配置密钥

secretKey=aABfwdeUympsYdY4h6tHzpZNzvXa6zmpVdQzi2hEtwqXDVAZAgvcb5FaR4gAYfpHepYKyYMy
#这里的值是加密密钥,可以去网上搜密码加密器生成

3.token工具类

新建一个JwtUntil的java类,建议放在untils目录下,用来生成、校验token

public class JwtUntil {
    // 使用SLF4J日志框架创建一个Logger实例,用于记录日志信息
    private static final Logger logger = LoggerFactory.getLogger(JwtUntil.class);

    // 从配置文件中加载的密钥,用于JWT的签名和验证
    private static final String secretKey = loadSecretKey();

    // 设置JWT的过期时间为604800秒(即1周)
//    private static final Long expiration = 1209600L;
    private static final Long expiration = 604800L;
    /**
     * 加载JWT使用的密钥。
     * <p>
     * 该方法从类路径下的配置文件`jwt.properties`中加载密钥。如果配置文件不存在或密钥未被正确指定,
     * 则抛出运行时异常,以确保应用程序在启动时能够正确配置密钥。
     *
     * @return 返回配置文件中指定的密钥字符串。
     * @throws RuntimeException 如果配置文件加载失败或密钥未被正确指定,抛出运行时异常。
     */
    private static String loadSecretKey() {
        // 创建Properties对象,用于存储配置文件中的键值对
        Properties properties = new Properties();
        try {
            // 使用JwtUtil类的类加载器加载配置文件jwt.properties
            // 这个方法会返回一个输入流,用于读取配置文件内容
            //这里的jwt.properties是密钥
            InputStream inputStream = JwtUntil.class.getClassLoader().getResourceAsStream("jwt.properties");

            // 检查输入流是否为null,如果是,则表示配置文件未找到
            if (inputStream == null) {
                // 抛出FileNotFoundException,表示配置文件未找到
                throw new FileNotFoundException("配置文件jwt.properties未找到");
            }

            // 使用Properties对象加载输入流中的配置信息
            properties.load(inputStream);

            // 从Properties对象中获取名为"secretKey"的属性值
            // 这个值是从配置文件中读取的密钥
            String secretKey = properties.getProperty("secretKey");

            // 检查获取到的密钥是否为空或null
            if (secretKey == null || secretKey.isEmpty()) {
                // 如果密钥未被正确指定,抛出IllegalArgumentException
                throw new IllegalArgumentException("配置文件中未指定secretKey");
            }

            // 如果一切正常,返回配置文件中指定的密钥
            return secretKey;
        } catch (FileNotFoundException e) {
            // 如果配置文件未找到,记录错误日志,并抛出运行时异常
            logger.error("配置文件jwt.properties未找到", e);
            throw new RuntimeException("配置文件加载失败,无法启动应用", e);
        } catch (IllegalArgumentException e) {
            // 如果配置文件中未指定密钥,记录错误日志,并抛出运行时异常
            logger.error("配置文件中未指定secretKey", e);
            throw new RuntimeException("配置文件加载失败,无法启动应用", e);
        } catch (Exception e) {
            // 捕获其他所有异常,记录错误日志,并抛出运行时异常
            // 这包括了Properties加载过程中可能发生的任何异常
            logger.error("密钥验证失败", e);
            throw new RuntimeException("密钥加载失败,无法启动应用", e);
        }
    }






    /**
     * 生成用户token,并设置token的超时时间。
     * 这个方法接受用户名作为参数,并创建一个包含用户名信息的JWT。
     * JWT的过期时间被设置为从生成时起的1周。
     *
     * @param userNumber 账号
     * @return 生成的JWT字符串
     */
    public static String createToken(String userNumber) {
        // 检查用户名是否为空,避免创建无效的token
        if (userNumber == null || userNumber.isEmpty()) {
            throw new IllegalArgumentException("账号不能为空");
        }

        // 设置JWT的过期时间为当前时间加上1周(1周的毫秒数)
        long twoWeeksInMilliseconds = 7 * 24 * 60 * 60 * 1000L;
        //创建了一个Date对象,表示从当前时间起加上一周后的日期和时间,这个日期和时间将被用作JWT的过期时间
        Date expireDate = new Date(System.currentTimeMillis() + twoWeeksInMilliseconds);

        // 创建一个Map来存储JWT头部信息
        Map<String, Object> headerMap = new HashMap<>();
        headerMap.put("alg", "HS256"); // 指定签名算法为HMAC256
        headerMap.put("typ", "JWT");   // 指定令牌类型为JWT

        // 使用Auth0的JWT库创建JWT
        // 首先创建一个JWT.builder()来配置JWT的各个部分
        JWTCreator.Builder builder = JWT.create()
                .withHeader(headerMap) // 添加头部信息
                .withClaim("userNumber", userNumber) // 添加账号
                .withExpiresAt(expireDate) // 设置过期时间
                .withIssuedAt(new Date()) // 设置签发时间
                .withIssuer("sdApp"); // 可选:设置发行者
        //这里的 "yourIssuer" 是一个占位符,你应该替换为实际的发行者名称,例如你的应用或服务的名称

        // 检查secretKey是否已加载,避免签名时出现空指针异常
        if (secretKey == null || secretKey.isEmpty()) {
            throw new IllegalStateException("密钥未加载或为空");
        }

        // 使用密钥进行签名,并构建最终的JWT字符串
        String token = builder.sign(Algorithm.HMAC256(secretKey));

        // 记录生成的JWT,便于调试和监控
        logger.info("Token created for user {}: {}", userNumber, token);
        return token;
    }




    /**
     * 校验token并解析token。
     * 这个方法接受一个JWT字符串作为参数,并验证其有效性。
     * 如果token有效,返回true;如果token无效或过期,返回false。
     *
     * @param token 需要验证的JWT字符串
     * @return true如果token有效,false如果token无效或过期
     */
    public static boolean verifyToken(String token) {
        // 检查传入的token是否为空,如果为空直接返回false
        if (token == null || token.isEmpty()) {
            logger.error("令牌空值");
            return false;
        }

        try {
            // 使用Auth0的JWT库构建一个JWTVerifier实例,用于验证JWT
            // 这里假设secretKey是一个已经定义好的密钥,用于HMAC256算法
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secretKey)).build();

            // 使用验证器验证JWT,如果token无效或被篡改,会抛出JWTVerificationException异常
            DecodedJWT jwt = verifier.verify(token);

            // 检查JWT是否过期,如果过期则返回false
            if (jwt.getExpiresAt().before(new Date())) {
                // 如果JWT过期,记录警告日志并返回false
                logger.warn("令牌已过期");
                return false;
            }

            // 如果JWT有效且未过期,返回true
            return true;
        } catch (JWTVerificationException e) {
            // 如果JWT验证失败,记录错误日志并返回false
            // 这可能发生在token无效、被篡改或签名不匹配时
            logger.error("令牌验证错误: {}", e.getMessage());
            return false;
        } catch (Exception e) {
            // 如果发生其他异常,记录错误日志并返回false
            // 这可能是由于内部错误或配置问题导致的
            logger.error("令牌验证过程中出现意外错误", e);
            return false;
        }
    }

4.拦截器

新建JwtInterceptor的Java类,建议放到interceptor目录下

JWT(JSON Web Tokens)拦截器是一种在Web应用程序中用于验证JWT的服务器端组件。它的作用是在请求到达具体的Controller之前检查HTTP请求头中是否包含有效的JWT,并且该JWT是否给予了访问特定资源的权限。以下是JWT拦截器的主要功能和实现细节:

主要功能

  1. 验证JWT:检查每个请求是否包含一个有效的JWT,并验证该令牌的有效性。
  2. 权限控制:确保只有带有有效JWT的请求才能访问受保护的资源。
  3. 日志记录:记录JWT验证过程中的重要信息,便于调试和监控。
  4. 错误处理:当JWT无效或过期时,拦截器会返回适当的HTTP状态码和错误信息。
/**
 * JwtInterceptor类实现了HandlerInterceptor接口,用于拦截进入Controller之前的请求,
 * 并验证请求头中的JWT是否有效。
 */
@Component
public class JwtInterceptor implements HandlerInterceptor {
    /**
     * 使用LoggerFactory创建一个Logger实例,用于记录日志信息。
     */
    private static final Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);

    /**
     * preHandle方法在请求处理之前被调用,用于在Controller方法执行之前进行预处理。
     * 如果该方法返回true,则请求继续向下传递到下一个拦截器或Controller;
     * 如果返回false,则请求将被中断,不会继续处理。
     *
     * @param request  HttpServletRequest对象,表示当前请求。该对象包含请求的信息,如请求头、查询参数等。
     * @param response HttpServletResponse对象,表示当前响应。该对象用于构造响应,如设置状态码、响应头等。
     * @param handler  当前请求的处理者,可能是Controller方法或另一个拦截器。这个参数可以用来判断请求是否被其他拦截器处理过。
     * @return boolean值,表示是否继续执行请求。如果返回true,则请求继续向下传递;如果返回false,则请求将被中断。
     * @throws Exception 可能抛出的异常,例如在处理请求或响应时发生的异常。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /**
         * 从请求头中获取"Authorization"字段,它应该包含Bearer令牌。
         * 这个令牌用于验证用户的身份。
         */
        String token = request.getHeader("Authorization");

        /**
         * 检查token是否为空或不以"Bearer "开头。
         * 如果令牌丢失或无效,设置响应状态为401 Unauthorized,并返回错误信息。
         */
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("令牌丢失或无效");
            return false; // 中断请求
        }

        /**
         * 提取令牌字符串,去掉"Bearer "前缀。
         * 这个字符串是JWT的实际内容,需要被验证。
         */
        String authToken = token.substring(7);

        /**
         * 使用JwtUtil验证令牌是否有效。
         * JwtUtil类提供了JWT的生成和验证功能。
         */
        boolean isValid = JwtUntil.verifyToken(authToken);
        if (!isValid) {
            /**
             * 如果令牌无效或已过期,设置响应状态为401 Unauthorized,并返回错误信息。
             */
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("令牌无效或已过期");
            return false; // 中断请求
        }

        /**
         * 如果令牌验证成功,返回true以继续执行下一个拦截器或Controller。
         */
        return true;
    }
}

5.注册拦截器配置文件

JwtInterceptorConfig
/**
 * JwtInterceptor类实现了HandlerInterceptor接口,用于拦截进入Controller之前的请求,
 * 并验证请求头中的JWT是否有效。
 */
@Component
public class JwtInterceptor implements HandlerInterceptor {
    /**
     * 使用LoggerFactory创建一个Logger实例,用于记录日志信息。
     */
    private static final Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);

    /**
     * preHandle方法在请求处理之前被调用,用于在Controller方法执行之前进行预处理。
     * 如果该方法返回true,则请求继续向下传递到下一个拦截器或Controller;
     * 如果返回false,则请求将被中断,不会继续处理。
     *
     * @param request  HttpServletRequest对象,表示当前请求。该对象包含请求的信息,如请求头、查询参数等。
     * @param response HttpServletResponse对象,表示当前响应。该对象用于构造响应,如设置状态码、响应头等。
     * @param handler  当前请求的处理者,可能是Controller方法或另一个拦截器。这个参数可以用来判断请求是否被其他拦截器处理过。
     * @return boolean值,表示是否继续执行请求。如果返回true,则请求继续向下传递;如果返回false,则请求将被中断。
     * @throws Exception 可能抛出的异常,例如在处理请求或响应时发生的异常。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /**
         * 从请求头中获取"Authorization"字段,它应该包含Bearer令牌。
         * 这个令牌用于验证用户的身份。
         */
        String token = request.getHeader("Authorization");

        /**
         * 检查token是否为空或不以"Bearer "开头。
         * 如果令牌丢失或无效,设置响应状态为401 Unauthorized,并返回错误信息。
         */
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("令牌丢失或无效");
            return false; // 中断请求
        }

        /**
         * 提取令牌字符串,去掉"Bearer "前缀。
         * 这个字符串是JWT的实际内容,需要被验证。
         */
        String authToken = token.substring(7);

        /**
         * 使用JwtUtil验证令牌是否有效。
         * JwtUtil类提供了JWT的生成和验证功能。
         */
        boolean isValid = JwtUntil.verifyToken(authToken);
        if (!isValid) {
            /**
             * 如果令牌无效或已过期,设置响应状态为401 Unauthorized,并返回错误信息。
             */
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("令牌无效或已过期");
            return false; // 中断请求
        }

        /**
         * 如果令牌验证成功,返回true以继续执行下一个拦截器或Controller。
         */
        return true;
    }
}

6.用户认证配置文件

SecurityConfig

// 使用@Configuration注解声明这是一个配置类,这样Spring容器就会自动扫描并加载这个类
@Configuration
// 使用@EnableWebSecurity注解启用Spring Security的Web安全功能
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 覆盖WebSecurityConfigurerAdapter的configure方法,用于配置HttpSecurity对象
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用CSRF保护,因为在REST API中通常不需要CSRF保护
        http.csrf().disable()
                // 配置请求授权规则
                .authorizeRequests()
                // 对于/login和/register路径下的请求,允许所有用户访问
                .antMatchers("/login", "/register").permitAll()
                // 其他所有请求都需要用户认证
                .anyRequest().authenticated()
                // 配置认证方式为HTTP基本认证
                .and()
                .httpBasic();
    }

    // 覆盖WebSecurityConfigurerAdapter的configure方法,用于配置AuthenticationManagerBuilder对象
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置用户详情服务和密码编码器
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    // 定义一个Bean,返回一个BCryptPasswordEncoder实例,用于密码加密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

7.自定义Mapper方法

/**
 UserMapper
 * UserMapper接口,继承自BaseMapper,提供对User实体的基本数据库操作。
 */
@Mapper // 1. 标记这个接口为MyBatis的Mapper接口,MyBatis会为这个接口生成代理实现类
public interface UserMapper extends BaseMapper<User> { // 2. 继承自BaseMapper,User表示这个Mapper操作的实体类

    // 这里可以添加UserMapper接口特有的方法,例如自定义的数据库查询方法
    // MyBatis会自动为这个接口生成一个实现类,并在内部使用XML或注解配置的SQL语句

    // 添加用户
    @Insert("INSERT INTO user(userNumber, password, username) VALUES(#{userNumber}, #{password}, #{username})")
    void registerUser(
            @Param("userNumber") String userNumber,
            @Param("password") String password,
            @Param("username") String username);



    //自定义查询账号方法
    @Select("SELECT userNumber, password FROM user WHERE userNumber = #{userNumber}")
    User selectLogin(@Param("userNumber") String userNumber);
}

8.Service业务登录方法

在此之前,,我们得先创建IUserService接口继承IService<User>

然后创建UserServiceImpi类去实现重写方法

/**
 * UserServiceImpl类,继承自ServiceImpl<UserMapper, User>并实现了IUserService接口,
 * 提供了用户服务的具体实现。
 */
@Service // 1. 标记这个类为Spring的一个Service,使其成为Spring上下文中的一个Bean
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    @Autowired
    UserMapper userMapper;
    public String login(User user) {
        User newUser = userMapper.selectLogin(user.getUserNumber());
        if (newUser != null && newUser.getPassword().equals(user.getPassword())){
            // 用户名和密码匹配,生成JWT
            return JwtUntil.createToken(user.getUserNumber());
        }
        // 验证失败
        return "用户名或密码错误";


    }
}

9.创建LoginController实现登录验证

@RestController
@RequestMapping("/login")
public class LoginController {
    @Autowired
    private UserServiceImpl userService;

    @PostMapping
    public String login(@RequestBody User user) {
        System.out.println(user);
        return userService.login(user);
    }
}

注册测试

账号密码均为123456(演示)

如图所示注册成功,我们用此账号试一试登录验证

登录验证测试

可以看到登录已经成功了,返回了一段带有请求头数据的加密token

用户名或密码错误演示

<think>好的,用户想了解如何在前端通过JWT实现Token登录验证的最佳实践。首先,我需要回顾一下JWT的基本流程。根据引用[1],后端在用户登录成功后生成JWT Token并返回给前端,前端需要存储这个Token。接下来,前端在每次请求时要携带这个Token,通常放在HTTP头里,比如Authorization: Bearer <token>。 然后,考虑前端存储Token的方式。常见的有localStorage、sessionStorage和Cookie。localStorage和sessionStorage的区别在于生命周期,前者持久存储,后者会话结束就清除。不过要注意XSS攻击的风险,所以需要确保代码的安全性,避免恶意脚本获取Token。引用[2]提到JWT不需要依赖Redis,用户信息存储在客户端,所以前端存储是关键。 接下来是Token的自动刷新问题。如果Token快过期了,前端可以发起刷新请求,获取新的Token。这需要后端支持,比如提供一个/refresh接口。引用[3]中的exp字段是过期时间,前端需要检查这个时间,并在适当的时候刷新。 另外,前端需要处理Token失效的情况,比如用户长时间不操作导致Token过期。这时候应该跳转到登录页面,并提示重新登录。还需要考虑路由守卫,在访问需要认证的路由时检查Token是否存在,否则重定向到登录页。 还要注意安全措施,比如使用HTTPS来加密传输,防止Token被截获。设置HttpOnly的Cookie可以防止XSS,但JWT通常放在Authorization头里,所以需要其他防护措施,比如内容安全策略(CSP)。 用户可能还会关心如何解析JWT获取用户信息。前端可以使用库如jwt-decode来解析Payload部分,获取用户数据,用于显示用户名、权限等。但要注意不要存储敏感信息在JWT中,因为Payload只是Base64编码,不是加密的。 最后,退出登录的处理比较简单,前端只需移除存储的Token即可,如引用[1]提到的。但如果有黑名单机制,可能需要通知后端使旧Token失效,不过这通常在后端处理。 总结步骤应该是:登录后存储Token,请求时携带,处理自动刷新,失效跳转,安全存储,退出删除。同时结合引用中的信息,确保引用正确,比如后端生成Token的部分,前端存储和传输的方式。</think>### 前端通过JWT实现Token登录验证的最佳实践 #### **1. 登录流程与Token存储** - **用户登录**:前端发送用户名密码到后端验证,成功后接收后端返回的JWT Token(格式如`lll.zzz.xxx`)[^1]。 - **Token存储**: - **localStorage/sessionStorage**:适合短期存储,需注意防范XSS攻击。示例: ```javascript // 存储Token localStorage.setItem('jwtToken', response.data.token); // 读取Token const token = localStorage.getItem('jwtToken'); ``` - **HttpOnly Cookie**:更安全但需配合跨域配置,适用于服务端渲染框架。 #### **2. 请求携带Token** 在每次请求的`Authorization`头中附加Token,格式为`Bearer <token>`: ```javascript axios.interceptors.request.use(config => { const token = localStorage.getItem('jwtToken'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); ``` #### **3. Token自动刷新** - **方案**:监听接口返回的`401`状态码,或通过定时器检测Token过期时间(解析JWT的`exp`字段)[^3]。 - **实现示例**: ```javascript // 解析Token过期时间 const decodeToken = (token) => { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000; // 转为毫秒 }; // 检查剩余时间并刷新 if (decodeToken(token) - Date.now() < 300000) { // 过期前5分钟刷新 axios.post('/refresh-token', { token }).then(res => { localStorage.setItem('jwtToken', res.data.newToken); }); } ``` #### **4. 安全防护** - **HTTPS传输**:确保所有请求通过HTTPS加密。 - **XSS防御**:避免使用`eval()`等不安全方法,对用户输入严格过滤。 - **短期有效性**:结合JWT的`exp`(过期时间)和`nbf`(生效时间)字段控制有效期。 #### **5. 用户状态管理与退出** - **全局状态**:通过Vuex/Redux等管理登录状态,或解析JWT Payload获取用户信息: ```javascript import jwtDecode from 'jwt-decode'; const userInfo = jwtDecode(token).sub; // 获取用户信息 ``` - **退出登录**:清除存储的Token并重置状态: ```javascript localStorage.removeItem('jwtToken'); // 跳转至登录页 window.location.href = '/login'; ``` #### **6. 路由守卫(以Vue Router为例)** ```javascript router.beforeEach((to, from, next) => { const isAuthenticated = localStorage.getItem('jwtToken'); if (to.meta.requiresAuth && !isAuthenticated) { next('/login'); } else { next(); } }); ``` ### **总结** 最佳实践围绕**安全存储、请求拦截、自动刷新、状态同步**展开,需结合业务需求选择存储方式,并严格防范XSS与CSRF攻击。JWT的无状态特性适合分布式系统,但需注意Token泄露风险[^2]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值