JSON Web Token(JWT)是目前流行的跨域身份驗證解決方案。 官網:https://jwt.io/ 本文spring boot 2 集成JWT實現api介面驗證。 ...
JSON Web Token(JWT)是目前流行的跨域身份驗證解決方案。
官網:https://jwt.io/
本文使用spring boot 2 集成JWT實現api介面驗證。
一、JWT的數據結構
JWT由header(頭信息)、payload(有效載荷)和signature(簽名)三部分組成的,用“.”連接起來的字元串。
JWT的計算邏輯如下:
(1)signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
其中私鑰secret保存於伺服器端,不能泄露出去。
(2)JWT = base64UrlEncode(header) + "." + base64UrlEncode(payload) + signature
下麵截圖以官網的例子,簡單說明
二、JWT工作機制
客戶端使用其憑據成功登錄時,伺服器生成JWT並返回給客戶端。
當客戶端訪問受保護的資源時,用戶代理使用Bearer模式發送JWT,通常在Authorization header中,如下所示:
Authorization: Bearer <token>
伺服器檢查Authorization header中的有效JWT ,如果有效,則允許用戶訪問受保護資源。JWT包含必要的數據,還可以減少查詢資料庫或緩存信息。
三、spring boot集成JWT實現api介面驗證
開發環境:
IntelliJ IDEA 2019.2.2
jdk1.8
Spring Boot 2.1.11
1、創建一個SpringBoot項目,pom.xml引用的依賴包如下
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
2、定義一個介面的返回類
package com.example.jwtdemo.entity; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; import java.io.Serializable; @Data @NoArgsConstructor @ToString public class ResponseData<T> implements Serializable { /** * 狀態碼:0-成功,1-失敗 * */ private int code; /** * 錯誤消息,如果成功可為空或SUCCESS * */ private String msg; /** * 返回結果數據 * */ private T data; public static ResponseData success() { return success(null); } public static ResponseData success(Object data) { ResponseData result = new ResponseData(); result.setCode(0); result.setMsg("SUCCESS"); result.setData(data); return result; } public static ResponseData fail(String msg) { return fail(msg,null); } public static ResponseData fail(String msg, Object data) { ResponseData result = new ResponseData(); result.setCode(1); result.setMsg(msg); result.setData(data); return result; } }
3、統一攔截介面返回數據
package com.example.jwtdemo.config; import com.alibaba.fastjson.JSON; import com.example.jwtdemo.entity.ResponseData; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * 實現ResponseBodyAdvice介面,可以對返回值在輸出之前進行修改 */ @RestControllerAdvice public class GlobalResponseHandler implements ResponseBodyAdvice<Object> { //判斷支持的類型 @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { // 判斷為null構建ResponseData對象進行返回 if (o == null) { return ResponseData.success(); } // 判斷是ResponseData子類或其本身就返回Object o本身,因為有可能是介面返回時創建了ResponseData,這裡避免再次封裝 if (o instanceof ResponseData) { return (ResponseData<Object>) o; } // String特殊處理,否則會拋異常 if (o instanceof String) { return JSON.toJSON(ResponseData.success(o)).toString(); } return ResponseData.success(o); } }
4、統一異常處理
package com.example.jwtdemo.exception; import com.example.jwtdemo.entity.ResponseData; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseData exceptionHandler(Exception e) { e.printStackTrace(); return ResponseData.fail(e.getMessage()); } }
5、創建一個JWT工具類
package com.example.jwtdemo.common; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.JWT; import java.util.Date; public class JwtUtils { public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; // 過期時間,這裡設為5分鐘 private static final long EXPIRE_TIME = 5 * 60 * 1000; // 密鑰 private static final String SECRET = "jwtsecretdemo"; /** * 生成簽名,5分鐘後過期 * * @param name 名稱 * @param secret 密碼 * @return 加密後的token */ public static String sign(String name) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(SECRET); //使用HS256演算法 String token = JWT.create() //創建令牌實例 .withClaim("name", name) //指定自定義聲明,保存一些信息 //.withSubject(name) //信息直接放在這裡也行 .withExpiresAt(date) //過期時間 .sign(algorithm); //簽名 return token; } /** * 校驗token是否正確 * * @param token 令牌 * @param secret 密鑰 * @return 是否正確 */ public static boolean verify(String token) { try{ String name = getName(token); Algorithm algorithm = Algorithm.HMAC256(SECRET); JWTVerifier verifier = JWT.require(algorithm) .withClaim("name", name) //.withSubject(name) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception e){ return false; } } /** * 獲得token中的信息 * * @return token中包含的名稱 */ public static String getName(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("name").asString(); }catch(Exception e){ return null; } } }
6、新建兩個自定義註解:一個需要認證、另一個不需要認證
package com.example.jwtdemo.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface LoginToken { boolean required() default true; }
package com.example.jwtdemo.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; }
7、新建攔截器並驗證token
package com.example.jwtdemo.config; import com.example.jwtdemo.common.JwtUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果不是映射到方法直接通過 if(!(handler instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)handler; Method method=handlerMethod.getMethod(); //檢查是否有passtoken註釋,有則跳過認證 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //檢查有沒有需要用戶許可權的註解 if (method.isAnnotationPresent(LoginToken.class)) { LoginToken loginToken = method.getAnnotation(LoginToken.class); if (loginToken.required()) { // 執行認證 String tokenHeader = request.getHeader(JwtUtils.TOKEN_HEADER);// 從 http 請求頭中取出 token if(tokenHeader == null){ throw new RuntimeException("沒有token"); } String token = tokenHeader.replace(JwtUtils.TOKEN_PREFIX, ""); if (token == null) { throw new RuntimeException("沒有token"); } boolean b = JwtUtils.verify(token); if (b == false) { throw new RuntimeException("token不存在或已失效,請重新獲取token"); } return true; } } return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
8、配置攔截器
package com.example.jwtdemo.config; 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; @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } }
9、新建一個測試的控制器
package com.example.jwtdemo.controller; import com.example.jwtdemo.common.JwtUtils; import com.example.jwtdemo.config.LoginToken; import com.example.jwtdemo.config.PassToken; import org.springframework.web.bind.annotation.*; @RestController public class DemoController { @PassToken @PostMapping("getToken") public String getToken(@RequestParam String userName, @RequestParam String password){ if(userName.equals("admin") && password.equals("123456")){ String token = JwtUtils.sign("admin"); return token; } return "用戶名或密碼錯誤"; } @LoginToken @GetMapping("getData") public String getData() { return "獲取數據..."; } }
10、Postman測試
(1)GET請求:http://localhost:8080/getData,返回如下
(2)GET請求:http://localhost:8080/getData,在token中隨便輸入字元串,返回如下
(3)POST請求:http://localhost:8080/getToken,並設置用戶名和密碼參數,返回如下
(4)GET請求:http://localhost:8080/getData,在token中輸入上面token字元串,返回如下