本文是一篇實戰demo,使用框架為io.jsonwebtoken的jjwt。你會了解到token的生成,解析過程,最后將在項目中體驗jwt的使用過程。如果不是很了解jwt,可以參考以下文章補充一下。
目錄
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
直接訪問test失敗
使用token進行訪問,成功獲取用戶id和權限
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異常
結語:希望對大家有幫助