2021-05-10 JWT集成

簡(jiǎn)介

JSON Web token簡(jiǎn)稱JWT康聂, 是用于對(duì)應(yīng)用程序上的用戶進(jìn)行身份驗(yàn)證的標(biāo)記枯饿。也就是說(shuō), 使用 JWTS 的應(yīng)用程序不再需要保存有關(guān)其用戶的 cookie 或其他session數(shù)據(jù)拘荡。此特性便于可伸縮性, 同時(shí)保證應(yīng)用程序的安全勿锅。

在身份驗(yàn)證過(guò)程中, 當(dāng)用戶使用其憑據(jù)成功登錄時(shí), 將返回 JSON Web token, 并且必須在本地保存 (通常在本地存儲(chǔ)中)荧缘。每當(dāng)用戶要訪問(wèn)受保護(hù)的路由或資源 (端點(diǎn)) 時(shí), 用戶代理(user agent)必須連同請(qǐng)求一起發(fā)送 JWT, 通常在授權(quán)標(biāo)頭中使用Bearer schema皆警。后端服務(wù)器接收到帶有 JWT 的請(qǐng)求時(shí), 首先要做的是驗(yàn)證token。

JWT的格式

JWT就是一個(gè)字符串截粗,經(jīng)過(guò)加密處理與校驗(yàn)處理的字符串信姓,形式為:A.B.C
A由JWT頭部信息header加密得到
B由JWT用到的身份驗(yàn)證信息json數(shù)據(jù)加密得到
C由A和B加密得到,是校驗(yàn)部分

背景

項(xiàng)目需要APP端與服務(wù)端Token驗(yàn)證绸罗。由于服務(wù)端包含pc端服務(wù)意推,且只有APP端服務(wù)需要token驗(yàn)證,增加過(guò)濾器和攔截器配置珊蟀,攔截APP端服務(wù)菊值。

集成

1. Application.yml 配置屬性jwt屬性(secret 加密鹽外驱、expire過(guò)期時(shí)間等)
 <!-- JWT驗(yàn)證 https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
 <dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>
jwt:
  secret: A0B1C2D3E4F5G6H*********************** # 加密yan
  expire: 300000 # tocken 過(guò)期時(shí)間,單位秒
  authorised-urls: /app/** # 需要認(rèn)證的url腻窒,多個(gè)URL使用英文逗號(hào),分割
2. 啟動(dòng)時(shí)配置類JwtConfig獲取jwt屬性并注冊(cè)JwtUtil Bean
@Configuration("myJwtConfig")
public class JwtConfig {
    // 加密鹽
    @Value("${jwt.secret}")
    private String secret;

    // 過(guò)期時(shí)間
    @Value("${jwt.expire}")
    private long expire;

    // 授權(quán)路徑
    @Value("${jwt.authorised-urls}")
    private String[] authorisedUrls;

    @Bean
    public JwtUtil jwtUtilBean() {
        return new JwtUtil(secret, expire);
    }

    @Bean
    public FilterRegistrationBean basicFilterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        JwtFilter filter = new JwtFilter(jwtUtilBean(), authorisedUrls);
        registrationBean.setFilter(filter);
        List<String> urlPatterns = new ArrayList<>();
        urlPatterns.add("/app/*"); // 攔截路徑
        registrationBean.setUrlPatterns(urlPatterns);
        return registrationBean;
    }
}
public class JwtUtil {
    private Long EXPIRATION_TIME;
    private String SECRET;

    public JwtUtil(String secret, long expire) {
        this.EXPIRATION_TIME = expire;
        this.SECRET = secret;
    }

    /**
     * 為指定用戶生成token
     *
     * @param claims 用戶信息
     * @return token
     */
    public JSONObject generateToken(Map<String, Object> claims) {
        JSONObject json = new JSONObject();
        json.put(AppConstants.HEADER_TOKEN, AppConstants.TOKEN_PREFIX + " " + generateJwt(claims));
        return json;
    }

    /**
     * 為指定用戶生成jwt
     * @param claims 用戶信息
     * @return jwt
     */
    public String generateJwt(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                // 計(jì)算過(guò)期時(shí)間
                .setExpiration(new Date(System.currentTimeMillis() + this.EXPIRATION_TIME * 1000))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    /**
     * 從token中獲取claim
     */
    public Claims getClaimsFromToken(String token) {
        System.out.println("token is:" + token);
        if (token == null) {
            return null;
        }
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token.replace(AppConstants.TOKEN_PREFIX, ""))
                .getBody();
    }

    /**
     * 獲取token的過(guò)期時(shí)間
     *
     * @param token token
     * @return 過(guò)期時(shí)間
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token)
                .getExpiration();
    }

    /**
     * 判斷token是否過(guò)期
     *
     * @param token token
     * @return 已過(guò)期返回true昵宇,未過(guò)期返回false
     */
    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 判斷token是否非法
     *
     * @param token token
     * @return 未過(guò)期返回true,否則返回false
     */
    public Boolean validateToken(String token) {
        return !isTokenExpired(token);
    }

//  public static void main(String[] args) throws Exception {
//      // 1. 初始化
//      JwtUtil jwtOperator = new JwtUtil("aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt", 1209600L);
//
//      // 2.設(shè)置用戶信息
//      HashMap<String, Object> objectObjectHashMap = new HashMap();
//      objectObjectHashMap.put("id", "1");
//
//      // 測(cè)試1: 生成token
//      String token = jwtOperator.generateToken(objectObjectHashMap);
//      // 會(huì)生成類似該字符串的內(nèi)容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
//      System.out.println(token);
//
//      // 將我改成上面生成的token!!!
//      String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ";
//      // 測(cè)試2: 如果能token合法且未過(guò)期儿子,返回true
//      Boolean validateToken = jwtOperator.validateToken(someToken);
//      System.out.println(validateToken);
//
//      // 測(cè)試3: 獲取用戶信息
//      Claims claims = jwtOperator.getClaimsFromToken(someToken);
//      System.out.println(claims);
//
//      // 將我改成你生成的token的第一段(以.為邊界)
//      String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
//      // 測(cè)試4: 解密Header
//      byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
//      System.out.println(new String(header));
//
//      // 將我改成你生成的token的第二段(以.為邊界)
//      String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
//      // 測(cè)試5: 解密Payload
//      byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
//      System.out.println(new String(payload));
//
//      // 測(cè)試6: 這是一個(gè)被篡改的token瓦哎,因此會(huì)報(bào)異常,說(shuō)明JWT是安全的
//      jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
//  }
}
3. AppLoginController調(diào)用JwtUtil的方法生成Token(可以攜帶一些必要參數(shù)柔逼,如登錄人信息蒋譬,單位信息等)
Map<String, Object> claims = new HashMap<>();
// 部門信息
Organization orgInfo = RightProxy.getPartyDataService().getEmployeeOrg(CommonTool.getPersonPartyId(resultUser.getLoginId()), CommonUtil.getDateTime());
claims.put("loginId", resultUser.getLoginId());
claims.put("loginCode", resultUser.getLoginCode());
claims.put("organizationId", orgInfo.getOrganizationId());
claims.put("personName", resultUser.getPersonName());
JSONObject json = new JSONObject();
json.put(AppConstants.HEADER_TOKEN, AppConstants.TOKEN_PREFIX + " " + jwtUtil.generateJwt(claims));
4. 通過(guò)InterceptorConfig配置攔截目錄和自定義攔截器JwtInterceptor
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor())
                .addPathPatterns("/app/**");
    }
    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }
}

/**
 * 不需要驗(yàn)證Token的方法、類
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {

    boolean required() default true;
}

public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

//  @Autowired
//  UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 如果不是映射到方法直接通過(guò)
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        //先檢查方法是否有passtoken注釋愉适,有則跳過(guò)認(rèn)證
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        Class<?> beanType = handlerMethod.getBeanType();
        //檢查類是否有passtoken注釋犯助,有則跳過(guò)認(rèn)證
        if (beanType.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = beanType.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        String token = httpServletRequest.getHeader(AppConstants.HEADER_TOKEN);
        try {
            if (StringUtils.isBlank(token)) {
                LogUtil.getAppLoger().info("無(wú)token,請(qǐng)重新登錄");
                printContent(httpServletResponse, ResultEnum.TOKEN_NULL);
                return false;
            } else {
                // 解析數(shù)據(jù)并放入httpServletRequest维咸,以便業(yè)務(wù)使用相關(guān)信息
                Claims claims = jwtUtil.getClaimsFromToken(token);
                UserView userView = new UserView();
                // 組織信息
                Organization organization = new Organization();
                organization.setOrganizationId(Long.valueOf(claims.get("organizationId").toString()));
                // 人員信息
                UserLogin userLogin = new UserLogin();
                userLogin.setLoginId(Long.valueOf(claims.get("loginId").toString()));
                userLogin.setLoginCode((String) claims.get("loginCode"));
                userLogin.setPersonName((String) claims.get("personName"));
                userView.setLoginCode((String) claims.get("loginCode"));
                userView.setOrganization(organization);
                userView.setUserLogin(userLogin);
                userView.setToken(token);
                httpServletRequest.setAttribute("userView", userView);
                // 獲取登錄信息
            }
        } catch (ExpiredJwtException e) {
            LogUtil.getAppLoger().info("token已過(guò)期;" + e.getMessage());
            printContent(httpServletResponse, ResultEnum.TOKEN_EXPIRED);
            return false;
        } catch (Exception e) {
            // 錯(cuò)誤信息: httpServletResponse. sendError(HttpServletResponse.SC_UNAUTHORIZED, "token非法");
            LogUtil.getAppLoger().info("token非法;" + e.getMessage());
            printContent(httpServletResponse, ResultEnum.TOKEN_ILLEGAL);
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e) throws Exception {
    }

    /**
     * 將錯(cuò)誤信息返回
     * @param response 響應(yīng)
     * @param resultEnum 返回錯(cuò)誤信息枚舉
     */
    private static void printContent(HttpServletResponse response, ResultEnum resultEnum) {
        PrintWriter pw = null;
        try {
            response.reset();
            response.setContentType("application/json");
            response.setHeader("Cache-Control", "no-store");
            response.setCharacterEncoding("UTF-8");
            ResultVO resultVO = ResultUtil.error(resultEnum);
            String content = JSON.toJSONString(resultVO);
            pw = response.getWriter();
            pw.write(content);
            pw.flush();
        } catch (Exception e) {
            LogUtil.getAppLoger().info("攔截器輸出流異常;" + e.getMessage());
        } finally {
            if (pw != null) {
                pw.close();
            }
        }
    }
}
5. 通過(guò)自定義攔截器JwtInterceptor 攔截指定請(qǐng)求路徑剂买,解析Token中的數(shù)據(jù)并放入HttpServletRequest
// 解析數(shù)據(jù)并放入httpServletRequest,以便業(yè)務(wù)使用相關(guān)信息
Claims claims = jwtUtil.getClaimsFromToken(token);
UserView userView = new UserView();
// 組織信息
Organization organization = new Organization();
organization.setOrganizationId(Long.valueOf(claims.get("organizationId").toString()));
// 人員信息
UserLogin userLogin = new UserLogin();
userLogin.setLoginId(Long.valueOf(claims.get("loginId").toString()));
userLogin.setLoginCode((String) claims.get("loginCode"));
userLogin.setPersonName((String) claims.get("personName"));
userView.setLoginCode((String) claims.get("loginCode"));
userView.setOrganization(organization);
userView.setUserLogin(userLogin);
userView.setToken(token);
httpServletRequest.setAttribute("userView", userView);
6. 各業(yè)務(wù)獲取HttpServletRequest中的userView來(lái)獲取Token中的登錄人信息
@ResponseBody
@RequestMapping("/getuserinfo")
public ResultVO getUserInfo(HttpServletRequest request) {
    UserView userView = (UserView) request.getAttribute("userView");
    return ResultUtil.success(userView);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末腰湾,一起剝皮案震驚了整個(gè)濱河市雷恃,隨后出現(xiàn)的幾起案子疆股,更是在濱河造成了極大的恐慌费坊,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旬痹,死亡現(xiàn)場(chǎng)離奇詭異附井,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)两残,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門永毅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人人弓,你說(shuō)我怎么就攤上這事沼死。” “怎么了崔赌?”我有些...
    開(kāi)封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵意蛀,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我健芭,道長(zhǎng)县钥,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任慈迈,我火速辦了婚禮若贮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己谴麦,他們只是感情好蠢沿,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著匾效,像睡著了一般搏予。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弧轧,一...
    開(kāi)封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天雪侥,我揣著相機(jī)與錄音,去河邊找鬼精绎。 笑死速缨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的代乃。 我是一名探鬼主播旬牲,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼搁吓!你這毒婦竟也來(lái)了原茅?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤堕仔,失蹤者是張志新(化名)和其女友劉穎擂橘,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體摩骨,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡通贞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恼五。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昌罩。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖灾馒,靈堂內(nèi)的尸體忽然破棺而出茎用,到底是詐尸還是另有隱情,我是刑警寧澤睬罗,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布轨功,位于F島的核電站,受9級(jí)特大地震影響傅物,放射性物質(zhì)發(fā)生泄漏夯辖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一董饰、第九天 我趴在偏房一處隱蔽的房頂上張望蒿褂。 院中可真熱鬧圆米,春花似錦、人聲如沸啄栓。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)昙楚。三九已至近速,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間堪旧,已是汗流浹背削葱。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留淳梦,地道東北人析砸。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像爆袍,于是被迫代替她去往敵國(guó)和親首繁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容