Forráskód Böngészése

Token权限控制

chao 3 éve
szülő
commit
3eab878cd1

+ 103 - 0
caimei365-cloud-gateway/src/main/java/com/caimei365/cloud/token/JwtUtil.java

@@ -0,0 +1,103 @@
+package com.caimei365.cloud.token;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JWT工具类
+ *
+ * 标准中注册的声明
+ * iss: jwt签发者
+ * sub: jwt所面向的用户
+ * aud: 接收jwt的一方
+ * exp: jwt的过期时间,这个过期时间必须要大于签发时间
+ * nbf: 定义在什么时间之前,该jwt都是不可用的.
+ * iat: jwt的签发时间
+ * jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
+ *
+ *
+ * @author : Charles
+ * @date : 2021/3/8
+ */
+public class JwtUtil {
+    /**
+     * 设置过期时间: 2*60分钟
+     */
+    private static final long EXPIRE_TIME = 2 * 60 * 60 * 1000;
+    /**
+     * 服务端的私钥secret,在任何场景都不应该流露出去
+     */
+    private static final String TOKEN_SECRET = "zhengchao";
+
+    /**
+     * 生成签名,6EXPIRE_TIME过期
+     */
+    public static String createToken(Integer userId) {
+        try {
+            // 设置过期时间
+            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
+            // 私钥和加密算法
+            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
+            // 设置头部信息
+            Map<String, Object> header = new HashMap<>(2);
+            header.put("typ", "JWT");
+            header.put("alg", "HS256");
+            // 返回token字符串
+            return JWT.create()
+                    .withHeader(header)
+                    .withClaim("uid", userId)
+                    /*.withClaim("aud", mobile)*/
+                    .withExpiresAt(date)
+                    .sign(algorithm);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 检验token是否正确
+     */
+    public static boolean isVerify(String token) {
+        try {
+            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
+            JWTVerifier verifier = JWT.require(algorithm).build();
+            verifier.verify(token);
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 从token解析出uid信息,用户ID
+     */
+    public static int parseTokenUid(String token) {
+        DecodedJWT jwt = JWT.decode(token);
+        return jwt.getClaim("uid").asInt();
+    }
+
+    /**
+     * 从token解析出过期日期时间
+     */
+    public static Date paraseExpiresAt(String token) {
+        DecodedJWT jwt = JWT.decode(token);
+        return jwt.getExpiresAt();
+    }
+
+    /**
+     * 返回设置的过期秒数
+     *
+     * @return long 秒数
+     */
+    public static long getExpireTime() {
+        return EXPIRE_TIME / 1000;
+    }
+
+}

+ 292 - 0
caimei365-cloud-gateway/src/main/java/com/caimei365/cloud/token/RedisService.java

@@ -0,0 +1,292 @@
+package com.caimei365.cloud.token;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis 服务工具类
+ *
+ * @author : Charles
+ * @date : 2021/3/4
+ */
+@Service
+public class RedisService {
+
+	@Resource
+	private RedisTemplate<Serializable, Object> redisTemplate;
+
+    /**
+     * 批量删除
+     * @param keys
+     */
+	public void remove(String... keys) {
+		for(String key :keys){
+			remove(key);
+		}
+	}
+
+    /**
+     * 批量删除正则匹配到的
+     * @param pattern
+     */
+	public void removePattern(String pattern) {
+		Set<Serializable> keys = redisTemplate.keys(pattern);
+        assert keys != null;
+        if (keys.size() > 0){
+			redisTemplate.delete(keys);
+		}
+	}
+
+    /**
+     * 删除
+     * @param key
+     */
+	public void remove(String key) {
+		if (exists(key)) {
+			redisTemplate.delete(key);
+		}
+	}
+
+    /**
+     * 判断缓存中是否存在
+     * @param key
+     * @return boolean
+     */
+	public boolean exists(String key) {
+		return StringUtils.isBlank(key) ? false : redisTemplate.hasKey(key);
+	}
+
+    /**
+     * 读取缓存
+     * @param key
+     * @return
+     */
+	public Object get(String key) {
+		Object result = null;
+		ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
+		result = operations.get(key);
+		return result;
+	}
+
+    /**
+     * 写入缓存
+     * @param key
+     * @param value
+     * @return
+     */
+	public boolean set(String key, Object value) {
+		boolean result = false;
+		try {
+			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
+			operations.set(key, value);
+			result = true;
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return result;
+	}
+
+    /**
+     * 写入缓存并加上过期时间(秒)
+     * @param key
+     * @param value
+     * @param expireTimeSeconds
+     * @return
+     */
+	public boolean set(String key, Object value, Long expireTimeSeconds) {
+		boolean result = false;
+		try {
+			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
+			operations.set(key, value);
+			redisTemplate.expire(key, expireTimeSeconds, TimeUnit.SECONDS);
+			result = true;
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return result;
+	}
+    /**
+     * 写入过期时间(秒)
+     * @param key
+     * @param expireTimeSeconds
+     * @return
+     */
+	public boolean expire(String key, Long expireTimeSeconds) {
+		boolean result = false;
+		try {
+			redisTemplate.expire(key, expireTimeSeconds, TimeUnit.SECONDS);
+			result = true;
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return result;
+	}
+    /* **************************** 针对list操作的方法 **************************** */
+    /**
+     * 在key对应list的尾部添加
+     * @param key
+     * @param value
+     * @return
+     */
+	public long rightPushForList(String key, Object value) {
+		return redisTemplate.opsForList().rightPush(key, value);
+	}
+
+    /**
+     * 在key对应list的头部添加
+     * @param key
+     * @param value
+     * @return
+     */
+	public long leftPushForList(String key, Object value) {
+		return redisTemplate.opsForList().leftPush(key, value);
+	}
+
+    /**
+     * key对应list的长度
+     * @param key
+     * @return
+     */
+	public long listSize(String key) {
+		return redisTemplate.opsForList().size(key);
+	}
+
+    /**
+     * 获取list集合
+     * @param Key
+     * @param begin
+     * @param end
+     * @return
+     */
+	public List<?> getList(String Key, int begin, int end) {
+		return redisTemplate.opsForList().range(Key, begin, end);
+	}
+
+    /**
+     * 在key对应list的尾部移除
+     * @param key
+     * @return
+     */
+	public Object rightPopForList(String key) {
+		return redisTemplate.opsForList().rightPop(key);
+	}
+
+    /**
+     * 在key对应list的头部移除
+     * @param key
+     * @return
+     */
+	public Object leftPopForList(String key) {
+		return redisTemplate.opsForList().leftPop(key);
+	}
+
+    /**
+     * 移除list的index位置上的值
+     * @param Key
+     * @param index
+     * @param value
+     */
+	public void removeList(String Key, long index, Object value) {
+		redisTemplate.opsForList().remove(Key, index, value);
+	}
+
+    /**
+     * 重置list的index位置上的值
+     * @param Key
+     * @param index
+     * @param value
+     */
+	public void setList(String Key, long index, Object value) {
+		redisTemplate.opsForList().set(Key, index, value);
+	}
+
+
+    /**
+     * 写入list
+     * @param key
+     * @param list
+     */
+	public void setList(String key, List list) {
+		if(list!=null&&list.size()>0){
+			for (Object object : list) {
+				rightPushForList(key,object);
+			}
+		}
+	}
+
+    /**
+     * 写入map
+     * @param key
+     * @param map
+     */
+	public void setMap(String key, Map<String, Object> map) {
+		redisTemplate.opsForHash().putAll(key, map);
+	}
+
+    /**
+     * 获取map
+     * @param key
+     * @param mapKey
+     * @return
+     */
+	public Object get(String key, String mapKey) {
+		return redisTemplate.opsForHash().get(key, mapKey);
+	}
+
+    /**
+     * 写入map
+     * @param key
+     * @param hashKey
+     * @param hashValue
+     */
+	public void setMapByKV(String key, Object hashKey, Object hashValue) {
+		redisTemplate.opsForHash().put(key, hashKey,hashValue);
+	}
+
+    /**
+     * 删除map中的某个key-value
+     * @param key
+     * @param hashKey
+     */
+	public void removeHash(String key, String hashKey) {
+		redisTemplate.opsForHash().delete(key, hashKey);
+	}
+
+    /**
+     *
+     * @param key
+     * @return
+     */
+	public Map<Object, Object> getEntries(String key) {
+		return redisTemplate.opsForHash().entries(key);
+	}
+
+    /**
+     *
+     * @param key
+     * @param step
+     * @return
+     */
+	public long increase(String key, long step) {
+		return redisTemplate.opsForValue().increment(key, step);
+	}
+
+    /**
+     * 获取失效时间
+     * @param key
+     * @return
+     */
+	public long getExpireTime(String key) {
+		return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+	}
+
+}

+ 136 - 0
caimei365-cloud-gateway/src/main/java/com/caimei365/cloud/token/TokenFilter.java

@@ -0,0 +1,136 @@
+package com.caimei365.cloud.token;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * JWT Token
+ *
+ * 续签逻辑:
+ * 登录成功后,用户在未过期时间内继续操作,续签token。
+ * 登录成功后,空闲超过过期时间,返回token已失效,重新登录。
+ * 实现逻辑:
+ * 1.登录成功后将token存储到redis里面(这时候k、v值一样都为token),并设置过期时间为token过期时间
+ * 2.当用户请求时token值还未过期,则重新设置redis里token的过期时间。
+ * 3.当用户请求时token值已过期,但redis中还在,则JWT重新生成token并覆盖v值(这时候k、v值不一样了),然后设置redis过期时间。
+ * 4.当用户请求时token值已过期,并且redis中也不存在,则用户空闲超时,返回token已失效,重新登录。
+ *
+ * @author : Charles
+ * @date : 2021/3/30
+ */
+@Slf4j
+@Component
+public class TokenFilter implements WebFilter {
+
+    private static final String AUTH = "X-Token";
+    /**
+     * 需要权限认证的接口路径
+     */
+    private static final String[] PERMISSION_URLS = new String[]{
+        "/user/club/info/update",
+        "/user/shop/info/update",
+        "/order/club/cart/add",
+        "/order/club/cart/update",
+        "/order/club/cart/delete",
+        "/order/seller/cart/add",
+        "/order/seller/cart/update",
+        "/order/seller/cart/delete",
+        "/order/club/confirm",
+        "/order/club/cancel",
+        "/order/club/delete",
+        "/order/club/receive",
+        "/order/club/second/payment/confirm",
+        "/order/submit/generate",
+        "/order/submit/recharge",
+        "/order/pay/balance/deduction"
+    };
+
+    private RedisService redisService;
+    @Autowired
+    public void setRedisService(RedisService redisService) {
+        this.redisService = redisService;
+    }
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+        ServerHttpRequest request = exchange.getRequest();
+        ServerHttpResponse response = exchange.getResponse();
+
+        HttpHeaders header = request.getHeaders();
+        String token = header.getFirst(AUTH);
+        String url = request.getURI().getPath();
+
+        if (StringUtils.isBlank(token) && Arrays.asList(PERMISSION_URLS).contains(url)) {
+            log.error("未经授权,Token为空!");
+            return tokenErrorResponse(response, "未经授权,Token为空!");
+        }
+
+        if (StringUtils.isNotBlank(token)) {
+            String cacheToken = (String) redisService.get(token);
+            // token续签
+            if (StringUtils.isNotBlank(cacheToken) && !"null".equals(cacheToken) && JwtUtil.isVerify(cacheToken)) {
+                int userId = JwtUtil.parseTokenUid(cacheToken);
+                log.debug("Token续签,UserId:"+userId+",Token:"+token);
+                // 再次校验token有效性
+                if (!JwtUtil.isVerify(cacheToken)) {
+                    // 生成token
+                    String newToken = JwtUtil.createToken(userId);
+                    // 将token存入redis,并设置超时时间(秒)
+                    redisService.set(token, newToken, JwtUtil.getExpireTime());
+                } else {
+                    // 重新设置超时时间(秒)
+                    redisService.expire(token, JwtUtil.getExpireTime());
+                }
+            } else {
+                // 需要验证的路径
+                if(Arrays.asList(PERMISSION_URLS).contains(url)) {
+                    // Token失效
+                    log.error("Token失效,token:"+token+",cacheToken:"+cacheToken);
+                    return tokenErrorResponse(response, "Token失效,请重新登录!");
+                }
+            }
+        }
+        //            //TODO 将用户信息存放在请求header中传递给下游业务
+        //            ServerHttpRequest.Builder mutate = request.mutate();
+        //            mutate.header("demo-user-name", username);
+        //            ServerHttpRequest buildReuqest = mutate.build();
+        //
+        //            //todo 如果响应中需要放数据,也可以放在response的header中
+        //            response.setStatusCode(HttpStatus.OK);
+        //            response.getHeaders().add("demo-user-name",username);
+        //            return chain.filter(exchange.mutate()
+        //                    .request(buildReuqest)
+        //                    .response(response)
+        //                    .build());
+        return chain.filter(exchange);
+    }
+
+    private Mono<Void> tokenErrorResponse(ServerHttpResponse response, String responseMsg){
+        response.setStatusCode(HttpStatus.OK);
+        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
+        JSONObject res = new JSONObject();
+        res.put("code", -99);
+        res.put("msg", responseMsg);
+        byte[] responseByte = res.toJSONString().getBytes(StandardCharsets.UTF_8);
+        DataBuffer buffer = response.bufferFactory().wrap(responseByte);
+        return response.writeWith(Flux.just(buffer));
+    }
+
+
+}

+ 32 - 0
pom.xml

@@ -46,6 +46,38 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-webflux</artifactId>
         </dependency>
+        <!-- redis依赖包 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.lettuce</groupId>
+                    <artifactId>lettuce-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+        </dependency>
+        <!-- jwt -->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.14.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <!-- fastjson -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.75</version>
+        </dependency>
         <!--
         <dependency>
             <groupId>org.springframework.boot</groupId>