API接口安全性實(shí)踐
最近在做一個項(xiàng)目,需要集成智能設(shè)備,一些操作智能設(shè)備的業(yè)務(wù)邏輯是通用的,所以獨(dú)立了一個API子工程出來,這樣容易擴(kuò)展、容易維護(hù),但是安全性上就需要多考慮一些了。基于此,我制定了一個接口調(diào)用的安全方案如下。
方案

1、發(fā)送請求之前,先從本地緩存拿到token。如果token能拿到,則進(jìn)行步驟6,拿不到進(jìn)行步驟2。
2、申請token,攜帶client_id和client_secret參數(shù)。
3、驗(yàn)證身份,接口系統(tǒng)根據(jù)client_id和client_secret鑒別終端身份,身份識別成功簽發(fā)token,時效15天;身份識別失敗無法繼續(xù)下一步。
4、生成token,生成access_token并使用redis進(jìn)行記錄,時效15天。
5、存儲token,終端使用本地緩存存儲token。
6、寫入header,發(fā)送請求時在header中使用Authorization->Bearer token方式攜帶token。
7、驗(yàn)證token,token驗(yàn)證通過后調(diào)用接口邏輯。
8、接口邏輯,執(zhí)行完后返回應(yīng)答信息。
client_id和client_secret是終端在API接口系統(tǒng)中的身份標(biāo)識,需要提前獲取到。
token工具類
- JJWTUtils
public class JJWTUtils {
public static final String JJWT_SERCETKEY = "china_shandong";
public static final String JJWT_AES = "AES"; // 加密標(biāo)準(zhǔn)
public static final String JJWT_TOKEN = "token";
public static final String JJWT_EXPIRATION = "expiration";
/** 單位: 秒 */
/** token緩存時效:基準(zhǔn)單位時間+延長時效 */
/** token過期時間(基準(zhǔn)單位時間)*/
public static final Integer JWT_TOKEN_TIMEOUT = 60*30;
/** WEB端token失效的過渡期(延長時效) */
public static final Integer JWT_WEB_EXPIRE_INTERIM_PERIOD = 60*3;
/** 手機(jī)端token失效的過渡期(延長時效)【當(dāng)前默認(rèn)為7天,該常量可根據(jù)業(yè)務(wù)場景進(jìn)行調(diào)整,建議最長設(shè)置為30天】 */
public static final Integer JWT_MOBILE_EXPIRE_INTERIM_PERIOD = 60*60*24*7;
/** token刷新的臨界點(diǎn) */
public static final Integer JWT_REFRESH_DIVIDING_POINT = 60*5;
/** 白名單超時時間 */
public static final Integer JWT_TOKEN_BLACKLIST_TIMEOUT = 30;
/** 客戶端標(biāo)識 */
public static final String HTTP_HEADER_BEARER = "Bearer ";
/**
* 生成SecretKey
* @param secret
* @return
*/
private static SecretKey generateKey(String secret) {
byte[] encodedKey = Base64.decodeBase64(secret);
return new SecretKeySpec(encodedKey, 0, encodedKey.length, JJWT_AES);
}
/**
* 新生成token
*
* @param clientId
* @param exp
* @return
* @throws JsonGenerationException
* @throws JsonMappingException
* @throws IOException
*/
public static String createToken(String clientId, Long exp) throws JsonGenerationException, JsonMappingException, IOException {
Map tokenMap = new HashMap();
Claims claims = new DefaultClaims();
JwtClient jwtClient = new JwtClient();
if (!StringUtils.isEmpty(clientId)) {
jwtClient.setMobile(clientId);
long expVal = 0;
if (exp != null) {
// milliseconds是毫秒 1000毫秒=1秒
expVal = System.currentTimeMillis() + exp*1000;
} else {
expVal = System.currentTimeMillis() + JWT_TOKEN_TIMEOUT*1000;
}
jwtClient.setExp(String.valueOf(expVal));
claims.setExpiration(new Date(expVal));
try {
claims.setSubject(JSON.marshal(jwtClient));
} catch (Exception e) {
e.printStackTrace();
}
String compactJws = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, generateKey(JJWT_SERCETKEY))
.compact();
tokenMap.put(JJWT_TOKEN, compactJws);
tokenMap.put(JJWT_EXPIRATION, String.valueOf(claims.getExpiration().getTime()));
try {
return JSON.marshal(tokenMap);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 解析token
* @param token
* @return
* @throws Exception
*/
public static Claims parseJWT(String token) throws ExpiredJwtException {
Claims claims = Jwts.parser()
.setSigningKey(generateKey(JJWT_SERCETKEY))
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 根據(jù)token獲取username
* @param token
* @return
* @throws JsonParseException
* @throws JsonMappingException
* @throws IOException
*/
public static String getClientIdByToken(String token) {
Claims claims = parseJWT(token);
if (claims != null) {
JwtClient jwtClient = null;
try {
jwtClient = JSON.unmarshal(claims.getSubject(), JwtClient.class);
} catch (Exception e) {
e.printStackTrace();
}
return jwtClient.getMobile();
}
return null;
}
public static JwtClient getJwtClientByToken(String token) {
Claims claims = parseJWT(token);
if (claims != null) {
JwtClient jwtClient = null;
try {
jwtClient = JSON.unmarshal(claims.getSubject(), JwtClient.class);
} catch (Exception e) {
e.printStackTrace();
}
return jwtClient;
}
return null;
}
/**
* 驗(yàn)證token
* @param token
* @return
*/
public static boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(generateKey(JJWT_SERCETKEY))
.parseClaimsJws(token);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 生成Claims
*
* @param clientId
* @param exp
* @return
*/
public static Claims getClaims(String clientId, Long exp) {
Claims claims = new DefaultClaims();
JwtClient jwtClient = new JwtClient();
if (!StringUtils.isEmpty(clientId)) {
jwtClient.setMobile(clientId);
try {
claims.setSubject(JSON.marshal(jwtClient));
} catch (Exception e) {
e.printStackTrace();
}
if (exp != null) {
claims.setExpiration(new Date(System.currentTimeMillis() + exp*1000));
} else {
claims.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_TIMEOUT*1000));
}
return claims;
}
return null;
}
/**
* 封裝JWT 的標(biāo)準(zhǔn)字段
* iss
* sub
* aud
* exp
* nbf
* iat
* jti
* @param clientId
* @param exp milliseconds(1000ms=1s) default 1000*60*10(10分鐘)
* @return
*/
private Map getClaimsMap(String clientId, Long exp) {
Map claimsMap = new HashMap();
JwtClient jwtClient = new JwtClient();
if (!StringUtils.isEmpty(clientId)) {
jwtClient.setMobile(clientId);
try {
claimsMap.put("sub", JSON.marshal(jwtClient));
} catch (Exception e) {
e.printStackTrace();
}
}
if (exp != null) {
claimsMap.put("exp", new Date().getTime() + exp*1000);
} else {
claimsMap.put("exp", new Date().getTime() + 60*10*1000);
}
return claimsMap;
}
/**
* 去掉token中的 'Bearer '
* @param token
* @return
*/
public static String removeBearerFromToken(String token) {
if (token != null) {
return token.contains(HTTP_HEADER_BEARER) ?
StringUtils.removeStart(token, HTTP_HEADER_BEARER) : token;
}
return null;
}
/**
* 判斷token 是否已經(jīng)超時(已廢棄請使用 {@link #isRefresh(String)})
* @param token
* @return
*/
@Deprecated
public static boolean isExpireTime(String token) {
Claims claims = JJWTUtils.parseJWT(token);
LocalDateTime expTime = LocalDateTime.ofInstant(claims.getExpiration().toInstant(), ZoneId.systemDefault());
return LocalDateTime.now().isAfter(expTime);
}
/**
* 判斷是否需要刷新token
*
* @param token
* @return
*/
public static boolean isRefresh(String token) {
try {
Claims claims = JJWTUtils.parseJWT(token);
LocalDateTime expTime = LocalDateTime.ofInstant(claims.getExpiration().toInstant(), ZoneId.systemDefault());
return LocalDateTime.now().isAfter(expTime);
} catch (ExpiredJwtException e) {
return true;
}
}
public static String generateToken(String appid) throws Exception {
String tokenJson = createToken(appid,60 * 60 * 24 * 365 * 100L);
@SuppressWarnings("unchecked")
Map tokenMap = JSON.unmarshal(tokenJson, Map.class);
String token = tokenMap.get("token");
return token;
}
}
- JSON
public class JSON {
public static final String DEFAULT_FAIL = "\"Parse failed\"";
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
public static void marshal(File file, Object value) throws Exception
{
try
{
objectWriter.writeValue(file, value);
}
catch (JsonGenerationException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static void marshal(OutputStream os, Object value) throws Exception
{
try
{
objectWriter.writeValue(os, value);
}
catch (JsonGenerationException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static String marshal(Object value) throws Exception
{
try
{
return objectWriter.writeValueAsString(value);
}
catch (JsonGenerationException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static byte[] marshalBytes(Object value) throws Exception
{
try
{
return objectWriter.writeValueAsBytes(value);
}
catch (JsonGenerationException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static T unmarshal(File file, Class valueType) throws Exception
{
try
{
return objectMapper.readValue(file, valueType);
}
catch (JsonParseException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static T unmarshal(InputStream is, Class valueType) throws Exception
{
try
{
return objectMapper.readValue(is, valueType);
}
catch (JsonParseException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static T unmarshal(String str, Class valueType) throws Exception
{
try
{
return objectMapper.readValue(str, valueType);
}
catch (JsonParseException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
public static T unmarshal(byte[] bytes, Class valueType) throws Exception
{
try
{
if (bytes == null)
{
bytes = new byte[0];
}
return objectMapper.readValue(bytes, 0, bytes.length, valueType);
}
catch (JsonParseException e)
{
throw new Exception(e);
}
catch (JsonMappingException e)
{
throw new Exception(e);
}
catch (IOException e)
{
throw new Exception(e);
}
}
/**
* 將Object轉(zhuǎn)成json串
* @param obj
* @return
* @throws IOException
* @throws JsonMappingException
* @throws JsonGenerationException
*/
public static String objToJson(Object obj) throws JsonGenerationException, JsonMappingException, IOException{
String objstr = objectMapper.writeValueAsString(obj) ;
if(objstr.indexOf("'")!=-1){
//將單引號轉(zhuǎn)義一下,因?yàn)镴SON串中的字符串類型可以單引號引起來的
objstr = objstr.replaceAll("'", "\\'");
}
if(objstr.indexOf("\"")!=-1){
//將雙引號轉(zhuǎn)義一下,因?yàn)镴SON串中的字符串類型可以單引號引起來的
objstr = objstr.replaceAll("\"", "\\\"");
}
if(objstr.indexOf("\r\n")!=-1){
//將回車換行轉(zhuǎn)換一下,因?yàn)镴SON串中字符串不能出現(xiàn)顯式的回車換行
objstr = objstr.replaceAll("\r\n", "\\u000d\\u000a");
}
if(objstr.indexOf("\n")!=-1){
//將換行轉(zhuǎn)換一下,因?yàn)镴SON串中字符串不能出現(xiàn)顯式的換行
objstr = objstr.replaceAll("\n", "\\u000a");
}
return objstr;
}
}
- JwtClient
public class JwtClient {
private String mobile;
private String exp;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getExp() {
return exp;
}
public void setExp(String exp) {
this.exp = exp;
}
@Override
public String toString() {
return "JwtClient{" +
"mobile='" + mobile + '\'' +
", exp='" + exp + '\'' +
'}';
}
}
過濾器
- JwtFilter
public class JwtFilter implements Filter {
protected Logger logger = LoggerFactory.getLogger(JwtFilter.class);
/**
* 排除鏈接
*/
public List excludes = new ArrayList<>();
/**
* jwt過濾開關(guān)
*/
public boolean enabled = false;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 是否需要驗(yàn)證token
if (handleExcludeURL(req, resp))
{
chain.doFilter(request, response);
return;
}
// 驗(yàn)證token, true為通過
if (checkToken(req, resp))
{
chain.doFilter(request, response);
return;
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("jwtFilter init");
String tempExcludes = filterConfig.getInitParameter("excludes");
String tempEnabled = filterConfig.getInitParameter("enabled");
if (StringUtils.isNotEmpty(tempExcludes))
{
String[] url = tempExcludes.split(",");
for (int i = 0; url != null && i < url.length; i++)
{
excludes.add(url[i]);
}
}
if (StringUtils.isNotEmpty(tempEnabled))
{
enabled = Boolean.valueOf(tempEnabled);
}
}
@Override
public void destroy() {
logger.info("jwtFilter destroy");
}
/**
* 當(dāng)前訪問接口是否排除在外
* @param request
* @param response
* @return
*/
private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response)
{
if (!enabled)
{
return true;
}
if (excludes == null || excludes.isEmpty())
{
return false;
}
String url = request.getServletPath();
for (String pattern : excludes)
{
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find())
{
return true;
}
}
return false;
}
private boolean checkToken(HttpServletRequest request, HttpServletResponse response) {
logger.error("call checkToken start");
String jwtToken = request.getHeader("Authorization");
if (jwtToken == null || "".equals(jwtToken)) {
logger.error("請求頭部未攜帶token數(shù)據(jù)");
return false;
}
String token = JJWTUtils.removeBearerFromToken(jwtToken);
String mobile = JJWTUtils.getClientIdByToken(token);
if (StringUtils.isNotEmpty(mobile)) {
logger.info("mobile: [{}]", mobile);
request.setAttribute("u_mobile", mobile);
return true;
}
return false;
}
}
自動裝配
- FilterConfig
@Configuration
public class FilterConfig {
@Value("${jwt.enabled}")
private String enabled;
@Value("${jwt.excludes}")
private String excludes;
@Value("${jwt.urlPatterns}")
private String urlPatterns;
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean jwtFilterRegistration()
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new JwtFilter());
registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
registration.setName("jwtFilter");
registration.setOrder(Integer.MAX_VALUE);
Map initParameters = new HashMap();
initParameters.put("excludes", excludes);
initParameters.put("enabled", enabled);
registration.setInitParameters(initParameters);
return registration;
}
}
- yml文件配置項(xiàng)
# token解析
jwt:
# 過濾開關(guān)
enabled: true
# 排除鏈接(多個用逗號分隔)
excludes: /appLogin
# 匹配鏈接
urlPatterns: /system/*,/user/*
應(yīng)答數(shù)據(jù)結(jié)構(gòu)
響應(yīng)體數(shù)據(jù)結(jié)構(gòu)約定:
{
“code”: 0,
“msg”:”描述信息”,
“data”:{
}
}
實(shí)踐
只有返回應(yīng)答body中code為0時,說明接口操作成功。否則操作視為失敗。
本文僅代表作者觀點(diǎn),版權(quán)歸原創(chuàng)者所有,如需轉(zhuǎn)載請?jiān)谖闹凶⒚鱽碓醇白髡呙帧?/p>
免責(zé)聲明:本文系轉(zhuǎn)載編輯文章,僅作分享之用。如分享內(nèi)容、圖片侵犯到您的版權(quán)或非授權(quán)發(fā)布,請及時與我們聯(lián)系進(jìn)行審核處理或刪除,您可以發(fā)送材料至郵箱:service@tojoy.com





