宅男在线永久免费观看网直播,亚洲欧洲日产国码无码久久99,野花社区在线观看视频,亚洲人交乣女bbw,一本一本久久a久久精品综合不卡

全部
常見問題
產(chǎn)品動(dòng)態(tài)
精選推薦

JWT 實(shí)現(xiàn)登錄認(rèn)證 + Token 自動(dòng)續(xù)期方案,這才是正確的使用姿勢(shì)!

管理 管理 編輯 刪除

w項(xiàng)目中基本都有用戶管理模塊,而用戶管理模塊會(huì)涉及到加密及認(rèn)證流程。


今天就來講講認(rèn)證功能的技術(shù)選型及實(shí)現(xiàn)。技術(shù)上沒啥難度當(dāng)然也沒啥挑戰(zhàn),但是對(duì)一個(gè)原先沒寫過認(rèn)證功能的人來說也是一種鍛煉吧


技術(shù)選型


要實(shí)現(xiàn)認(rèn)證功能,很容易就會(huì)想到JWT或者session,但是兩者有啥區(qū)別?各自的優(yōu)缺點(diǎn)?應(yīng)該P(yáng)ick誰?奪命三連


區(qū)別


基于session和基于JWT的方式的主要區(qū)別就是用戶的狀態(tài)保存的位置,session是保存在服務(wù)端的,而JWT是保存在客戶端的。


認(rèn)證流程


基于session的認(rèn)證流程


  • 用戶在瀏覽器中輸入用戶名和密碼,服務(wù)器通過密碼校驗(yàn)后生成一個(gè)session并保存到數(shù)據(jù)庫
  • 服務(wù)器為用戶生成一個(gè)sessionId,并將具有sesssionId的cookie放置在用戶瀏覽器中,在后續(xù)的請(qǐng)求中都將帶有這個(gè)cookie信息進(jìn)行訪問
  • 服務(wù)器獲取cookie,通過獲取cookie中的sessionId查找數(shù)據(jù)庫判斷當(dāng)前請(qǐng)求是否有效
基于JWT的認(rèn)證流程


  • 用戶在瀏覽器中輸入用戶名和密碼,服務(wù)器通過密碼校驗(yàn)后生成一個(gè)token并保存到數(shù)據(jù)庫
  • 前端獲取到token,存儲(chǔ)到cookie或者local storage中,在后續(xù)的請(qǐng)求中都將帶有這個(gè)token信息進(jìn)行訪問
  • 服務(wù)器獲取token值,通過查找數(shù)據(jù)庫判斷當(dāng)前token是否有效

優(yōu)缺點(diǎn)


  • JWT保存在客戶端,在分布式環(huán)境下不需要做額外工作。而session因?yàn)楸4嬖诜?wù)端,分布式環(huán)境下需要實(shí)現(xiàn)多機(jī)數(shù)據(jù)共享
  • session一般需要結(jié)合Cookie實(shí)現(xiàn)認(rèn)證,所以需要瀏覽器支持cookie,因此移動(dòng)端無法使用session認(rèn)證方案
安全性


  • JWT的payload使用的是base64編碼的,因此在JWT中不能存儲(chǔ)敏感數(shù)據(jù)。而session的信息是存在服務(wù)端的,相對(duì)來說更安全

如果在JWT中存儲(chǔ)了敏感信息,可以解碼出來非常的不安全


性能


  • 經(jīng)過編碼之后JWT將非常長(zhǎng),cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。并且用戶在系統(tǒng)中的每一次http請(qǐng)求都會(huì)把JWT攜帶在Header里面,HTTP請(qǐng)求的Header可能比Body還要大。而sessionId只是很短的一個(gè)字符串,因此使用JWT的HTTP請(qǐng)求比使用session的開銷大得多
一次性


無狀態(tài)是JWT的特點(diǎn),但也導(dǎo)致了這個(gè)問題,JWT是一次性的。想修改里面的內(nèi)容,就必須簽發(fā)一個(gè)新的JWT


  • 無法廢棄 一旦簽發(fā)一個(gè)JWT,在到期之前就會(huì)始終有效,無法中途廢棄。若想廢棄,一種常用的處理手段是結(jié)合redis。
  • 續(xù)簽 如果使用JWT做會(huì)話管理,傳統(tǒng)的cookie續(xù)簽方案一般都是框架自帶的,session有效期30分鐘,30分鐘內(nèi)如果有訪問,有效期被刷新至30分鐘。一樣的道理,要改變JWT的有效時(shí)間,就要簽發(fā)新的JWT。最簡(jiǎn)單的一種方式是每次請(qǐng)求刷新JWT,即每個(gè)HTTP請(qǐng)求都返回一個(gè)新的JWT。這個(gè)方法不僅暴力不優(yōu)雅,而且每次請(qǐng)求都要做JWT的加密解密,會(huì)帶來性能問題。另一種方法是在redis中單獨(dú)為每個(gè)JWT設(shè)置過期時(shí)間,每次訪問時(shí)刷新JWT的過期時(shí)間

選擇JWT或session


我投JWT一票,JWT有很多缺點(diǎn),但是在分布式環(huán)境下不需要像session一樣額外實(shí)現(xiàn)多機(jī)數(shù)據(jù)共享,雖然seesion的多機(jī)數(shù)據(jù)共享可以通過粘性session、session共享session復(fù)制、持久化session、terracoa實(shí)現(xiàn)seesion復(fù)制等多種成熟的方案來解決這個(gè)問題。


但是JWT不需要額外的工作,使用JWT不香嗎?且JWT一次性的缺點(diǎn)可以結(jié)合redis進(jìn)行彌補(bǔ)。揚(yáng)長(zhǎng)補(bǔ)短,因此在實(shí)際項(xiàng)目中選擇的是使用JWT來進(jìn)行認(rèn)證。


功能實(shí)現(xiàn)


JWT所需依賴



    com.auth0
    java-jwt
    3.10.3


JWT工具類


public class JWTUtil {
    private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);

    //私鑰
    private static final String TOKEN_SECRET = "123456";

    /**
     * 生成token,自定義過期時(shí)間 毫秒
     *
     * @param userTokenDTO
     * @return
     */
    public static String generateToken(UserTokenDTO userTokenDTO) {
        try {
            // 私鑰和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 設(shè)置頭部信息
            Map header = new HashMap<>(2);
            header.put("Type", "Jwt");
            header.put("alg", "HS256");

            return JWT.create()
                    .withHeader(header)
                    .withClaim("token", JSONObject.toJSONString(userTokenDTO))
                    //.withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            logger.error("generate token occur error, error is:{}", e);
            return null;
        }
    }

    /**
     * 檢驗(yàn)token是否正確
     *
     * @param token
     * @return
     */
    public static UserTokenDTO parseToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        DecodedJWT jwt = verifier.verify(token);
        String tokenInfo = jwt.getClaim("token").asString();
        return JSON.parseObject(tokenInfo, UserTokenDTO.class);
    }
}

說明:


  • 生成的token中不帶有過期時(shí)間,token的過期時(shí)間由redis進(jìn)行管理
  • UserTokenDTO中不帶有敏感信息,如password字段不會(huì)出現(xiàn)在token中

Redis工具類


public final class RedisServiceImpl implements RedisService {
    /**
     * 過期時(shí)長(zhǎng)
     */
    private final Long DURATION = 1 * 24 * 60 * 60 * 1000L;

    @Resource
    private RedisTemplate redisTemplate;

    private ValueOperations valueOperations;

    @PostConstruct
    public void init() {
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        redisTemplate.setHashValueSerializer(redisSerializer);
        valueOperations = redisTemplate.opsForValue();
    }

    @Override
    public void set(String key, String value) {
        valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS);
        log.info("key={}, value is: {} into redis cache", key, value);
    }

    @Override
    public String get(String key) {
        String redisValue = valueOperations.get(key);
        log.info("get from redis, value is: {}", redisValue);
        return redisValue;
    }

    @Override
    public boolean delete(String key) {
        boolean result = redisTemplate.delete(key);
        log.info("delete from redis, key is: {}", key);
        return result;
    }

    @Override
    public Long getExpireTime(String key) {
        return valueOperations.getOperations().getExpire(key);
    }
}

RedisTemplate簡(jiǎn)單封裝


業(yè)務(wù)實(shí)現(xiàn)


登陸功能


public String login(LoginUserVO loginUserVO) {
    //1.判斷用戶名密碼是否正確
    UserPO userPO = userMapper.getByUsername(loginUserVO.getUsername());
    if (userPO == null) {
        throw new UserException(ErrorCodeEnum.TNP1001001);
    }
    if (!loginUserVO.getPassword().equals(userPO.getPassword())) {
        throw new UserException(ErrorCodeEnum.TNP1001002);
    }

    //2.用戶名密碼正確生成token
    UserTokenDTO userTokenDTO = new UserTokenDTO();
    PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);
    userTokenDTO.setId(userPO.getId());
    userTokenDTO.setGmtCreate(System.currentTimeMillis());
    String token = JWTUtil.generateToken(userTokenDTO);

    //3.存入token至redis
    redisService.set(userPO.getId(), token);
    return token;
}

說明:


  • 判斷用戶名密碼是否正確
  • 用戶名密碼正確則生成token
  • 將生成的token保存至redis
登出功能


public boolean loginOut(String id) {
     boolean result = redisService.delete(id);
     if (!redisService.delete(id)) {
        throw new UserException(ErrorCodeEnum.TNP1001003);
     }

     return result;
}

將對(duì)應(yīng)的key刪除即可。


更新密碼功能


public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {
    //1.修改密碼
    UserPO userPO = UserPO.builder().password(updatePasswordUserVO.getPassword())
            .id(updatePasswordUserVO.getId())
            .build();
    UserPO user = userMapper.getById(updatePasswordUserVO.getId());
    if (user == null) {
        throw new UserException(ErrorCodeEnum.TNP1001001);
    }

    if (userMapper.updatePassword(userPO) != 1) {
        throw new UserException(ErrorCodeEnum.TNP1001005);
    }
    //2.生成新的token
    UserTokenDTO userTokenDTO = UserTokenDTO.builder()
            .id(updatePasswordUserVO.getId())
            .username(user.getUsername())
            .gmtCreate(System.currentTimeMillis()).build();
    String token = JWTUtil.generateToken(userTokenDTO);
    //3.更新token
    redisService.set(user.getId(), token);
    return token;
}

說明:更新用戶密碼時(shí)需要重新生成新的token,并將新的token返回給前端,由前端更新保存在local storage中的token,同時(shí)更新存儲(chǔ)在redis中的token,這樣實(shí)現(xiàn)可以避免用戶重新登陸,用戶體驗(yàn)感不至于太差。


其他說明


  • 在實(shí)際項(xiàng)目中,用戶分為普通用戶和管理員用戶,只有管理員用戶擁有刪除用戶的權(quán)限,這一塊功能也是涉及token操作的,但是我太懶了,demo工程就不寫了
  • 在實(shí)際項(xiàng)目中,密碼傳輸是加密過的

攔截器類


public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
    String authToken = request.getHeader("Authorization");
    String token = authToken.substring("Bearer".length() + 1).trim();
    UserTokenDTO userTokenDTO = JWTUtil.parseToken(token);
    //1.判斷請(qǐng)求是否有效
    if (redisService.get(userTokenDTO.getId()) == null 
            || !redisService.get(userTokenDTO.getId()).equals(token)) {
        return false;
    }

    //2.判斷是否需要續(xù)期
    if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) {
        redisService.set(userTokenDTO.getId(), token);
        log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token);
    }
    return true;
}

說明:攔截器中主要做兩件事,一是對(duì)token進(jìn)行校驗(yàn),二是判斷token是否需要進(jìn)行續(xù)期 token校驗(yàn):


  • 判斷id對(duì)應(yīng)的token是否不存在,不存在則token過期
  • 若token存在則比較token是否一致,保證同一時(shí)間只有一個(gè)用戶操作

token自動(dòng)續(xù)期: 為了不頻繁操作redis,只有當(dāng)離過期時(shí)間只有30分鐘時(shí)才更新過期時(shí)間


攔截器配置類


@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticateInterceptor())
                .excludePathPatterns("/logout/**")
                .excludePathPatterns("/login/**")
                .addPathPatterns("/**");
    }

    @Bean
    public AuthenticateInterceptor authenticateInterceptor() {
        return new AuthenticateInterceptor();
    }
}


請(qǐng)登錄后查看

CRMEB-慕白寒窗雪 最后編輯于2022-11-12 10:04:14

快捷回復(fù)
回復(fù)
回復(fù)
回復(fù)({{post_count}}) {{!is_user ? '我的回復(fù)' :'全部回復(fù)'}}
排序 默認(rèn)正序 回復(fù)倒序 點(diǎn)贊倒序

{{item.user_info.nickname ? item.user_info.nickname : item.user_name}} LV.{{ item.user_info.bbs_level || item.bbs_level }}

作者 管理員 企業(yè)

{{item.floor}}# 同步到gitee 已同步到gitee {{item.is_suggest == 1? '取消推薦': '推薦'}}
{{item.is_suggest == 1? '取消推薦': '推薦'}}
沙發(fā) 板凳 地板 {{item.floor}}#
{{item.user_info.title || '暫無簡(jiǎn)介'}}
附件

{{itemf.name}}

{{item.created_at}}  {{item.ip_address}}
打賞
已打賞¥{{item.reward_price}}
{{item.like_count}}
{{item.showReply ? '取消回復(fù)' : '回復(fù)'}}
刪除
回復(fù)
回復(fù)

{{itemc.user_info.nickname}}

{{itemc.user_name}}

回復(fù) {{itemc.comment_user_info.nickname}}

附件

{{itemf.name}}

{{itemc.created_at}}
打賞
已打賞¥{{itemc.reward_price}}
{{itemc.like_count}}
{{itemc.showReply ? '取消回復(fù)' : '回復(fù)'}}
刪除
回復(fù)
回復(fù)
查看更多
打賞
已打賞¥{{reward_price}}
2508
{{like_count}}
{{collect_count}}
添加回復(fù) ({{post_count}})

相關(guān)推薦

快速安全登錄

使用微信掃碼登錄
{{item.label}} 加精
{{item.label}} {{item.label}} 板塊推薦 常見問題 產(chǎn)品動(dòng)態(tài) 精選推薦 首頁頭條 首頁動(dòng)態(tài) 首頁推薦
取 消 確 定
回復(fù)
回復(fù)
問題:
問題自動(dòng)獲取的帖子內(nèi)容,不準(zhǔn)確時(shí)需要手動(dòng)修改. [獲取答案]
答案:
提交
bug 需求 取 消 確 定
打賞金額
當(dāng)前余額:¥{{rewardUserInfo.reward_price}}
{{item.price}}元
請(qǐng)輸入 0.1-{{reward_max_price}} 范圍內(nèi)的數(shù)值
打賞成功
¥{{price}}
完成 確認(rèn)打賞

微信登錄/注冊(cè)

切換手機(jī)號(hào)登錄

{{ bind_phone ? '綁定手機(jī)' : '手機(jī)登錄'}}

{{codeText}}
切換微信登錄/注冊(cè)
暫不綁定
CRMEB客服

CRMEB咨詢熱線 咨詢熱線

400-8888-794

微信掃碼咨詢

CRMEB開源商城下載 源碼下載 CRMEB幫助文檔 幫助文檔
返回頂部 返回頂部
CRMEB客服