概述 【Java|JWT入门教程】安全的重要性。
简介 JWT,JSON Web Token,开放的、行业标准(RFC 7519),用于网络应用环境间安全传递声明。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所须的声明信息。
特点:
- 跨语言:支持主流语言
- 自包含:包含必要的所有信息,如用户信息和签名等
- 易传递:很方便通过HTTP头部传递
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
- JWT 不加密的情况下,不能将秘密数据写入 JWT
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
- JWT的最大缺点:由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或更改 token 的权限。即,一旦 JWT 签发,在到期之前就会始终有效,除非服务器部署额外的逻辑
- JWT 本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
- 为减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输
header
头部包含两部分:声明类型和使用的散列算法(通常直接使用HMAC SHA256,就是HS256)
{
"typ": "JWT",
"alg": "HS256"
}
将头部进行base64编码构成第一部分。Base64是一种用64个字符来表示任意二进制数据的方法,Base64是一种任意二进制到文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。
payload
也称为JWT claims,放置需要传输的信息,有三类:
- 保留claims,主要包括iss发行者、exp过期时间、sub主题、aud用户等
- 公共claims,定义新创的信息,比如用户信息和其他重要信息
- 私有claims,用于发布者和消费者都同意以私有的方式使用的信息
- 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": "sinwaj",
"role": "admin","
}
JWT默认是不加密的,任何人都可以读到,所以不要把隐私敏感信息放在这个部分。同样需要Base64编码。
signature
需要采用编码的header、编码的payload、secret,使用header中指定的算法进行签名。
对头部以及载荷内容进行签名
适用场景
- 向Web应用传递一些非敏感信息
- 用户认证和授权系统
- Web应用的单点登录
编码
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 = "restapiuser";
/**
* 过期时间,2天
*/
private final static long TTLMillis = 172800 * 1000;
/**
* 解析jwt
*/
public static Claims parseJWT(String token) {
if () {
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;
}
}
}
拦截器
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) {
}}
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 {
private String domainIp;
private String domainPort;
@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, domainIp, domainPort);
if (status) {
String jwtToken = JwtUtil.createJWT(userInfo.getUserName(), userInfo.getId());
JSONObject data = https://www.it610.com/article/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("系统异常,请稍后再试!"));
}
}}
/**
* 内网ldap账户认证
*/
public static Boolean checkDomain(String userName, String password, String domainIp, String domainPort) {
String url = "ldap://" + domainIp + ":" + domainPort;
Hashtable 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认证
推荐阅读
- 重地和通话的哈希值(hash)为何相同?
- c语言|C/JAVA 每日一练——零基础学习动态规划
- 第l个数到第r个数中第K大的数是哪个———蓝桥杯
- java|蓝桥杯第五届省赛java试题及解析(不断更新)
- 还在用递归,试试迭代吧
- 如何在Quarkus 框架中使用 Native Image
- Redis 存储结构体信息,选 hash 还是string()
- 2021 年最常用密码出炉,第一毫无悬念!
- 面试|真别卷了 , 踏踏实实金三银四 , 少走点弯路