JWT 的背景知识可以看这篇文章: JSON Web Token 入门教程
JWT 由三个部分组成:
- Header(头部)
- Payload(负载)
- Signature(签名)
在分布式系统下,存在跨session的问题,则使用JWT实现分布式下,身份验证的功能
JWT有很多的第三方实现,Java JWT 类库对比及使用 - 简书,
此处我们选择Auth0,此案例实现的功能如下
认证中心
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.3</version>
</dependency>
配置:
server:
port: 8080
jwt:
secret: "123321" #私钥
expireTime: 1 #JWT有效期,单位是分钟
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
private String secret;
private int expireTime;
}
自定义异常
public class TokenUnavailableException extends RuntimeException{
public TokenUnavailableException(String message) {
super(message);
}
}
import com.lb.bean.JwtResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(TokenUnavailableException.class)
private ResponseEntity<Object> handlerTokenUnavailableException(TokenUnavailableException e) {
return new ResponseEntity<>(JwtResponse.error(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
JWT - 生成,检验
package com.lb.config;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.lb.bean.JwtResponse;
import com.lb.exception.TokenUnavailableException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Map;
@Component
public class JwtService {
@Autowired
JwtProperties jwtProperties;
public String createToken(String subject, Map<String, String> claimMap) {
JWTCreator.Builder builder = JWT.create()
// .withIssuer("")
// .withNotBefore(null)
// .withIssuedAt(null)
// .withAudience("")
.withSubject(subject) //主题
.withExpiresAt(Date.from(
LocalDateTime.now().plusMinutes(jwtProperties.getExpireTime())
.atZone(ZoneId.systemDefault()).toInstant())
);
claimMap.forEach(builder::withClaim);
return builder.sign(Algorithm.HMAC256(jwtProperties.getSecret()));
}
public JwtResponse validateToken(String token) {
try {
JWTVerifier jwtVerifier =
JWT.require(Algorithm.HMAC256(jwtProperties.getSecret()))
.build();
DecodedJWT decodedJwt = jwtVerifier.verify(token);
return JwtResponse.ok(decodedJwt.getClaims());
} catch (Exception e) {
//校验失败
throw new TokenUnavailableException(e.getMessage());
}
}
public boolean isNeedUpdate(String token) {
try {
Date expiresAt = JWT.require(Algorithm.HMAC256(jwtProperties.getSecret()))
.build()
.verify(token)
.getExpiresAt();
// 需要更新 时间小于一半
return (expiresAt.getTime() - System.currentTimeMillis()) < ((jwtProperties.getExpireTime() * 60000) >> 1);
} catch (TokenExpiredException e) {
return true;
} catch (Exception e) {
//校验失败
throw new TokenUnavailableException(e.getMessage());
}
}
}
controller
@RestController
public class JwtController {
@Autowired
private JwtService jwtService;
@PostMapping("create")
public ResponseEntity<String> generateJwtToken(@RequestBody JwtRequest request) {
String token = jwtService.createToken(request.getSubject(), request.getClaimsMap());
return ResponseEntity.ok(token);
}
@PostMapping("validate")
public ResponseEntity<JwtResponse> validateToken(@RequestParam String token) {
JwtResponse response = jwtService.validateToken(token);
return ResponseEntity.ok(response);
}
@PostMapping("expire")
public ResponseEntity<Boolean> expire(@RequestParam String token) {
return ResponseEntity.ok(jwtService.isNeedUpdate(token));
}
}
客户端模块
请求认证中心,生成token,验证token,更新token
自定义注解,跳过JWT验证的API
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
自定义拦截器,验证JWT
package com.lb.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lb.annotation.PassToken;
import com.lb.bean.JwtResponse;
import com.lb.exception.JwtException;
import com.lb.util.JwtHttpEntityUtil;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
private static ObjectMapper MAPPER = new ObjectMapper();
@Resource(name = "facePlusRestTemplate")
RestTemplate restTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//检查时候包含passtoken注解
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new JwtException(HttpStatus.UNAUTHORIZED, "token 不存在");
}
HttpEntity formEntity = JwtHttpEntityUtil.getValidateEntity(token);
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity("http://localhost:8080/validate", formEntity, String.class);
if (stringResponseEntity == null) {
throw new JwtException(HttpStatus.SERVICE_UNAVAILABLE, "服务不可用,请稍后再试");
}
String body = stringResponseEntity.getBody();
JwtResponse jwtRes = MAPPER.readValue(body, JwtResponse.class);
if (!jwtRes.isFlag()) {
throw new JwtException(HttpStatus.UNAUTHORIZED, jwtRes.getMsg());
}
Map<String, String> claimsMap = jwtRes.getClaimsMap();
request.setAttribute("claims", jwtRes.getClaimsMap());
stringResponseEntity = restTemplate.postForEntity("http://localhost:8080/expire", formEntity, String.class);
if (stringResponseEntity != null && Boolean.parseBoolean(stringResponseEntity.getBody())) {
formEntity = JwtHttpEntityUtil.getCreateTokenEntity(claimsMap.get("sub"), claimsMap.get("pwd"));
token = restTemplate.postForObject("http://localhost:8080/create", formEntity, String.class);
response.setHeader("token", token);
}
return true;
}
}
注册拦截器
import com.lb.exception.FacePlusThrowErrorHandler;
import com.lb.interceptor.JwtAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
@Configuration
public class ApplicationConfig implements WebMvcConfigurer {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);
factory.setConnectTimeout(15000);
return factory;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求
registry.addInterceptor(jwtAuthenticationInterceptor()).addPathPatterns("/**");
}
@Bean
public JwtAuthenticationInterceptor jwtAuthenticationInterceptor() {
return new JwtAuthenticationInterceptor();
}
//自定义处理400,500返回
@Bean
public RestTemplate facePlusRestTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
restTemplate.setErrorHandler(new FacePlusThrowErrorHandler());
return restTemplate;
}
}
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import java.io.IOException;
public class FacePlusThrowErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
throw new JwtException(response.getStatusCode(), response.getBody().toString());
}
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.HashMap;
import java.util.Map;
public class JwtHttpEntityUtil {
private static ObjectMapper MAPPER = new ObjectMapper();
public static HttpEntity<String> getCreateTokenEntity(String userName, String password) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HashMap<String, Object> map = new HashMap<>();
map.put("subject", userName);
Map<String, String> calims = new HashMap<>();
calims.put("pwd", password);
map.put("claimsMap", calims);
String stu = MAPPER.writeValueAsString(map);
return new HttpEntity<String>(stu, headers);
}
public static HttpEntity getValidateEntity(String token) throws JsonProcessingException {
MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<String, String>();
paramMap.set("token",token);
//2、添加请求头
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type","application/x-www-form-urlencoded");
return new HttpEntity(paramMap, headers);
}
}
客户端Controller
@RestController
public class MyController {
@Autowired
RestTemplate restTemplate;
@PassToken
@PostMapping("login")
public String login(String userName, String password, HttpServletResponse response) throws JsonProcessingException {
//模拟数据库查询用户
if ("admin".equals(userName) && "admin".equals(password)) {
HttpEntity<String> formEntity = JwtHttpEntityUtil.getCreateTokenEntity(userName, password);
String token = restTemplate.postForObject("http://localhost:8080/create", formEntity, String.class);
response.setHeader("token", token);
return "登陆成功";
} else {
return "用户名密码不正确";
}
}
@GetMapping("test")
public String test(HttpServletRequest request) {
return request.getAttribute("claims").toString();
}
}
以上代码省略了 异常类和异常处理类
结果
create API -- 拦截器放行login API,并返回token
validate 、expire API -- 验证和更新token
以上案例是为了,实现注册中心是单独服务,如果不需要它是单独的服务,直接在项目内提供工具类直接生成token,以及token的验证。更加简单