传统session认证
传统session,cookie的认证方式,第一次请求是会把用户信息存到服务器内存中,并返回一个JSESSIONId给请求端,之后每次请求都需要携带jsessionId来做认证
传统session,cookie的不足:
JWT官网
JSON Web Token Libraries - jwt.io
官方案例:
pom坐标
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.3</version> </dependency>
JWT的结构:
payload中信息可能被拦截到且可以用base64解码,所以中仅仅放非敏感信息,例如用户名 、appId。而不是用户密码等敏感信息
另外加密过程中的秘钥不能暴露
JWTCreator生成器核心源代码
public final class JWTCreator {
private final Algorithm algorithm;
// 头json
private final String headerJson;
// 负载json
private final String payloadJson;
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if (algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
}
// 给header中赋值
headerClaims.put(PublicClaims.ALGORITHM, algorithm.getName());
if (!headerClaims.containsKey(PublicClaims.TYPE)) {
headerClaims.put(PublicClaims.TYPE, "JWT");
}
String signingKeyId = algorithm.getSigningKeyId();
if (signingKeyId != null) {
withKeyId(signingKeyId);
}
// 先调用创建jwtCreator对象方法,在调用sign生成token
return new JWTCreator(algorithm, headerClaims, payloadClaims).sign();
}
// 创建jwtCreator对象方法
private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {
this.algorithm = algorithm;
try {
// map转String 默认值:"{"typ":"JWT","alg":"HS256"}"
headerJson = mapper.writeValueAsString(headerClaims);
payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
} catch (JsonProcessingException e) {
throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", e);
}
}
// 签名获取token
private String sign() throws SignatureGenerationException {
String header = Base64.getUrlEncoder().withoutPadding().encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
String signature = Base64.getUrlEncoder().withoutPadding().encodeToString((signatureBytes));
// 格式 header.payload.signature
return String.format("%s.%s.%s", header, payload, signature);
}
}
调用JWTCreator生成token工具类:
public class JWTUtils {
private static final String PAYLOAD_ISSUER = "xiaozhen";
private static final String PAYLOAD_COMPANY_ID = "company_id";
private static final String PAYLOAD_APP_ID = "app_id";
/**
* 生成token timestamp代表sign的生成时间
* token格式:由三部分组成header.payload.signature,中间用点分割
* 案例:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ5ZGwiLCJjb21wYW55X3VpZCI6IjEyIiwiZXhwIjoxNjY5MDI0ODYxLCJhcHBfaWQiOiJzZGYiLCJpYXQiOjE2NjkwMTc2NjF9.cy3KN5bOMzzYh4pcTR0pbfFeMD7cl5jqeFXYgxz4JeY
*/
public static TokenDTO generateToken(String appId, String sign, Long timestamp) {
// 查业务数据库 根据appId查询appSecret等秘钥信息 (秘钥不能暴露否则校验就没用了)
String appSecret = "数据库存储的秘钥";
private static final Algorithm TOKEN_ALGORITHM = Algorithm.HMAC256(appSecret);
// 检查签名
String assertSign = DigestUtils.md5Hex(appId + appSecret);
if (!sign.equals(assertSign)) {
throw new XiaoZhenException("code", "签名错误");
}
// 是否过期
boolean expired = (System.currentTimeMillis() - timestamp) > 7200000;
if (expired) {
throw new XiaoZhenException("code", "签名过期");
}
// 设置Token过期时间
long issuedAt = System.currentTimeMillis();
long expiresAt = issuedAt + 7200000;
// 生成Token, with开头的方法均是在设置payload
String token = JWT.create()
.withIssuer(PAYLOAD_ISSUER)
.withClaim(PAYLOAD_COMPANY_ID, "主体id")
.withClaim(PAYLOAD_APP_ID, "appId")
.withIssuedAt(new Date(issuedAt))
.withExpiresAt(new Date(expiresAt))
.sign(TOKEN_ALGORITHM);
log.info("generateToken token: {}", token);
return new TokenDTO()
.setAccessToken(token)
.setExpiresAt(expiresAt);
}
/**
* 返回sign,timestamp生成时间
*/
public Map<String,String> getSign (String appId) {
// 查业务数据库 根据appId查询appSecret等秘钥信息 (秘钥不能暴露否则校验就没用了)
String appSecret = "数据库存储的秘钥";
// 根据appSecret,appId 生产sign
map.put("sign",sign);
map.put(Constants.TIMESTAMP,String.valueOf(timeStamp));
return map;
}
}
拦截器验证token
@Component
public class OpenInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(OpenInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
// 获取token
String token = req.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Boolean result = false;
try {
result = bgTokenFacade.parseToken(token);
// jwt验证器
JWTVerifier verifier = JWT.require(TOKEN_ALGORITHM)
.withIssuer(TOKEN_ISSUER)
.build();
/**
* 验证token
* 源码原理:把token按hearder.payload.signature格式拆分,
* 重新用header和payload生成签名, 和入参中token的signature部分比较,
* 如果一致说明请求合法,否则为非法请求
* (这样不论header、payload、signature哪个被篡改都不会验证通过)
*/
verifier.verify(accessToken);
} catch (Exception e) {
log.error("解析token异常", e);
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
return result;
}
}
生成token的api层:
@RestController
@RequestMapping("/v1/open/oauth")
@Slf4j
public class BgAuthController {
@RequestMapping(value = "/getSign", method = RequestMethod.GET)
@ApiOperation(value = "获取验签")
public Map<String, String> getSign(@RequestParam(value = "appKey",required = true)String appId) {
return JWTUtils.getSign(appId);
}
/**
* 获取 accessToken
*/
@ApiOperation(value = "获取 accessToken")
@PostMapping("/token")
public TokenDTO token(@RequestBody @Valid TokenReq req) {
return JWTUtils.generateToken(req.getAppId(),req.getSign, req.getTimestamp());
}
}
生成的token案例:
格式 header.payload.signature
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ5ZGwiLCJjb21wYW55X3VpZCI6IjEyIiwiZXhwIjoxNjY5MDI0ODYxLCJhcHBfaWQiOiJzZGYiLCJpYXQiOjE2NjkwMTc2NjF9.cy3KN5bOMzzYh4pcTR0pbfFeMD7cl5jqeFXYgxz4JeY
springBoot设置拦截器配置需要token验证的api
@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
//token验证api拦截器
registry.addInterceptor(openInterceptor)
.addPathPatterns("/v1/open/**") // 此contorller业务api均拦截
.excludePathPatterns("/v1/open/oauth/**"); //生成token的api不需拦截
}
}
// 自定义实现拦截器,拦截器中校验token
@Component
public class OpenInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
// 获取token
String token = req.getHeader("Authorization");
// 解析token
Boolean result = JWTUtils.verifierToken();
// 可定义ThreadLocal 把请求头中的用户信息保存起来,供本线程全局使用
// todo private static final ThreadLocal<BO> THREAD_LOCAL = new ThreadLocal<>();
return result;
}
}