欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

springboot+vue3無感知刷新token實(shí)戰(zhàn)教程

 更新時(shí)間:2025年03月05日 09:29:30   作者:李晨豪 Lch  
本文介紹了基于Spring Boot和Vue3的無感知刷新Token的實(shí)現(xiàn),包括后端token構(gòu)造和刷新邏輯,以及前端的請(qǐng)求處理和緩存機(jī)制

web網(wǎng)站中,前后端交互時(shí),通常使用token機(jī)制來做認(rèn)證,token一般會(huì)設(shè)置有效期,當(dāng)token過了有效期后,用戶需要重新登錄授權(quán)獲取新的token,但是某些業(yè)務(wù)場(chǎng)景下,用戶不希望頻繁的進(jìn)行登錄授權(quán),但是安全考慮,token的有效期不能設(shè)置太長(zhǎng)時(shí)間,所以有了刷新token的設(shè)計(jì),無感知刷新token的機(jī)制更進(jìn)一步優(yōu)化了用戶體驗(yàn),本文是博主實(shí)際業(yè)務(wù)項(xiàng)目中基于springboot和vue3無感知刷新token的代碼實(shí)戰(zhàn)。

首先介紹無感知刷新token的實(shí)現(xiàn)思路:

①首次授權(quán)頒發(fā)token時(shí),我們通過后端給前端請(qǐng)求response中寫入兩種cookie

  • - access_token
  • - refresh_token(超時(shí)時(shí)間比access_token長(zhǎng)一些)

需要注意:

-后端setCookie時(shí)httpOnly=true(限制cookie只能被http請(qǐng)求攜帶使用,不能被js操作)

-前端axios請(qǐng)求參數(shù)withCredentials=true(http請(qǐng)求時(shí),自動(dòng)攜帶token)

  • ②access_token失效時(shí),拋出特殊異常,前后端約定http響應(yīng)碼(401),此時(shí)觸發(fā)刷新token邏輯
  • ③前段http請(qǐng)求鉤子中,如果出現(xiàn)http響應(yīng)碼為401時(shí),立即觸發(fā)刷新token邏輯,同時(shí)緩存后續(xù)請(qǐng)求,刷新token結(jié)束后,依次續(xù)發(fā)緩存中的請(qǐng)求

一、java后端

后端java框架使用springboot,spring-security

登錄接口:

/**
 * @author lichenhao
 * @date 2023/2/8 17:41
 */
@RestController
public class AuthController {

    /**
     * 登錄方法
     *
     * @param loginBody 登錄信息
     * @return 結(jié)果
     */
    @PostMapping("/oauth")
    public AjaxResult login(@RequestBody LoginBody loginBody) {
        ITokenGranter granter = TokenGranterBuilder.getGranter(loginBody.getGrantType());
        return granter.grant(loginBody);
    }
}


import lombok.Data;

/**
 * 用戶登錄對(duì)象
 *
 * @author lichenhao
 */
@Data
public class LoginBody {

    /**
     * 用戶名
     */
    private String username;

    /**
     * 用戶密碼
     */
    private String password;

    /**
     * 驗(yàn)證碼
     */
    private String code;

    /**
     * 唯一標(biāo)識(shí)
     */
    private String uuid;

    /*
     * grantType 授權(quán)類型
     * */
    private String grantType;

    /*
    * 是否直接強(qiáng)退該賬號(hào)登陸的其他客戶端
    * */
    private Boolean forceLogoutFlag;
}

token構(gòu)造接口類和token實(shí)現(xiàn)類構(gòu)造器如下:

/**
 * @author lichenhao
 * @date 2023/2/8 17:29
 * <p>
 * 獲取token
 */
public interface ITokenGranter {

    AjaxResult grant(LoginBody loginBody);
}


/**
 * @author lichenhao
 * @date 2023/2/8 17:29
 */
@AllArgsConstructor
public class TokenGranterBuilder {

    /**
     * TokenGranter緩存池
     */
    private static final Map<String, ITokenGranter> GRANTER_POOL = new ConcurrentHashMap<>();

    static {
        GRANTER_POOL.put(CaptchaTokenGranter.GRANT_TYPE, SpringUtils.getBean(CaptchaTokenGranter.class));
        GRANTER_POOL.put(RefreshTokenGranter.GRANT_TYPE, SpringUtils.getBean(RefreshTokenGranter.class));
    }

    /**
     * 獲取TokenGranter
     *
     * @param grantType 授權(quán)類型
     * @return ITokenGranter
     */
    public static ITokenGranter getGranter(String grantType) {
        ITokenGranter tokenGranter = GRANTER_POOL.get(StringUtils.toStr(grantType, PasswordTokenGranter.GRANT_TYPE));
        if (tokenGranter == null) {
            throw new ServiceException("no grantType was found");
        } else {
            return tokenGranter;
        }
    }

}

這里通過LoginBody的grantType屬性,指定實(shí)際的token構(gòu)造實(shí)現(xiàn)類;同時(shí),需要有token

本文我們用到了驗(yàn)證碼方式和刷新token方式,如下:

1、token構(gòu)造實(shí)現(xiàn)類

①驗(yàn)證碼方式實(shí)現(xiàn)類

/**
 * @author lichenhao
 * @date 2023/2/8 17:32
 */
@Component
public class CaptchaTokenGranter implements ITokenGranter {

    public static final String GRANT_TYPE = "captcha";

    @Autowired
    private SysLoginService loginService;

    @Override
    public AjaxResult grant(LoginBody loginBody) {
        String username = loginBody.getUsername();
        String code = loginBody.getCode();
        String password = loginBody.getPassword();
        String uuid = loginBody.getUuid();
        Boolean forceLogoutFlag = loginBody.getForceLogoutFlag();

        AjaxResult ajaxResult = validateLoginBody(username, password, code, uuid);
        // 驗(yàn)證碼
        loginService.validateCaptcha(username, code, uuid);
        // 登錄
        loginService.login(username, password, uuid, forceLogoutFlag);
        // 刪除驗(yàn)證碼
        loginService.deleteCaptcha(uuid);
        return ajaxResult;
    }

    private AjaxResult validateLoginBody(String username, String password, String code, String uuid) {
        if (StringUtils.isBlank(username)) {
            return AjaxResult.error("用戶名必填");
        }
        if (StringUtils.isBlank(password)) {
            return AjaxResult.error("密碼必填");
        }
        if (StringUtils.isBlank(code)) {
            return AjaxResult.error("驗(yàn)證碼必填");
        }
        if (StringUtils.isBlank(uuid)) {
            return AjaxResult.error("uuid必填");
        }
        return AjaxResult.success();
    }
}


    /**
     * 登錄驗(yàn)證
     *
     * @param username 用戶名
     * @param password 密碼
     * @return 結(jié)果
     */
    public void login(String username, String password, String uuid, Boolean forceLogoutFlag) {
        // 校驗(yàn)basic auth
        IClientDetails iClientDetails = tokenService.validBasicAuth();
        // 用戶驗(yàn)證
        Authentication authentication = null;
        try {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 該方法會(huì)去調(diào)用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        } catch (Exception e) {
            if (e instanceof BadCredentialsException) {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            } else {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        } finally {
            AuthenticationContextHolder.clearContext();
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        tokenService.setUserAgent(loginUser);
        Long customerId = loginUser.getUser().getCustomerId();
        Boolean singleClientFlag = SystemConfig.isSingleClientFlag();
        if(customerId != null){
            Customer customer = customerService.selectCustomerById(customerId);
            singleClientFlag = customer.getSingleClientFlag();
            log.info(String.format("客戶【%s】單賬號(hào)登錄限制開關(guān):%s", customer.getCode(), singleClientFlag));
        }
        if(singleClientFlag){
            List<SysUserOnline> userOnlineList = userOnlineService.getUserOnlineList(null, username);
            if(CollectionUtils.isNotEmpty(userOnlineList)){
                if(forceLogoutFlag != null && forceLogoutFlag){
                    // 踢掉其他使用該賬號(hào)登陸的客戶端
                    userOnlineService.forceLogoutBySysUserOnlineList(userOnlineList);
                }else{
                    throw new ServiceException("【" + username + "】已登錄,是否仍然登陸", 400);
                }
            }
        }
        // 生成token
        tokenService.createToken(iClientDetails, loginUser, uuid);
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        recordLoginInfo(loginUser.getUserId());
    }

②刷新token方式實(shí)現(xiàn)類

/**
 * @author lichenhao
 * @date 2023/2/8 17:35
 */
@Component
public class RefreshTokenGranter implements ITokenGranter {

    public static final String GRANT_TYPE = "refresh_token";

    @Autowired
    private TokenService tokenService;

    @Override
    public AjaxResult grant(LoginBody loginBody) {
        tokenService.refreshToken();
        return AjaxResult.success();
    }
}

2、token相關(guān)操作:setCookie

①createToken

    /**
     * 創(chuàng)建令牌
     * 注意:access_token和refresh_token 使用同一個(gè)tokenId
     */
    public void createToken(IClientDetails clientDetails, LoginUser loginUser, String tokenId) {

        if(loginUser == null){
            throw new ForbiddenException("用戶信息無效,請(qǐng)重新登陸!");
        }

        loginUser.setTokenId(tokenId);

        String username = loginUser.getUsername();
        String clientId = clientDetails.getClientId();

        // 設(shè)置jwt要攜帶的用戶信息
        Map<String, Object> claimsMap = new HashMap<>();
        initClaimsMap(claimsMap, loginUser);

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        int accessTokenValidity = clientDetails.getAccessTokenValidity();
        long accessTokenExpMillis = nowMillis + accessTokenValidity * MILLIS_SECOND;
        Date accessTokenExpDate = new Date(accessTokenExpMillis);
        String accessToken = createJwtToken(SecureConstant.ACCESS_TOKEN, accessTokenExpDate, now, JWT_TOKEN_SECRET, claimsMap, clientId, tokenId, username);

        int refreshTokenValidity = clientDetails.getRefreshTokenValidity();
        long refreshTokenExpMillis = nowMillis + refreshTokenValidity * MILLIS_SECOND;
        Date refreshTokenExpDate = new Date(refreshTokenExpMillis);
        String refreshToken = createJwtToken(SecureConstant.REFRESH_TOKEN, refreshTokenExpDate, now, JWT_REFRESH_TOKEN_SECRET, claimsMap, clientId, tokenId, username);

        // 寫入cookie中
        HttpServletResponse response = ServletUtils.getResponse();
        WebUtil.setCookie(response, SecureConstant.ACCESS_TOKEN, accessToken, accessTokenValidity);
        WebUtil.setCookie(response, SecureConstant.REFRESH_TOKEN, refreshToken, refreshTokenValidity);

        //插入緩存(過期時(shí)間為最長(zhǎng)過期時(shí)間=refresh_token的過期時(shí)間 理論上,保持操作的情況下,一直會(huì)被刷新)
        loginUser.setLoginTime(nowMillis);
        loginUser.setExpireTime(refreshTokenExpMillis);
        updateUserCache(loginUser);
    }

    private void initClaimsMap(Map<String, Object> claims, LoginUser loginUser) {
        // 添加jwt自定義參數(shù)
    }

    /**
     * 生成jwt token
     *
     * @param jwtTokenType token類型:access_token、refresh_token
     * @param expDate      token過期日期
     * @param now          當(dāng)前日期
     * @param signKey      簽名key
     * @param claimsMap    jwt自定義信息(可攜帶額外的用戶信息)
     * @param clientId     應(yīng)用id
     * @param tokenId      token的唯一標(biāo)識(shí)(建議同一組 access_token、refresh_token 使用一個(gè))
     * @param subject      jwt下發(fā)的用戶標(biāo)識(shí)
     * @return token字符串
     */
    private String createJwtToken(String jwtTokenType, Date expDate, Date now, String signKey, Map<String, Object> claimsMap, String clientId, String tokenId, String subject) {

        JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
                .setId(tokenId)
                .setSubject(subject)
                .signWith(SignatureAlgorithm.HS512, signKey);

        //設(shè)置JWT參數(shù)(user維度)
        claimsMap.forEach(jwtBuilder::claim);

        //設(shè)置應(yīng)用id
        jwtBuilder.claim(SecureConstant.CLAIMS_CLIENT_ID, clientId);

        //設(shè)置token type
        jwtBuilder.claim(SecureConstant.CLAIMS_TOKEN_TYPE, jwtTokenType);

        //添加Token過期時(shí)間
        jwtBuilder.setExpiration(expDate).setNotBefore(now);
        return jwtBuilder.compact();
    }

    /*
     * 更新緩存中的用戶信息
     * */
    public void updateUserCache(LoginUser loginUser) {
        // 根據(jù)tokenId將loginUser緩存
        String userKey = getTokenKey(loginUser.getTokenId());
        redisService.setCacheObject(userKey, loginUser, parseIntByLong(loginUser.getExpireTime() - loginUser.getLoginTime()), TimeUnit.MILLISECONDS);
    }

    private String getTokenKey(String uuid) {
        return "login_tokens:" + uuid;
    }

②refreshToken

    /**
     * 刷新令牌有效期
     */
    public void refreshToken() {
        // 從cookie中拿到refreshToken
        String refreshToken = WebUtil.getCookieVal(ServletUtils.getRequest(), SecureConstant.REFRESH_TOKEN);
        if (StringUtils.isBlank(refreshToken)) {
            throw new ForbiddenException("認(rèn)證失敗!");
        }
        // 驗(yàn)證 refreshToken 是否有效
        Claims claims = parseToken(refreshToken, JWT_REFRESH_TOKEN_SECRET);
        if (claims == null) {
            throw new ForbiddenException("認(rèn)證失??!");
        }
        String clientId = StringUtils.toStr(claims.get(SecureConstant.CLAIMS_CLIENT_ID));
        String tokenId = claims.getId();
        LoginUser loginUser = getLoginUserByTokenId(tokenId);
        if(loginUser == null){
            throw new ForbiddenException("用戶信息無效,請(qǐng)重新登陸!");
        }
        IClientDetails clientDetails = getClientDetailsService().loadClientByClientId(clientId);
        // 刪除原token緩存
        delLoginUserCache(tokenId);
        // 重新生成token
        createToken(clientDetails, loginUser, IdUtils.simpleUUID());
    }

    /**
     * 根據(jù)tokenId獲取用戶信息
     *
     * @return 用戶信息
     */
    public LoginUser getLoginUserByTokenId(String tokenId) {
        String userKey = getTokenKey(tokenId);
        LoginUser user = redisService.getCacheObject(userKey);
        return user;
    }

    /**
     * 刪除用戶緩存
     */
    public void delLoginUserCache(String tokenId) {
        if (StringUtils.isNotEmpty(tokenId)) {
            String userKey = getTokenKey(tokenId);
            redisService.deleteObject(userKey);
        }
    }

③異常碼

  • 401:access_token無效,開始刷新token邏輯
  • 403:refresh_token無效,或者其他需要跳轉(zhuǎn)登錄頁(yè)面的場(chǎng)景

二、前端(vue3+axios)

// 創(chuàng)建axios實(shí)例
const service = axios.create({
    // axios中請(qǐng)求配置有baseURL選項(xiàng),表示請(qǐng)求URL公共部分
    baseURL: import.meta.env.VITE_APP_BASE_API,
    // 超時(shí)
    timeout: 120000,
    withCredentials: true
})

// request攔截器
service.interceptors.request.use(config => {
    // do something
    return config
}, error => {

})


// 響應(yīng)攔截器
service.interceptors.response.use(res => {
        loadingInstance?.close()
        loadingInstance = null
        // 未設(shè)置狀態(tài)碼則默認(rèn)成功狀態(tài)
        const code = res.data.code || 200;
        // 獲取錯(cuò)誤信息
        const msg = errorCode[code] || res.data.msg || errorCode['default']
        if (code === 500) {
            ElMessage({message: msg, type: 'error'})
            return Promise.reject(new Error(msg))
        } else if (code === 401) {
            return refreshFun(res.config);
        } else if (code === 601) {
            ElMessage({message: msg, type: 'warning'})
            return Promise.reject(new Error(msg))
        } else if (code == 400) {
            // 需要用戶confirm是否強(qiáng)制登陸
            return Promise.resolve(res.data)
        } else if (code !== 200) {
            ElNotification.error({title: msg})
            return Promise.reject('error')
        } else {
            return Promise.resolve(res.request.responseType === 'blob' ? res : res.data)
        }
    },
    error => {
        loadingInstance?.close()
        loadingInstance = null
        if (error.response.status == 401) {
            return refreshFun(error.config);
        }
        let {message} = error;
        if (message == "Network Error") {
            message = "后端接口連接異常";
        } else if (message.includes("timeout")) {
            message = "系統(tǒng)接口請(qǐng)求超時(shí)";
        } else {
            message = error.response.data ? error.response.data.msg : 'message'
        }
        ElMessage({message: message, type: 'error', duration: 5 * 1000})
        return Promise.reject(error)
    }
)

// 正在刷新標(biāo)識(shí),避免重復(fù)刷新
let refreshing = false;
// 請(qǐng)求等待隊(duì)列
let waitQueue = [];

function refreshFun(config) {
    if (refreshing == false) {
        refreshing = true;
        return useUserStore().refreshToken().then(() => {
            waitQueue.forEach(callback => callback()); // 已成功刷新token,隊(duì)列中的所有請(qǐng)求重試
            waitQueue = [];
            refreshing = false;
            return service(config)
        }).catch((err) => {
            waitQueue = [];
            refreshing = false;
            if (err.response) {
                if (err.response.status === 403) {
                    ElMessageBox.confirm('登錄狀態(tài)已過期(認(rèn)證失?。?,您可以繼續(xù)留在該頁(yè)面,或者重新登錄', '系統(tǒng)提示', {
                        confirmButtonText: '重新登錄',
                        cancelButtonText: '取消',
                        type: 'warning'
                    }).then(() => {
                        useUserStore().logoutClear();
                        router.push(`/login`);
                    }).catch(() => {

                    });
                    return Promise.reject()
                } else {
                    console.log('err:' + (err.response && err.response.data.msg) ? err.response.data.msg : err)
                }
            } else {
                ElMessage({
                    message: err.message,
                    type: 'error',
                    duration: 5 * 1000
                })
            }
        })
    } else {
        // 正在刷新token,返回未執(zhí)行resolve的Promise,刷新token執(zhí)行回調(diào)
        return new Promise((resolve => {
            waitQueue.push(() => {
                resolve(service(config))
            })
        }))
    }
}

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java中與數(shù)字相關(guān)的常用類的用法詳解

    Java中與數(shù)字相關(guān)的常用類的用法詳解

    在我們的代碼中,經(jīng)常會(huì)遇到一些數(shù)字&數(shù)學(xué)問題、隨機(jī)數(shù)問題、日期問題和系統(tǒng)設(shè)置問題等,為了解決這些問題,Java給我們提供了多個(gè)處理相關(guān)問題的類,比如Number類、Math類、Random類等等,本篇文章我們先從Number數(shù)字類和Math數(shù)學(xué)類學(xué)起
    2023-05-05
  • Java 實(shí)戰(zhàn)項(xiàng)目之誠(chéng)途旅游系統(tǒng)的實(shí)現(xiàn)流程

    Java 實(shí)戰(zhàn)項(xiàng)目之誠(chéng)途旅游系統(tǒng)的實(shí)現(xiàn)流程

    讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SpringBoot+Vue+maven+Mysql實(shí)現(xiàn)一個(gè)精美的物流管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平
    2021-11-11
  • Spring Boot 自動(dòng)配置的實(shí)現(xiàn)

    Spring Boot 自動(dòng)配置的實(shí)現(xiàn)

    這篇文章主要介紹了Spring Boot 自動(dòng)配置的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-08-08
  • SpringBoot?整合?ShardingSphere4.1.1實(shí)現(xiàn)分庫(kù)分表功能

    SpringBoot?整合?ShardingSphere4.1.1實(shí)現(xiàn)分庫(kù)分表功能

    ShardingSphere是一套開源的分布式數(shù)據(jù)庫(kù)中間件解決方案組成的生態(tài)圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(計(jì)劃中)這3款相互獨(dú)立的產(chǎn)品組成,本文給大家介紹SpringBoot?整合?ShardingSphere4.1.1實(shí)現(xiàn)分庫(kù)分表,感興趣的朋友一起看看吧
    2023-12-12
  • Java中抽象類的作用及說明

    Java中抽象類的作用及說明

    這篇文章主要介紹了Java中抽象類的作用及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • springCloud項(xiàng)目搭建流程步驟分解

    springCloud項(xiàng)目搭建流程步驟分解

    SpringCloud 作為當(dāng)下最為流行的微服務(wù)框架,也越來越多的人去學(xué)習(xí)和使用這個(gè)框架。下面,我將帶大家簡(jiǎn)單地認(rèn)識(shí)一下 SpringCloud 框架,以及如何來搭建一個(gè) SpringCloud 項(xiàng)目環(huán)境的教程
    2022-05-05
  • springboot整合Quartz實(shí)現(xiàn)動(dòng)態(tài)配置定時(shí)任務(wù)的方法

    springboot整合Quartz實(shí)現(xiàn)動(dòng)態(tài)配置定時(shí)任務(wù)的方法

    本篇文章主要介紹了springboot整合Quartz實(shí)現(xiàn)動(dòng)態(tài)配置定時(shí)任務(wù)的方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下
    2017-10-10
  • java中類和對(duì)象的知識(shí)點(diǎn)總結(jié)

    java中類和對(duì)象的知識(shí)點(diǎn)總結(jié)

    在本篇文章里小編給大家整理了一篇關(guān)于java中類和對(duì)象的知識(shí)點(diǎn)總結(jié),有需要的朋友們可以學(xué)習(xí)下。
    2020-12-12
  • Java實(shí)現(xiàn)AI五子棋游戲的示例代碼

    Java實(shí)現(xiàn)AI五子棋游戲的示例代碼

    本文只是介紹五子棋AI的實(shí)現(xiàn),最終的成品只是一個(gè)?AI?接口,并不包括?GUI,且不依賴?GUI,文中的示例代碼講解詳細(xì),感興趣的可以嘗試一下
    2022-09-09
  • 詳解SpringBoot+Thymeleaf 基于HTML5的現(xiàn)代模板引擎

    詳解SpringBoot+Thymeleaf 基于HTML5的現(xiàn)代模板引擎

    本篇文章主要介紹了SpringBoot+Thymeleaf 基于HTML5的現(xiàn)代模板引擎,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-10-10

最新評(píng)論