双 Token 无感刷新机制实现

admin2024-07-07  38

双 Token 无感刷新机制实现

      • 后端依赖
      • 安全配置
      • Jwt过滤器 *
      • 前端的配置

后端依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <!-- 其他依赖 -->
</dependencies>

安全配置

配置Jwt过滤器,以及认证失败过滤器。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;
	/**
	* Jwt过滤器
	*/
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .cors().and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .headers().cacheControl().disable().and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers("/user/login", "/user/forgetPassword/**", "/user/sendUpdatePasswordEmailCode/**", "/user/register", "/swagger-ui.html", "/user/sendEmailLoginCode", "/user/verifyEmailLoginCode/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();

        return http.build();
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 使用BCryptPasswordEncoder作为security默认的passwordEncoder
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Jwt过滤器 *

这个过滤器是实现token刷新机制的核心,每次前端的请求携带accessToken与refreshToken过来,此过滤器拿到之后,先对accessToken进行解析,如果解析失败(过期),那么接下来会对refreshToken进行解析,解析完成之后,如果没有过期,就会生成新的accessToken与refreshToken返回给前端,并且设置一个新的请求头Token-Refreshed,值可以随便设,前端能拿到就好。

package com.hblog.backend.config;

import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hblog.backend.entity.LoginUser;
import com.hblog.backend.entity.User;
import com.hblog.backend.exception.BusinessException;
import com.hblog.backend.exception.EnumException;
import com.hblog.backend.mapper.IUserMapper;
import com.hblog.backend.response.CommonResponse;
import com.hblog.backend.utli.*;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName: JwtAuthenticationFilter
 * @author: Hhzzy99
 * @date: 2024/3/17 16:09
 * description:继承每个请求只会经过一次的过滤器
 */
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Value("${token.expiration}")
    private Long expiration;

    @Autowired
    private IUserMapper userMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 获取当前请求路径
        String requestPath = request.getRequestURI();

        // 排除不需要过滤的路径
        if (requestPath.equals("/user/login") || requestPath.equals("/user/register")) {
            filterChain.doFilter(request, response);
            return;
        }

        // 获取token
        String accessToken = request.getHeader("access_token");
        String refreshToken = request.getHeader("refresh_token");
        if ("null".equals(accessToken) || "".equals(accessToken) || "undefined".equals(accessToken) || null == accessToken) {
            // 放行
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userId = "";
        boolean isRefresh = false;

        try {
            userId = JwtUtils.parseJWT(accessToken).getSubject();
        } catch (Exception e) {
            isRefresh = true;
            e.printStackTrace();
        }
        if (isRefresh) {
            try {
                userId = JwtUtils.parseJWT(refreshToken).getSubject();
                accessToken = JwtUtils.createJWT(userId);
                refreshToken = JwtUtils.createRefreshToken(userId);
                User loginUser = userMapper.getUserById(Long.valueOf(userId));
                Integer ttl = expiration.intValue() / 1000;
                log.warn("@@@@@@@@@@@@@@@@@@刷新token@@@@@@@@@@@@@@@@@@@@@");
                redisCache.setCacheObject("userInfo:" + userId, loginUser, ttl, TimeUnit.SECONDS);
                writeTokenResponse(response, accessToken, refreshToken);
                return;
            } catch (Exception e1) {
                throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);
            }
        }
        // 从redis里面获取用户信息
        User loginUser = redisCache.getCacheObject("userInfo:" + userId);
        if (Objects.isNull(loginUser)) {
            throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);
        }
        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);
    }

    private void writeTokenResponse(HttpServletResponse response, String accessToken, String refreshToken) throws IOException {
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("accessToken", accessToken);
        tokenMap.put("refreshToken", refreshToken);
        Map<String, String> headers = new HashMap<>();
        headers.put("Token-Refreshed", "true");
        CommonResponse<Map<String, String>> commonResponse = new CommonResponse<>(200, "Token refreshed successfully", tokenMap);
        WebUtil.renderString(response, headers, JSON.toJSONString(commonResponse));
    }
}


前端的配置

前端对所有的axios请求进行全局配置,先在每次请求的时候设置好请求头accessToken与refreshToken,并且将每次请求都保存起来,如果在请求时后端解析到accessToken失效,并且返回了新的accessToken与refreshToken,在请求头拿到了后端设置好的Token-Refreshed,此时就可以重新将新的accessToken与refreshToken保存在浏览器本地,并且重新发送之前保存好的请求,就可以实现无感刷新。

request.js

import axios from 'axios'
import {ref} from "vue";

// create an axios instance
const service = axios.create({
    baseURL: '/api', // url = base url + request url
    timeout: 20000 // request timeout
})

const retryRequest = ref(null)

// request interceptor
service.interceptors.request.use(
    config => {
        // 加入头信息配置
        if (localStorage.getItem("access_token") !== null && localStorage.getItem("access_token") !== undefined){
            config.headers['access_token'] = localStorage.getItem("access_token")
        }
        if (localStorage.getItem("refresh_token") !== null && localStorage.getItem("refresh_token") !== undefined){
            config.headers['refresh_token'] = localStorage.getItem("refresh_token")
        }
        retryRequest.value = config
        return config
    }
)

// response interceptor
service.interceptors.response.use(
    response => {
        if (response.headers['token-refreshed']) {
            console.log('Token刷新成功');
            // 如果有Token-Refreshed头部,更新本地存储中的Token
            localStorage.setItem('access_token', response.data.data.accessToken);
            localStorage.setItem('refresh_token', response.data.data.refreshToken);
            console.log("继续")
            // 继续发送原始请求
            return axios(retryRequest.value)
        }
        return response;
    },
    async error => {
        const originalRequest = error.config;

        // 如果是Token过期导致的401错误,并且没有retry标记,尝试刷新Token
        if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;

            const refreshToken = localStorage.getItem('refresh_token');
            if (refreshToken) {
                try {
                    const response = await axios.post('/refresh-token', { refreshToken });
                    const { accessToken, refreshToken: newRefreshToken } = response.data.data;
                    localStorage.setItem('access_token', accessToken);
                    localStorage.setItem('refresh_token', newRefreshToken);

                    // 更新原始请求的Authorization头部
                    originalRequest.headers['access_token'] = accessToken;
                    // 重新发送原始请求
                    return instance(originalRequest);
                } catch (refreshError) {
                    // 刷新Token失败,跳转到登录页或执行其他处理
                    console.error('Token刷新失败:', refreshError);
                    // 这里可以跳转到登录页或者执行其他处理
                }
            }
        }

        return Promise.reject(error);
    }
)

export default service



通过以上步骤,我们就可以实现双 Token 无感刷新机制。该机制通过短期有效的访问 Token 和长期有效的刷新 Token 相结合,在 Token 过期时自动刷新。

本示例仅展示了基础的实现方式,实际生产环境中还需要考虑更多安全性和健壮性的问题。

希望这篇文章能帮助你更好地理解和实现 JWT 双 Token 无感刷新机制。如有任何问题或建议,欢迎在评论区留言讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!