文章内容
一、JWT简介
JWT,JSON Web Token,开放的、行业标准(RFC 7519),用于网络应用环境间安全传递声明。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所须的声明信息。
1、特点
- 跨语言:支持主流语言
- 自包含:包含必要的所有信息,如用户信息和签名等
- 易传递:很方便通过HTTP头部传递
2、使用注意事项
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
- JWT 不加密的情况下,不能将秘密数据写入 JWT
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
- JWT的最大缺点:由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或更改 token 的权限。即,一旦 JWT 签发,在到期之前就会始终有效,除非服务器部署额外的逻辑
- JWT 本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
- 为减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输
二、JWT组成
JWT的token是三段由小数点分隔组成的字符串:header.payload.signature
1、header
头部包含两部分:声明类型和使用的哈希算法(通常直接使用HMAC SHA256,就是HS256)
{
"typ": "JWT",
"alg": "HS256"
}
将头部进行base64编码构成第一部分。Base64是一种用64个字符来表示任意二进制数据的方法,Base64是一种任意二进制到文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。
2、payload
也称为JWT claims,放置需要传输的信息,有三类:
- 保留claims,主要包括iss发行者、exp过期时间、sub主题、aud用户等
- 公共claims,定义新创的信息,比如用户信息和其他重要信息
- 私有claims,用于发布者和消费者都同意以私有的方式使用的信息
JWT规定7个官方字段,供选用:
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
支持定义私有字段,示例:
{
"iss": "jwt.io",
"exp": 1496199995458,
"name": "sinwaj",
"role": "admin","
}
JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分
3、signature
需要采用编码的header、编码的payload、secret,使用header中指定的算法进行签名。
三、JWT实战
1、JwtUtil
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
public class JwtUtil {
private final static String base64Security = "";
private final static String clientId = "";
private final static String jwtName = "restapiuser";
/**
* 过期时间,2天
*/ private final static long TTLMillis = 172800 * 1000;
/**
* 解析jwt
*/ public static Claims parseJWT(String jsonWebToken) {
try {
return Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
.parseClaimsJws(jsonWebToken).getBody();
} catch (Exception e) {
return null;
}
}
public static String createJWT(String name, Integer userId) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// 添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
.claim("unique_name", name)
.claim("userid", userId)
.setIssuer(jwtName)
.setAudience(clientId)
.signWith(signatureAlgorithm, signingKey);
// 添加Token过期时间
if (TTLMillis >= 0) {
long expMillis = nowMillis + TTLMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp).setNotBefore(now);
}
// 生成JWT
return builder.compact();
}
public static Claims getUserInfo() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final String authHeader = request.getHeader("authorization");
final String token = authHeader.substring(7);
try {
return JwtUtil.parseJWT(token);
} catch (Exception e) {
return null;
}
}
}
2、拦截器
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
final String authHeader = request.getHeader("authorization");
String url = request.getRequestURL().toString();
String[] urls = url.split("/");
String root = urls[0] + "/" + urls[1] + "/" + urls[2];
// OPTIONS方法放行
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
} else {
boolean checkAuth = null == authHeader || !authHeader.startsWith("Bearer") || authHeader.length() < 7;
if (checkAuth) {
response.sendRedirect(root);
return false;
}
}
final String token = authHeader.substring(7);
try {
final Claims claims = JwtUtil.parseJWT(token);
if (claims == null) {
response.sendRedirect(root);
return false;
}
request.setAttribute("CLAIMS", claims);
return true;
} catch (final Exception e) {
response.sendRedirect(root);
return false;
}
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
}
}
3、Service类
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private String domainIp;
private String domianPort;
@Autowired
UserMapper userMapper;
@Override
public String login(JSONObject jsonObject) {
String name = (String) jsonObject.get("userName");
String pass = (String) jsonObject.get("password");
if (StringUtils.isEmpty(name) || StringUtils.isEmpty(pas)) {
return JSONObject.toJSONString(ServiceUtil.returnError("用户名或密码不能为空!"));
}
try {
User user = new User(name, "1", true);
// 手动在db里面配置新增用户
User userInfo = userMapper.selectBySelectiveFields(user);
if (userInfo == null) {
return JSONObject.toJSONString(ServiceUtil.returnError("用户名不存在!"));
}
Boolean status = DomainUtil.checkDomain("CORP" + name, pass, domainIp, domainPort);
if (status) {
String jwtToken = JwtUtil.createJWT(userInfo.getUserName(), userInfo.getId());
JSONObject data = new JSONObject();
data.put("jwtToken", jwtToken);
data.put("roleId", userMapper.getUserRole(userInfo.getId()));
return JSONObject.toJSONString(ServiceUtil.returnSuccessData(data));
} else {
return JSONObject.toJSONString(ServiceUtil.returnError("用户名或者密码错误!"));
}
} catch (Exception e) {
return JSONObject.toJSONString(ServiceUtil.returnError("系统异常,请稍后再试!"));
}
}
}
4、DomainUtil
public class DomainUtil {
/**
* 内网ldap账户认证
*/ public static Boolean checkDomain(String userName, String password, String domainIp, String domainPort) {
String url = "ldap://" + domainIp + ":" + domainPort;
Hashtable<String, String> env = new Hashtable<>();
javax.naming.directory.DirContext ctx;
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.PROVIDER_URL, url);
env.put(Context.SECURITY_PRINCIPAL, userName);
env.put(Context.SECURITY_CREDENTIALS, password);
try {
// 初始化上下文
ctx = new javax.naming.directory.InitialDirContext(env);
ctx.close();
// 验证成功返回name
return true;
} catch (javax.naming.AuthenticationException e) {
logger.error("认证失败:" + e.getMessage());
return false;
} catch (Exception e) {
logger.error("认证出错:" + e.getMessage());
return false;
}
}
}