springboot JWT項目實戰demo


本文是一篇實戰demo,使用框架為io.jsonwebtoken的jjwt。你會了解到token的生成,解析過程,最后將在項目中體驗jwt的使用過程。如果不是很了解jwt,可以參考以下文章補充一下。

什么是 JWT -- JSON WEB TOKEN

一篇文章帶你分清楚JWT,JWS與JWE

目錄

1、引入所用到的庫

<!-- jwt相關 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

(可以在 https://github.com/jwtk/jjwt找到最新版)

2、生成一個token

使用JwtBuilder可以很方便的幫助我們創建出一個jws,也就是我們平時拿來傳的token

// 1、創建私鑰,這里的私鑰是建議隨機生成的,這里只是個例子
String secretString = "12345678901234567890123456789012";
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));

// 2、創建jwtBuilder
JwtBuilder jwtBuilder = Jwts.builder().setId("自定義id")
	.setSubject("自定義subject")
	.setIssuedAt(new Date())  // 簽發時間
	.signWith(key);  // 簽名

// 3、獲取token
String token = jwtBuilder.compact();

3、解析Token

這里我們可以通過定義的私鑰和token來解析出來有用的數據

// 1、解析token,這里的key要和私鑰是一個
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

// 2、獲取參數
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());

4、自定義加密數據

自定義數據的加密和解密和1、2步相同,只是使用了claim(String, Object)來定義數據。使用claims.get(String)來解釋數據。

// 加密過程
JwtBuilder jwtBuilder = Jwts.builder().claim("para1","value1").claim("參數2","值2").signWith(key);

// 解析過程
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
claims.get("para1");
claims.get("參數2");

5、實際運用

概述

內容:在知道了jwt是如何生成和解析token的,接下來,作者將會舉一個例子,其中包括

  • 用戶登錄獲取token,token中包含用戶id,用戶昵稱,用戶權限信息
  • 訪問無token攔截(使用攔截器)
  • 請求有token時進行解析,獲取
    目錄:

目錄

過程簡述:用戶在登錄時獲取token。在訪問其他鏈接時檢查是否存在token,如果存在,進行解析以便后繼使用,如果不存在,不允許訪問。

①封裝JwtUtils工具類

為便於操作,我們把jwt生成token和解析封裝成一個工具類

package com.xxx.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtils {
    private final String secretString = "12345678901234567890123456789012";
    private final SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
    public String generateToken(String userId, String userNick, Map<String, Object> other) {
        // 設置有效時間
        long period = 7200000;
        JwtBuilder jwtBuilder = Jwts.builder()
                .setClaims(other) // 使用setClaims可以將Map數據進行加密,必須放在其他變量之前
                .setId(userId)
                .setSubject(userNick)
                .setExpiration(new Date(System.currentTimeMillis() + period)) // 設置有效期
                .signWith(key);
        return jwtBuilder.compact();
    }
    public Claims parseToken(String token){
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}

②定義並配置攔截器

在這一步主要是為了完成對於無token請求的攔截,你將使用到如下三個類,如果對攔截器還不是很了解,可以看看這篇文章https://blog.csdn.net/weixin_49736959/article/details/107843179

定義攔截器

// InterceptorConfig.java
package com.xxx.demo.config.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.xxx.demo.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Component
public class TokenInterceptor implements HandlerInterceptor {
    // 自動注入一下
    @Resource
    private JwtUtils jwtUtils;
    // 這個方法是在訪問接口之前執行的,我們只需要在這里寫驗證登陸狀態的業務邏輯,就可以在用戶調用指定接口之前驗證登陸狀態了
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 設置返回值屬性
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        String token = request.getHeader("token");
        PrintWriter out;
        // 對於注解的判斷
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        if(handlerMethod.getMethodAnnotation(NoNeedToken.class)!=null || handlerMethod.getBeanType().isAnnotationPresent(NoNeedToken.class)){
            // 如果自己擁有NoNeedToken標注或者所屬的class擁有NoNeedToken 就直接放行
            return true;
        }
        // 在這里寫你的判斷邏輯 return true是通過攔截器,可以繼續訪問controller,return false是不通過
        if (token != null) {
            Claims claims = null;
            try{
                claims = jwtUtils.parseToken(token);
            }catch (Exception ignored){
            }
            if(claims != null){
                request.setAttribute("user_claims", claims);
                return true;
            }
        }
        JSONObject res = new JSONObject();
        res.put("state","false");
        res.put("msg","token is null or wrong");
        out = response.getWriter();
        out.append(res.toString());
        return false;
    }
}

將攔截器配置到項目中

// InterceptorConfig.java
package com.xxx.demo.config.interceptor;
import com.xxx.demo.util.JwtUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class InterceptorConfig  implements WebMvcConfigurer {
    @Resource
    TokenInterceptor tokenInterceptor;
    @Bean
    public JwtUtils jwtUtils(){
        return new JwtUtils();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 設置所有的路徑都要進行攔截,除了/test/login
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}

定義免token訪問的注解

// NoNeedToken.java
package com.xxx.demo.config.interceptor;
import java.lang.annotation.*;


@Target({ElementType.METHOD, ElementType.TYPE}) //注解的范圍是類、接口、枚舉的方法上
@Retention(RetentionPolicy.RUNTIME)//被虛擬機保存,可用反射機制讀取
@Documented
public @interface NoNeedToken {
}

③使用BaseController將解析的內容存儲一下

一個請求訪問進行訪問的順序是,攔截器 -> @ModelAttribute修飾的方法 -> 業務處理的controller, 所以在這里我們定義BaseController,寫入@ModelAttribute 來統一解析token,后繼需要進行token解析的controller,都繼承這個類。

// BaseController
package com.xxx.demo.controller;

import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.ModelAttribute;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class BaseController {
    protected HttpServletRequest request;
    protected HttpServletResponse response;
    protected String UserId;  // 用戶id
    protected String authority;  // 用戶權限

    @ModelAttribute
    public void parseClaims(HttpServletRequest request, HttpServletResponse response){
        this.request = request;
        this.response = response;
        // 獲取到在攔截器里設置的 user_claims, 並將其保存到類的成員變量中
        Claims userClaims = (Claims) request.getAttribute("user_claims");
        if(userClaims != null) {
            this.UserId = userClaims.getId();
            this.authority = userClaims.get("authority").toString();
        }
    }
}

④設置用戶controller測試

// 
package com.xxx.demo.controller;
import com.xxx.demo.config.interceptor.NoNeedToken;
import com.xxx.demo.model.Users;
import com.xxx.demo.service.UsersService;
import com.xxx.demo.util.JwtUtils;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UsersController extends BaseController{
    @Resource
    UsersService usersService; // 提供查詢功能,根據用戶nick查詢到用戶

    @Resource
    JwtUtils jwtUtils;

	// 用戶登錄方法
    @NoNeedToken // 用於取消token驗證
    @RequestMapping(value = "/login")
    public String login(@RequestBody Map<String, String> arg){
        String token = "";
        if(arg.get("nick")!= null){
            Users user = usersService.getPassByNick(arg.get("nick"));
            if(user.getPassword()!=null && user.getPassword().equals(arg.get("password"))){
                Map<String, Object> jwtArg = new HashMap<>();
                jwtArg.put("authority",user.getAuthority());
                System.out.println("" + user.getId().toString() +  user.getNick());
                token = jwtUtils.generateToken(user.getId().toString(), user.getNick(), jwtArg);
            }
        }
        return token;
    }
	// 獲取用戶信息方法
    @RequestMapping(value = "/test")
    public Map<String, String> test(){
        Map<String, String> res = new HashMap<>();
        res.put("userID", this.UserId);  // 這里可以用this.UserId是繼承了BaseController
        res.put("authority",this.authority);
        return res;
    }
}

⑤設置用戶實例類,和usersService

  • user實體類包含userid, nick, password, authority字段,
  • dao層設計有查詢功能,可以通過用戶nick找到用戶
  • UsersService 使用getPassByNick調用dao層查詢功能
    由於jpa實現的種類比較多,大家使用自己的方式實現一下就好

⑥測試結果

登錄獲取token


登錄獲取token

直接訪問test失敗


直接test

使用token進行訪問,成功獲取用戶id和權限


token登錄

6、注意事項

setClaims方法

在使用 setClaims方法自定義加密數據時,會覆蓋掉之前聲明的加密數據,請務必把setClaim寫在第一位

7、其他可選項

1、自動生成簽名

這個方法也是官方推薦的,用於生成一個足夠強的密鑰用於JWT hmc-ha算法,請使用密鑰. secretkeyfor (signaturealgalgorithm)助手方法

Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

2、常用錯誤

在本文中,沒有對錯誤類型進行區分,大家在使用時可以自行選擇

  • SignatureException:簽名錯誤異常
  • MalformedJwtException:JWT格式錯誤異常
  • ExpiredJwtException:JWT過期異常
  • UnsupportedJwtException:不支持的JWT異常

結語:希望對大家有幫助


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM