JWT入门教程
简介
JWT,JSON Web Token,开放的、行业标准(RFC 7519),用于网络应用环境间安全传递声明。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所须的声明信息。
特点:
- 跨语言:支持主流语言
- 自包含:包含必要的所有信息,如用户信息和签名等
- 易传递:很方便通过HTTP头部传递
具体来说:
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
- JWT 不加密的情况下,不能将秘密数据写入 JWT
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
- JWT的最大缺点:由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或更改 token 的权限。即一旦签发JWT,在到期之前就会始终有效,除非服务器部署额外的逻辑
- JWT 本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
- 为减少盗用,JWT应使用HTTPS协议传输
JWT的工作流程
组成
JWT的token是三段由小数点分隔组成的字符串:header.payload.signature
,即头部、载荷与签名。Base64编码各个字符串。Base64用64个字符来表示任意二进制数据的方法,常用于URL、Cookie、网页中传输少量二进制数据。
header
头部包含两部分:声明类型和使用的散列算法(通常直接使用HMAC SHA256,就是HS256)
{
"typ": "JWT",
"alg": "HS256"
}
将头部进行base64编码构成第一部分。
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):编号,唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
支持定义私有字段,示例:
{
"iss": "jwt.io",
"exp": 1496199995458,
"name": "johnny",
"role": "admin","
}
JWT默认是不加密的,任何人都可以读到,所以不要把隐私敏感信息放在这个部分。
signature
需要采用编码的header、编码的payload、secret,使用header中指定的算法进行签名。
对头部以及载荷内容进行签名。
适用场景
- 向Web应用传递一些非敏感信息
- 用户认证和授权系统
- Web应用的单点登录
实战
引入类库jjwt(jwt的Java实现版):
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
JwtUtil.java
工具类,有3个工具类方法:根据用户名(一般是域账户)和用户ID生成jwt token,解析JWT token,根据登录态解析并获取该token对应的用户信息。记住,需要替换base64Security、clientId、jwtName3个变量,
package com.aaa.utils;
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 = "";
/**
* 过期时间,2天
*/
private final static long TTLMillis = 172800 * 1000;
/**
* 解析jwt
*/
public static Claims parseJWT(String token) {
if (StringUtils.isBlank(token) || !token.contains(".")) {
return null;
}
try {
return Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
.parseClaimsJws(token).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;
}
}
}
JWTInterceptor.java
,登录态拦截器,request为空或非法,则跳转到登录页面;随后,request解析JWT token失败,依然是跳转到登录页面,解析成功,将JWT token信息再通过request.setAttribute()
存入Request中。
package com.aaa.filter;
import com.aaa.utils.JwtUtil;
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) {
}
}
UserServiceImpl.java
用户登录类,先根据LDAP来校验用户名和密码信息,校验通过后,将用户的域名信息和用户ID信息生成JWT token。
package com.aaa.service.user.impl;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.aaa.dao.UserMapper;
import com.aaa.model.User;
import com.aaa.service.user.UserService;
import com.aaa.utils.DomainUtil;
import com.aaa.utils.JwtUtil;
import com.aaa.utils.ServiceUtil;
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 {
@Autowired
private 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);
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("系统异常,请稍后再试!"));
}
}
}
DomainUtil.checkDomain()
方法,根据LDAP协议服务来校验用户的登录认证(域账户和电脑开机密码,此密码一般情况下会依据公司的安全部门要求3个月更新一次)
/**
* 内网ldap账户认证
*/
public static Boolean checkDomain(String userName, String password) {
String url = "ldap://" + ldapIp + ":" + ldapPort;
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;
}
}
对比
token的认证和session认证