最近在做一个项目,需要集成智能设备,一些操作智能设备的业务逻辑是通用的,所以独立了一个API子工程出来,这样容易扩展、容易维护,但是安全性上就需要多考虑一些了。基于此,我制定了一个接口调用的安全方案如下。
方案
1、发送请求之前,先从本地缓存拿到token。如果token能拿到,则进行步骤6,拿不到进行步骤2。
2、申请token,携带client_id和client_secret参数。
3、验证身份,接口系统根据client_id和client_secret鉴别终端身份,身份识别成功签发token,时效15天;身份识别失败无法继续下一步。
4、生成token,生成access_token并使用redis进行记录,时效15天。
5、存储token,终端使用本地缓存存储token。
6、写入header,发送请求时在header中使用Authorization->Bearer token方式携带token。
7、验证token,token验证通过后调用接口逻辑。
8、接口逻辑,执行完后返回应答信息。
client_id和client_secret是终端在API接口系统中的身份标识,需要提前获取到。
token工具类
- JJWTUtils
public class JJWTUtils {
public static final String JJWT_SERCETKEY = "china_shandong";
public static final String JJWT_AES = "AES"; // 加密标准
public static final String JJWT_TOKEN = "token";
public static final String JJWT_EXPIRATION = "expiration";
/** 单位: 秒 */
/** token缓存时效:基准单位时间+延长时效 */
/** token过期时间(基准单位时间)*/
public static final Integer JWT_TOKEN_TIMEOUT = 60*30;
/** WEB端token失效的过渡期(延长时效) */
public static final Integer JWT_WEB_EXPIRE_INTERIM_PERIOD = 60*3;
/** 手机端token失效的过渡期(延长时效)【当前默认为7天,该常量可根据业务场景进行调整,建议最长设置为30天】 */
public static final Integer JWT_MOBILE_EXPIRE_INTERIM_PERIOD = 60*60*24*7;
/** token刷新的临界点 */
public static final Integer JWT_REFRESH_DIVIDING_POINT = 60*5;
/** 白名单超时时间 */
public static final Integer JWT_TOKEN_BLACKLIST_TIMEOUT = 30;
/** 客户端标识 */
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<String, String> tokenMap = new HashMap<String, String>();
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;
}
/**
* 根据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;
}
/**
* 验证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 的标准字段
* iss
* sub
* aud
* exp
* nbf
* iat
* jti
* @param clientId
* @param exp milliseconds(1000ms=1s) default 1000*60*10(10分钟)
* @return
*/
private Map<String, Object> getClaimsMap(String clientId, Long exp) {
Map<String, Object> claimsMap = new HashMap<String, Object>();
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 是否已经超时(已废弃请使用 {@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<String, String> 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> T unmarshal(File file, Class<T> 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> T unmarshal(InputStream is, Class<T> 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> T unmarshal(String str, Class<T> 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> T unmarshal(byte[] bytes, Class<T> 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转成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){
//将单引号转义一下,因为JSON串中的字符串类型可以单引号引起来的
objstr = objstr.replaceAll("'", "\\'");
}
if(objstr.indexOf("\"")!=-1){
//将双引号转义一下,因为JSON串中的字符串类型可以单引号引起来的
objstr = objstr.replaceAll("\"", "\\\"");
}
if(objstr.indexOf("\r\n")!=-1){
//将回车换行转换一下,因为JSON串中字符串不能出现显式的回车换行
objstr = objstr.replaceAll("\r\n", "\\u000d\\u000a");
}
if(objstr.indexOf("\n")!=-1){
//将换行转换一下,因为JSON串中字符串不能出现显式的换行
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<String> excludes = new ArrayList<>();
/**
* jwt过滤开关
*/
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;
// 是否需要验证token
if (handleExcludeURL(req, resp))
{
chain.doFilter(request, response);
return;
}
// 验证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");
}
/**
* 当前访问接口是否排除在外
* @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数据");
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<String, String> initParameters = new HashMap<String, String>();
initParameters.put("excludes", excludes);
initParameters.put("enabled", enabled);
registration.setInitParameters(initParameters);
return registration;
}
}
- yml文件配置项
# token解析
jwt:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /appLogin
# 匹配链接
urlPatterns: /system/*,/user/*
应答数据结构
响应体数据结构约定:
{
“code”: 0,
“msg”:”描述信息”,
“data”:{
}
}
实践
只有返回应答body中code为0时,说明接口操作成功。否则操作视为失败。