chao před 4 roky
rodič
revize
8c0f0071be

+ 69 - 0
src/main/java/com/caimei365/commodity/config/RedisCacheConfig.java

@@ -0,0 +1,69 @@
+package com.caimei365.commodity.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2021/4/28
+ */
+@Configuration
+@EnableCaching
+public class RedisCacheConfig implements Serializable {
+/**
+     * 最新版,设置redis缓存过期时间
+     */
+
+    @Bean
+    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+        return new RedisCacheManager(
+           RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
+           // 默认策略,未配置的 key 会使用这个,3小时失效
+           this.getRedisCacheConfigurationWithTtl( 3 * 60 * 60),
+           // 指定 key 策略
+           this.getRedisCacheConfigurationMap()
+        );
+    }
+
+    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
+        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
+        //自定义设置缓存时间,6小时
+        redisCacheConfigurationMap.put("getCommodityClassify", this.getRedisCacheConfigurationWithTtl(6 * 60 * 60));
+        return redisCacheConfigurationMap;
+    }
+
+
+    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
+        ObjectMapper om = new ObjectMapper();
+        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        jackson2JsonRedisSerializer.setObjectMapper(om);
+        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
+        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
+                RedisSerializationContext
+                        .SerializationPair
+                        .fromSerializer(jackson2JsonRedisSerializer)
+        ).entryTtl(Duration.ofSeconds(seconds));
+
+        return redisCacheConfiguration;
+    }
+
+}

+ 123 - 0
src/main/java/com/caimei365/commodity/config/TokenFilter.java

@@ -0,0 +1,123 @@
+package com.caimei365.commodity.config;
+
+import com.alibaba.fastjson.JSONObject;
+import com.caimei365.commodity.components.RedisService;
+import com.caimei365.commodity.utils.JwtUtil;
+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[]{
+        "/commodity/shop/product/release",
+        "/commodity/shop/product/offline"
+    };
+    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));
+    }
+
+
+}

+ 1 - 1
src/main/java/com/caimei365/commodity/controller/ProductShopApi.java

@@ -201,8 +201,8 @@ public class ProductShopApi {
      *
      * @param soldOutDto {productIds 商品id集合,以','隔开}
      */
-    @PostMapping("/product/offline")
     @ApiOperation("供应商-批量下架商品(旧:/supplier/soldOut)")
+    @PostMapping("/product/offline")
     public ResponseJson soldOut(SoldOutDto soldOutDto) {
         return shopService.soldOutProducts(soldOutDto.getProductIds());
     }

+ 3 - 3
src/main/java/com/caimei365/commodity/service/impl/PageServiceImpl.java

@@ -54,7 +54,7 @@ public class PageServiceImpl implements PageService {
      * @param source   请求来源:www,crm
      */
     @Override
-    @Cacheable(value = "getClassify", key = "#typeSort +'-'+ #source", unless = "#result == null")
+    @Cacheable(value = "getCommodityClassify", key = "#typeSort +'-'+ #source", unless = "#result == null")
     public ResponseJson<List<BigTypeVo>> getClassify(Integer typeSort, String source) {
         List<BigTypeVo> bigTypeList = productTypeMapper.getBigTypeList(typeSort,source);
         bigTypeList.forEach(bigType -> {
@@ -89,7 +89,7 @@ public class PageServiceImpl implements PageService {
      * @param source 来源:1网站,2小程序
      */
     @Override
-    @Cacheable(value = "instrumentData", key = "#pageId+'-'+#userId+'-'+#source", unless = "#result == null")
+    @Cacheable(value = "insCommodityData", key = "#pageId+'-'+#userId+'-'+#source", unless = "#result == null")
     public ResponseJson<Map<String, Object>> getClassifyData(Integer pageId, Integer userId, Integer source) {
         source = source == null ? 1 : source;
         Map<String, Object> map = new HashMap<>(3);
@@ -116,7 +116,7 @@ public class PageServiceImpl implements PageService {
      * @param userId 用户id
      * @param source 来源:1网站,2小程序
      */
-    @Cacheable(value = "getHomeFloorData", key = "#userId +'-'+ #source", unless = "#result == null")
+    @Cacheable(value = "getHomeCommodityData", key = "#userId +'-'+ #source", unless = "#result == null")
     @Override
     public ResponseJson<Map<String, Object>> getHomeData(Integer userId, Integer source) {
         Map<String, Object> map = new HashMap<>(2);

+ 113 - 0
src/main/java/com/caimei365/commodity/utils/JwtUtil.java

@@ -0,0 +1,113 @@
+package com.caimei365.commodity.utils;
+
+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;
+    }
+
+    /**
+     * 从token解析出aud信息,用户名
+     *
+     * @param token
+     * @return
+     */
+    /*public static String parseTokenAud(String token) {
+        DecodedJWT jwt = JWT.decode(token);
+        return jwt.getClaim("aud").asString();
+    }*/
+}