springCloud gateway+jwt方式做token校验
项目中需要做接口的token校验,项目api是作为eureka-client去调用服务的,所有的以api开头请求过来都需要进行校验是否有token并校验其正确性。1.pom文件:<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=...
项目中需要做接口的token校验,项目大致分为以下几个模块:
eureka:eureka的注册中心
eureka-server:服务提供者
api:服务消费者,使用fegin消费服务
gateway:网关,用于进行token校验
1. eureka注册中心
配置了一个服务中心,并无具体的代码操作
pom文件中引入eureka的依赖,在启动类中加入@EnableEurekaServer
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
配置文件内容:
server:
port: 8083
spring:
application:
name: register-center
eureka:
instance:
#注册中心的ip地址
hostname: 127.0.0.1
client:
register-with-eureka: false
fetch-registry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
2. eureka-server
提供服务,返回数据均为json串,使用restful方式的接口,接口中使用RequestMapping代替GetMapping和PostMapping
3. 服务的消费端api
这里需要用到redis及md5加密,引入jar包
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version> 2.6.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.0.0</version>
</dependency>
<!-- jwt鉴权 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
登录的controller:
@RestController
@RequestMapping("/route-api")
public class LoginController {
@Autowired
private MemberService memberService;
private ResponseDto responseDto;
@Autowired
private TokenUtil tokenUtil;
/**
* 登录方法
* @param phone
* @param password
* @return
*/
@GetMapping(value = "/login")
public String login(@RequestParam("phone") String phone, @RequestParam("password") String password){
//账号密码校验
if(StrUtil.isNotEmpty(phone) && StrUtil.isNotEmpty(password)){
String result = memberService.login(phone,password);
if (result != null ){
String object = (String)JSON.parse(result);
DataResponseDto responseData = JSON.parseObject(object,DataResponseDto.class);
if (responseData.getCode().equals(StatusCodeConstants.SUCCESS)){
Map<String, String> map = tokenUtil.getToken(phone, "1");
//返回结果
responseDto = new ResponseDto(StatusCodeConstants.SUCCESS,"成功并返回数据", map.get("token"), map.get("refreshToken"),responseData.getData());
}else {
responseDto = new ResponseDto(responseData.getCode(),responseData.getMessage(),null);
}
}else {
responseDto = new ResponseDto(StatusCodeConstants.PHONE_OR_PASSWORD_ERROR,"手机号或密码错误",null);
}
}else {
responseDto = new ResponseDto(StatusCodeConstants.PHONE_OR_PASSWORD_ERROR,"手机号或密码错误",null);
}
return JSON.toJSONString(responseDto);
}
/**
* 刷新JWT
* @param refreshToken
* @return
*/
@GetMapping("/refresh")
public String refreshToken(@RequestParam("refreshToken") String refreshToken, @RequestParam("phone") String phone){
responseDto = tokenUtil.refreshToken(phone, "1", refreshToken);
return JSON.toJSONString(responseDto);
}
/**
* 退出时删除key
* @param phone
* @return
*/
@GetMapping("/logout")
public String logout( @RequestParam("phone") String phone){
tokenUtil.removeToken(phone, "1");
responseDto = new ResponseDto(StatusCodeConstants.SUCCESS, "退出成功",null);
return JSON.toJSONString(responseDto);
}
}
TokenUtil代码:
@Component
@PropertySource(value = "classpath:jwt.properties")
public class TokenUtil {
@Value("${token.expire.time}")
private long tokenExpireTime;
@Value("${refresh.token.expire.time}")
private long refreshTokenExpireTime;
private Map<String , String> map = new HashMap<>(2);
/**
* 固定的头
*/
private static final String OPERATE = "OPERATE";
private static final String USER = "USER";
private static final String WX = "WX";
private ResponseDto responseDto;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 生成token和refreshToken
* @param phone
* @param type
* @return
*/
public Map<String, String> getToken(String phone, String type){
//生成refreshToken
String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
String prefix = this.getPrefix(type);
String token = this.buildJWT(phone, prefix);
String key = SecureUtil.md5(prefix + phone);
//向hash中放入数值
stringRedisTemplate.opsForHash().put(key,"token", token);
stringRedisTemplate.opsForHash().put(key,"refreshToken", refreshToken);
//设置key过期时间
stringRedisTemplate.expire(key,
refreshTokenExpireTime, TimeUnit.MILLISECONDS);
map.put("token", token);
map.put("refreshToken", refreshToken);
return map;
}
/**
* 刷新token
* @param phone
* @param type
* @param refreshToken
* @return
*/
public ResponseDto refreshToken(String phone, String type, String refreshToken){
String prefix = this.getPrefix(type);
String key = SecureUtil.md5(prefix + phone);
String oldRefresh = (String) stringRedisTemplate.opsForHash().get(key, "refreshToken");
if (StrUtil.isBlank(oldRefresh)){
responseDto = new ResponseDto(StatusCodeConstants.REFRESH_TOKEN_TIME_OUT,"refreshToken过期",null);
}else {
if (!oldRefresh.equals(refreshToken)){
responseDto = new ResponseDto(StatusCodeConstants.REFRESH_TOKEN_ERROR,"refreshToken错误",null);
System.out.println("refreshToken错误");
}else {
String token = this.buildJWT(phone, prefix);
stringRedisTemplate.opsForHash().put(key,"token", token);
stringRedisTemplate.opsForHash().put(key,"refreshToken", refreshToken);
stringRedisTemplate.expire(key,
refreshTokenExpireTime, TimeUnit.MILLISECONDS);
responseDto = new ResponseDto(StatusCodeConstants.SUCCESS,"成功并返回数据", token, refreshToken);
}
}
return responseDto;
}
/**
* 删除key
* @param phone
* @param type
*/
public boolean removeToken(String phone, String type){
String prefix = this.getPrefix(type);
String key = SecureUtil.md5(prefix + phone);
return stringRedisTemplate.delete(key);
}
/**
* 获取前缀
* @param type 1 操作端 2 用户端 3 小程序
* @return
*/
private String getPrefix(String type){
String prefix = null;
if ("1".equals(type)){
prefix =OPERATE;
}else if ("2".equals(type)){
prefix = USER;
}else if ("3".equals(type)){
prefix =WX;
}
return prefix;
}
/**
* 生成jwt
* @param phone 手机号
* @param prefix 前缀
* @return
*/
private String buildJWT(String phone, String prefix){
//生成jwt
Date now = new Date();
Algorithm algo = Algorithm.HMAC256(prefix);
String token = JWT.create()
//签发人
.withIssuer("userPhone")
//签发时间
.withIssuedAt(now)
//过期时间
.withExpiresAt(new Date(now.getTime() + tokenExpireTime))
//自定义的存放的数据
.withClaim("phone", phone)
//签名
.sign(algo);
return token;
}
}
因为我这里是需要给操作端、用户端、小程序提供接口,所以用type定义不同的头以区分key,也可以选择使用不同的datebase。
ResponseDto 是自定义的返回参数类:
@Setter
@Getter
public class ResponseDto<T>{
/**
* 状态码
*/
private Integer code;
/**
* 状态信息
*/
private String message;
/**
* 数据
*/
private T data;
/**
* token
*/
private String token;
/**
* 刷新token
*/
private String refreshToken;
public ResponseDto(){
}
public ResponseDto(Integer code, String message, T data){
this.code = code;
this.data = data;
this.message = message;
}
public ResponseDto(Integer code, String message, String token,String refreshToken){
this.code = code;
this.message = message;
this.token = token;
this.refreshToken = refreshToken;
}
public ResponseDto(Integer code, String message, String token,String refreshToken,T data){
this.code = code;
this.message = message;
this.token = token;
this.refreshToken = refreshToken;
this.data = data;
}
}
service层主要是在进行调用方法操作:
@FeignClient(name = "service-provider")
public interface MemberService {
/**
* 调取登录的方法
* @param phone
* @param password
* @return
*/
@RequestMapping("/login")
String login(@RequestParam("phone") String phone, @RequestParam("password") String password);
}
需要在启动类上加@EnableFeignClients和@EnableEurekaClient注解
yaml配置文件:
eureka:
instance:
prefer-ip-address: true
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则),默认30
lease-renewal-interval-in-seconds: 10
#Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒,超过则剔除(客户端告诉服务端按照此规则等待自己),默认90
lease-expiration-duration-in-seconds: 12
client:
registry-fetch-interval-seconds: 10 #eureka client刷新本地缓存时间,默认30
serviceUrl:
defaultZone: http://127.0.0.1:8083/eureka/
server:
port: 8082
spring:
application:
name: server-client
#设置允许 一个项目中有name一样的FeignClient
main:
allow-bean-definition-overriding: true
#redis配置
redis:
host:
port:
password:
database: 1
timeout: 60s
## springboot2.0之后将连接池由jedis改为lettuce
lettuce:
pool:
max-idle: 30
max-active: 8
max-wait: 10000
min-idle: 10
jwt.properties中的配置:
#token过期时间:30分钟
token.expire.time=1800000
#refreshToken过期时间:12小时
refresh.token.expire.time=43200000
四.gateway中转发和过滤接口
这里在引入时候,需要注意:Spring Cloud Gateway 不使用 Web 作为服务器,而是 使用 WebFlux 作为服务器,Gateway 项目已经依赖了 starter-webflux,所以这里 千万不要依赖 starter-web
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>4.5.2</version>
</dependency>
<!-- 使用阿里的fastjson解析json数据 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.57</version>
</dependency>
全局过滤器代码,其中StatusCodeConstants定义了一些常量:
@Component
@PropertySource(value = "classpath:jwt.properties")
public class ApiGlobalFilter implements GlobalFilter, Ordered {
/**
* 不进行token校验的请求地址
*/
@Value("#{'${jwt.ignoreUrlList}'.split(',')}")
public List<String> ignoreUrl;
/**
* 拦截所有的请求头
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().toString();
boolean status = CollectionUtil.contains(ignoreUrl, requestUrl);
if (!status){
String token = exchange.getRequest().getHeaders().getFirst("token");
//type用于区分不同的端,在做校验token时需要
String type= exchange.getRequest().getHeaders().getFirst("type");
ServerHttpResponse response = exchange.getResponse();
//没有数据
if (StrUtil.isBlank(token) || StrUtil.isBlank(type)) {
JSONObject message = new JSONObject();
message.put("code", StatusCodeConstants.TOKEN_NONE);
message.put("message", "鉴权失败,无token或类型");
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "text/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
//有数据
}else {
String prefix = this.getPrefix(type);
//校验token
String userPhone = verifyJWT(token ,prefix);
if (StrUtil.isEmpty(userPhone)){
JSONObject message = new JSONObject();
message.put("message", "token错误");
message.put("code", StatusCodeConstants.TOKEN_ERROR);
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "text/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
//将现在的request,添加当前身份
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserName", userPhone).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
}
return chain.filter(exchange);
}
/**
* JWT验证
* @param token
* @return userPhone
*/
private String verifyJWT(String token, String prefix){
String userPhone;
try {
Algorithm algorithm = Algorithm.HMAC256(prefix);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("userPhone")
.build();
DecodedJWT jwt = verifier.verify(token);
userPhone = jwt.getClaim("phone").asString();
} catch (JWTVerificationException e){
e.printStackTrace();
return "";
}
return userPhone;
}
/**
* 根据type获取前缀
* @param type
* @return
*/
private String getPrefix(String type){
String prefix = null;
if ("1".equals(type)){
prefix = "OPERATE";
}else if ("2".equals(type)){
prefix = "USER";
}else if ("3".equals(type)){
prefix = "WX";
}
return prefix;
}
@Override
public int getOrder() {
return -200;
}
}
jwt.properties中的配置了不拦截的请求:
jwt.ignoreUrlList=/route-api/login,/route-api/refresh
配置文件:
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:8083/eureka/
server:
port: 8087
spring:
application:
name: gateway
cloud:
gateway:
routes:
#netty 路由过滤器,http或https开头 lb://代表是eureka服务的名称 predicates:表示会过滤掉的请求头
- id: gateway-route
uri: lb://server-client
predicates:
- Path=/api/**,/route-api/**
#处理跨域请求问题
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
#redis配置
redis:
host:
port: 6379
password:
database: 1
timeout: 60s
## springboot2.0之后将连接池由jedis改为lettuce
lettuce:
pool:
max-idle: 30
max-active: 8
max-wait: 10000
min-idle: 10
更多推荐
所有评论(0)