問題背景:
后端服務對手機APP端開放API,沒有基本的校驗就是裸奔,別人抓取接口后容易惡意請求,不要求嚴格的做的安全,但是簡單的基礎安全屏障是要建立的,再配合HTTPS使用,這樣使后端服務盡可能的安全。
對接口安全問題,采用JWT對接口進行token驗證,判斷請求的有效性,目前對JWT解釋的博客文章很多,對JWT不了解的可以查找相關資料,JWT官網。
JWT是JSON Web Token的簡寫,一些是JWT官網的解釋:
什么是JWT?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
看不懂的可以用Google翻譯:
JSON Web Token(JWT)是一個開放標准(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON對象安全地傳輸信息。 此信息可以通過數字簽名進行驗證和信任。 JWT可以使用密鑰(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰進行簽名。
JWT的結構是怎樣的?
JWT主要由三部分構成,
- Header 頭部,說明使用JWT的類型,和使用的算法
- Payload 中間體,定義的一些有效數據,比如簽發者,簽發時間,過期時間等等,具體可查看RFC7519,除了一些公共的屬性外,可以定義一些私有屬性,用於自己的業務邏輯。
- Signature 簽名,創建簽名,base64UrlEncode對header和Payload進行處理后,再根據密鑰和頭部中定義的算法進行簽名。如下格式:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
//生成的Token如下樣式
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJFU0JQIiwibmFtZSI6IuWImOWFhuS8nyIsImV4cCI6MTUzMTQ0OTExNSwiaWF0IjoxNTMxNDQ5MDg1LCJqdGkiOjEsImFjY291bnQiOiIxNTAwMTEwMTUzNiJ9.4IEi95xcOQ4SfXvjz34bBC8ECej56jiMuq7Df4Vd9YQ
具體實現:
1. maven構建,可以查看Github
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
2. 創建Token
1 import com.alibaba.fastjson.JSONObject; 2 import com.woasis.wos.api.UserClaim; 3 import io.jsonwebtoken.Claims; 4 import io.jsonwebtoken.JwtBuilder; 5 import io.jsonwebtoken.Jwts; 6 import io.jsonwebtoken.SignatureAlgorithm; 7 8 import javax.crypto.spec.SecretKeySpec; 9 import javax.xml.bind.DatatypeConverter; 10 import java.security.Key; 11 12 public class JwtHandler { 13 14 //簽發者 15 private static final String ISSUER = "iss"; 16 //簽發時間 17 private static final String ISSUED_AT = "iat"; 18 //過期時間 19 private static final String EXPIRATION_TIME = "exp"; 20 private static final Long EXPIRATION_TIME_VALUE = 1000*30L; 21 //JWT ID 22 private static final String JWT_ID = "jti"; 23 //密鑰 24 private static final String SECRET = "AAAABBBCCC"; 25 26 /** 27 * 構造Token 28 * @param userId 用戶ID 29 * @param userName 用戶名稱 30 * @param phone 手機號 31 * @return 32 */ 33 public static String createToken(Integer userId, String userName, String phone) { 34 35 //采用HS256簽名算法對token進行簽名 36 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; 37 38 //當前系統時間 39 long nowMillis = System.currentTimeMillis(); 40 41 //采用密鑰對JWT加密簽名 42 byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET); 43 Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); 44 45 //構造payload 46 JSONObject payload = new JSONObject(); 47 payload.put(ISSUER, "ESBP"); 48 payload.put(ISSUED_AT, nowMillis/1000); 49 payload.put(JWT_ID, userId); 50 payload.put("account", phone); 51 payload.put("name",userName); 52 //設置過期時間 53 long expMillis = nowMillis + EXPIRATION_TIME_VALUE; 54 payload.put(EXPIRATION_TIME, expMillis/1000); 55 56 //設置JWT參數 57 JwtBuilder builder = Jwts.builder() 58 .setPayload(payload.toJSONString()) 59 .signWith(signatureAlgorithm, signingKey); 60 //構造token字符串 61 return builder.compact(); 62 } 63 }
3. 解析JWT
private static Logger logger = LoggerFactory.getLogger(JwtHandler.class); /** * JWT解析 * @param jwt * @return */ public static UserClaim parseJWT(String jwt) { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET)) .setAllowedClockSkewSeconds(100) //設置允許過期時間,在構造token的時候有設置過期時間,此處是指到了過期時間之后還允許多少秒有效,且此token可以解析 .parseClaimsJws(jwt).getBody(); UserClaim userClaim = new UserClaim(); userClaim.setAccount((String) claims.get("account")); userClaim.setName((String) claims.get("name")); userClaim.setJti(claims.getId()); userClaim.setIss(claims.getIssuer()); userClaim.setIat(claims.getIssuedAt()); userClaim.setExp(claims.getExpiration()); logger.debug("parseJWT UserClaim:"+JSONObject.toJSONString(userClaim)); return userClaim; }
特別說明:
在jjwt源碼文件JwtMap.java中有這么個方法toDate(),在解析數據的時候這個地方按秒對時間處理的,所以在設置簽發時間或過期時間的時候要設置秒。
protected static Date toDate(Object v, String name) { if (v == null) { return null; } else if (v instanceof Date) { return (Date) v; } else if (v instanceof Number) { // https://github.com/jwtk/jjwt/issues/122: // The JWT RFC *mandates* NumericDate values are represented as seconds. // Because Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = ((Number) v).longValue(); long millis = seconds * 1000; return new Date(millis); } else if (v instanceof String) { // https://github.com/jwtk/jjwt/issues/122 // The JWT RFC *mandates* NumericDate values are represented as seconds. // Because Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = Long.parseLong((String) v); long millis = seconds * 1000; return new Date(millis); } else { throw new IllegalStateException("Cannot convert '" + name + "' value [" + v + "] to Date instance."); } }
4. 攔截器使用
要想對api進行控制,就要使用攔截器,或是過濾器,提問:攔截器和過濾器的區別是什么?此處采用攔截器進行控制。
攔截器具體實現代碼:
import com.woasis.wos.api.UserClaim; import com.woasis.wos.common.exception.ExceptionEnum; import com.woasis.wos.common.exception.WosException; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.SignatureException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Token驗證攔截器 */ public class TokenInterceptor implements HandlerInterceptor { private static Logger logger = LoggerFactory.getLogger(TokenInterceptor.class); @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { logger.debug("path:"+httpServletRequest.getRequestURI()); String token = httpServletRequest.getParameter("token"); String userId = httpServletRequest.getParameter("id"); if (!StringUtils.isBlank(token)){ UserClaim claim = null; try { claim = JwtHandler.parseJWT(token); }catch (ExpiredJwtException e){//token過期 throw new WosException(ExceptionEnum.EXPIRATION_TIME); }catch (SignatureException e){//簽名被篡改 throw new WosException(ExceptionEnum.SIGNATUREEXCEPTION); } if (claim != null && userId != null){ if (userId.equals(claim.getJti())){ return true; }else {//token用戶非請求用戶,非法請求 throw new WosException(ExceptionEnum.ILLEGAL_REQUEST); } }else { throw new WosException(ExceptionEnum.ILLEGAL_REQUEST); } }else {//token為空,非法請求 throw new WosException(ExceptionEnum.ILLEGAL_REQUEST); } } @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 { } }
在Spring Boot中攔截器的使用:
import com.woasis.wos.api.util.TokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class WosAppConfigurer extends WebMvcConfigurerAdapter { //排除攔截的請求路徑 private static String[] excludePatterns = new String[]{"/oauth/login"}; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**").excludePathPatterns(excludePatterns); super.addInterceptors(registry); } }
5. 效果測試
模擬獲取token
模擬token過期
模擬token中簽名被篡改
參數簽名://TODO