《微服务之间鉴权的流程》
说明:之前我们在网关微服务已经测试通过了生成token的方法,现在我们需要编写定时来获取token。
我们这里用到了SpringTask定时任务功能,接着我们在网关开启SpringTask进行测试。
package com.leyou.gateway.scheduler;
import org.joda.time.DateTime;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时任务类
*/
@Component
public class SchedulingDemo {
/**
* 固定频率(每隔多久) 单位:毫秒
*/
@Scheduled(fixedRate = 5000)
public void rateJob(){
System.out.println("固定频率任务触发:"+ DateTime.now());
}
}
上面不管是固定频率还是固定延迟,执行规则都比较简单,假如我们有非常复杂的执行要求,就要用到cron表达式了。
cron表达式分七个域,分别为:
年:一般不指定
周:? * ,- /
月:* , - /
日:? * , - /
时:* , - /
分:* , - /
秒:* , - /
使用cron表达式指定一个,每年,每月,1到6号,每天上午9点,从3分开始,每隔5分钟的第八秒执行一次。
package com.leyou.gateway.scheduler;
import org.joda.time.DateTime;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时任务类
*/
@Component
public class SchedulingDemo {
/**
* cron表达式
* cron表达式:SpringTask的cron只有6位(不能设置年)
* 秒 分 时 日 月 周
*/
@Scheduled(cron = "0/5 * * * * ?")
public void cronJob(){
System.out.println("cron任务触发:"+ DateTime.now());
}
}
spring:
task:
scheduling:
pool:
size: 1 #一般有几个定时任务就开启几个线程
接下来我们分别在网关微服务和搜索微服务定时获取Token。
首先,在网关微服务完成Token获取。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.leyougroupId>
<artifactId>ly-client-authartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
package com.leyou;
import com.leyou.gateway.config.FilterProperties;
import com.leyou.gateway.config.JwtProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringCloudApplication
@EnableScheduling //开启定时任务
@EnableFeignClients
public class LyGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(LyGatewayApplication.class, args);
}
}
在application.yml添加当前服务的服务名称和服务密钥
ly:
jwt:
pubKeyPath: D:\studyProject\rsa\ras-key.pub # 公钥地址
cookie:
cookieName: LY_TOKEN # cookie名称
app:
serviceName: api-gateway
secret: api-gateway
package com.leyou.gateway.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
……
private AppPojo app = new AppPojo();
@Data
public class AppPojo{
private String serviceName;
private String secret;
}
……
}
package com.leyou.gateway.scheduler;
import com.leyou.auth.client.AuthClient;
import com.leyou.gateway.config.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时向授权中心申请服务token
*/
@Component
@Slf4j
public class AppTokenScheduler {
@Autowired
private AuthClient authClient;
@Autowired
private JwtProperties jwtProps;
//设计一个成员变量,用于存放当前服务的token
private String appToken;
/**
* token刷新间隔
*/
private static final long TOKEN_REFRESH_INTERVAL = 86400000L; //24小时
/**
* token获取失败后重试的间隔
*/
private static final long TOKEN_RETRY_INTERVAL = 10000L;
/**
* 固定频率(1天调用1次)
*/
@Scheduled(fixedRate = TOKEN_REFRESH_INTERVAL)
public void appToken(){
//需要设计重试机制,避免1次申请不到没有token可用的情况
while(true) {
try {
//向授权中心申请服务token
String appToken = authClient.authorization(
jwtProps.getApp().getServiceName(),
jwtProps.getApp().getSecret());
//进行token赋值
this.appToken = appToken;
log.info("【申请token】申请token成功,服务名称:"+jwtProps.getApp().getServiceName());
//一旦toke获取成功,则退出
break;
} catch (Exception e) {
e.printStackTrace();
log.error("【申请token】申请token失败,10秒后重试...");
//10秒后重试
try {
Thread.sleep(TOKEN_RETRY_INTERVAL);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
//提供get方法给外部获取服务token
public String getAppToken() {
return appToken;
}
}
<dependency>
<groupId>com.leyougroupId>
<artifactId>ly-client-authartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 搜素微服务
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableScheduling
public class LySearchApplication {
public static void main(String[] args) {
SpringApplication.run(LySearchApplication.class,args);
}
}
ly:
jwt:
pubKeyPath: D:\studyProject\rsa\ras-key.pub # 公钥地址
cookie:
cookieName: LY_TOKEN # cookie名称
app:
serviceName: search-service
secret: search-service
package com.leyou.search.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
/**
* 读取Jwt相关配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
private String pubKeyPath;//公钥路径
private PublicKey publicKey;//公钥
private CookiePojo cookie = new CookiePojo();
private AppTokenPojo app = new AppTokenPojo();
@Data
public class CookiePojo{
private String cookieName;
}
@Data
public class AppTokenPojo{
private String serviceName;
private String secret;
}
/**
* 读取公钥
*/
@PostConstruct
public void initMethod() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
package com.leyou.search.scheduled;
import com.leyou.auth.client.AuthClient;
import com.leyou.search.config.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时获取Token
*/
@Component
@Slf4j
public class AppTokenScheduled {
/**
* token刷新间隔
*/
private static final long TOKEN_REFRESH_INTERVAL = 86400000L;
/**
* token获取失败后重试的间隔
*/
private static final long TOKEN_RETRY_INTERVAL = 10000L;
@Autowired
private AuthClient authClient;
@Autowired
private JwtProperties jwtProperties;
private String token;//保存成功后token
/**
* 每个24小时获取一次Token,如果获取失败,10s后重试
*/
@Scheduled(fixedRate = TOKEN_REFRESH_INTERVAL)
public void autoAppAuth(){
while(true){
try {
//请求token
String token = authClient.authorization(jwtProperties.getApp().getServiceName(),
jwtProperties.getApp().getSecret());
this.token = token;
log.info("【服务自动获取token】- "+jwtProperties.getApp().getServiceName()+"连接成功");
break;
}catch (Exception e){
try {
log.info("【服务自动获取token】- "+jwtProperties.getApp().getServiceName()+"连接失败,10秒后重试...");
//10秒后重试
Thread.sleep(TOKEN_RETRY_INTERVAL);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
//获取token的方法
public String getToken() {
return token;
}
}
思路:网关里面有很多过滤器,我们可以在网关去请求其他微服务之前,对所有请求加上请求头。
这里,我们直接修改之前编写的认证请求过滤器。
package com.leyou.common.constants;
/**
* 常量类
*/
public class LyConstants {
/**
* 服务token的请求头名称
*/
public static final String APP_TOKEN_HEADER = "LY_APP_HEADER";
}
package com.leyou.gateway.filter;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.common.auth.utils.Payload;
import com.leyou.common.auth.utils.UserInfo;
import com.leyou.common.constants.LyConstants;
import com.leyou.common.utils.CookieUtils;
import com.leyou.gateway.config.FilterProperties;
import com.leyou.gateway.config.JwtProperties;
import com.leyou.gateway.scheduler.AppTokenScheduler;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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 reactor.core.publisher.Mono;
import java.util.List;
/**
* 网关鉴权过滤器
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private JwtProperties jwtProps;
@Autowired
private FilterProperties filterProps;
@Autowired
private AppTokenScheduler appTokenScheduler;
/**
* 编写过滤逻辑
* @param exchange 封装了request和response
* @param chain 过滤器链,用于控制过滤器的执行(例如,放行)
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request和response
//ServerHttpRequest是Spring提供,对HttpServletRequest对象的二次封装(不是继承关系)
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//======在网关的请求头中加入服务token===================
request.mutate().header(LyConstants.APP_TOKEN_HEADER,appTokenScheduler.getAppToken());
//加入请求白名单
//1)获取当前请求信息(URL)
String uri = request.getURI().getPath(); // /api/item/category/of/parent
//2)判断uri是否在白名单中,在的话则放行
List<String> allowPaths = filterProps.getAllowPaths();
for(String allowPath:allowPaths){
if(uri.contains(allowPath)){
//放行请求
return chain.filter(exchange);
}
}
//1. 获取请求中的token(Cookie)
//getCookies():获取浏览器的所有的Cookie
String token = null;
try {
token = request.getCookies().getFirst(jwtProps.getCookie().getCookieName()).getValue();
} catch (Exception e) {
//返回状态码401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//注意:必须中止请求
return response.setComplete();
}
if(StringUtils.isEmpty(token)){
//返回状态码401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//注意:必须中止请求
return response.setComplete();
}
//2. 校验token是否合法(是否可以使用公钥解密)
Payload<UserInfo> payload = null;
try {
payload = JwtUtils.getInfoFromToken(token, jwtProps.getPublicKey(), UserInfo.class);
} catch (Exception e) {
//返回状态码401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//注意:必须中止请求
return response.setComplete();
}
//3)从token中取出登录用户信息(登录用户ID)
UserInfo userInfo = payload.getInfo();
Long userId = userInfo.getId();
/**
* 3)从token中取出登录用户信息(登录用户ID)
* 4)根据用户ID查询当前用户拥有的权限(用户->角色->权限),得到一个权限列表(RBAC模型)(path和method)
* 5)获取当前访问的请求信息(请求URL,请求方式)
* 6) 判断权限列表中是否包含当前请求,如果包含,可以放行;否则,拒绝访问。
*/
//放行请求
return chain.filter(exchange);
}
/**
* 过滤器优先级的设置
* 数值越小,优先级越大
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
在ly-item
项目的CategoryController,看看经过网关转发后,能否接收到token
/**
* 分类控制器
*/
@RestController
//@CrossOrigin //跨域注解
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 根据父id查询分类
*/
@GetMapping("/category/of/parent")
public ResponseEntity<List<Category>> findCategoriesById(@RequestParam("pid") Long pid, HttpServletRequest request){
System.out.println("服务token="+request.getHeader(LyConstants.APP_TOKEN_HEADER));
List<Category> categories = categoryService.findCategoriesById(pid);
//return ResponseEntity.status(HttpStatus.OK).body(categories);
return ResponseEntity.ok(categories);
}
重启项目后,浏览器直接输入:http://api.leyou.com/api/item/category/of/parent?pid=0
如果看到输入了token信息,代表网关成功携带token啦!
因为搜索微服务属于普通微服务,是通过Feign来调用另一个微服务的,所以需要编写feign的请求拦截器来携带token到另一个微服务。
在ly-search
项目添加AuthFeignInterceptor类,如下:
package com.leyou.search.feign;
import com.leyou.common.constants.LyConstants;
import com.leyou.search.scheduler.AppTokenScheduler;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 把服务token放入Feign的请求头中
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
@Autowired
private AppTokenScheduler appTokenScheduler;
@Override
public void apply(RequestTemplate template) {
template.header(LyConstants.APP_TOKEN_HEADER,appTokenScheduler.getAppToken());
}
}
因为搜索功能需要从搜索服务 调用 商品服务,所以随便找个需要调用的方法来测试就好
/**
* 根据分类id集合 查询 分类对象集合
*/
@GetMapping("/category/list")
public ResponseEntity<List<Category>> findCategoriesByIds(@RequestParam("ids") List<Long> ids, HttpServletRequest request){
System.out.println("AppToken: "+request.getHeader(LyConstants.APP_TOKEN_HEADER));
List<Category> categories = categoryService.findCategoriesByIds(ids);
return ResponseEntity.ok(categories);
}
注意:测试后记得把测试代码删除!!!
思路:如果我们在每个处理器内部,对访问者进行权限校验,就会使得权限校验的代码与业务代码耦合太紧密,springmvc为我们提供了aop思想的拦截器,可以解开此耦合。
下面在搜索微服务添加过滤器
package com.leyou.search.interceptor;
import com.leyou.common.auth.utils.AppInfo;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.common.auth.utils.Payload;
import com.leyou.common.constants.LyConstants;
import com.leyou.search.config.JwtProperties;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 服务token验证拦截器
*/
@Component
public class AppTokenInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProps;
/**
* 在Controller方法执行之前 被调用
* @param request
* @param response
* @param handler
* @return true: 放行 false:拒绝访问
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求的token
String appToken = request.getHeader(LyConstants.APP_TOKEN_HEADER);
if(StringUtils.isEmpty(appToken)){
//拒绝访问
return false;
}
//2.验证token的合法性
Payload<AppInfo> payload = null;
try {
payload = JwtUtils.getInfoFromToken(appToken, jwtProps.getPublicKey(), AppInfo.class);
} catch (Exception e) {
//拒绝访问
return false;
}
//3.取出token的目标服务列表
AppInfo appInfo = payload.getInfo();
List<String> targetList = appInfo.getTargetList();
//4.判断当前服务是否在目标服务列表中
if(!targetList.contains(jwtProps.getApp().getServiceName())){
//拒绝访问
return false;
}
//放行
return true;
}
}
package com.leyou.search.config;
import com.leyou.search.interceptor.AppTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 把拦截器放入环境中
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private AppTokenInterceptor appTokenInterceptor;
/**
* 用于添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* addPathPatterns(): 给拦截器添加拦截路径 (默认值:/ 或 /**)
* excludePathPatterns(): 给拦截器添加放行路径
*/
registry.addInterceptor(appTokenInterceptor);
}
}
ly:
jwt:
pubKeyPath: D:\studyProject\rsa\ras-key.pub # 公钥地址
cookie:
cookieName: LY_TOKEN # cookie名称
app:
serviceName: item-service
secret: item-service
package com.leyou.item.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
/**
* 读取Jwt相关配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
private String pubKeyPath;//公钥路径
private PublicKey publicKey;//公钥
private CookiePojo cookie = new CookiePojo();
private AppTokenPojo app = new AppTokenPojo();
@Data
public class CookiePojo{
private String cookieName;
}
@Data
public class AppTokenPojo{
private String serviceName;
private String secret;
}
/**
* 读取公钥
*/
@PostConstruct
public void initMethod() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
package com.leyou.item.interceptor;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.leyou.common.auth.pojo.AppInfo;
import com.leyou.common.auth.pojo.Payload;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.common.constants.LyConstants;
import com.leyou.item.config.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 应用token拦截器
*/
@Component
@Slf4j
public class AppTokenInterceptor implements HandlerInterceptor{
@Autowired
private JwtProperties jwtProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//1.获取请求的应用token
String token = request.getHeader(LyConstants.APP_TOKEN_HEADER);
//2.如果没有,则阻止访问
if(StringUtils.isEmpty(token)){
return false;
}
//3.取出合法目标服务列表
try{
Payload<AppInfo> payload = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey(), AppInfo.class);
AppInfo appInfo = payload.getUserInfo();
List<String> targetList = appInfo.getTargetList();
if(CollectionUtils.isEmpty(targetList) || !targetList.contains(jwtProperties.getApp().getServiceName())){
log.error("【服务鉴权】- 认证失败");
return false;
}
//4.判断当前服务是否在目标服务列表中,如果在,则放行;不在,也是阻止访问
}catch (Exception e){
log.error("【服务鉴权】- 认证失败");
return false;
}
return true;
}
}
package com.leyou.item.config;
import com.leyou.item.interceptor.AppTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private AppTokenInterceptor appTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(appTokenInterceptor);
}
}
使用postman直接访问搜索微服务的搜索方法
看到返回值为空,代表无法访问
使用postman直接访问商品微服务的方法查询分类方法
看到返回值为空,代表无法访问
使用postman通过网关访问搜索微服务的搜索方法
可以看到结果:
需求描述:
在需求描述中,不管用户是否登录,都需要实现加入购物车功能,那么已登录和未登录下,购物车数据应该存放在哪里呢?
未登录购物车
用户如果未登录,将数据保存在服务端存在一些问题:
那么我们应该用把数据保存在客户端,这样每个用户保存自己的数据,就不存在身份识别的问题了,而且也解决了服务端数据存储压力问题。
无状态,就意味着,当前购物车没有所有者,如果用户从无状态切换到有状态,那么当前浏览器存储的购物车就属于这个用户了。
这里,无状态购物车我们选择存储在浏览器的本地存储中,购物车数据会被持久化到本地存储中。
已登录购物车
用户登录时,数据保存在哪里呢?
大家首先想到的应该是数据库,不过购物车数据比较特殊,读和写都比较频繁,存储数据库压力会比较大。因此我们可以考虑存入Redis
中。
不过大家可能会担心Redis存储空间问题,我们可以效仿淘宝,限制购物车最多只能添加99件商品,或者更少。
最后的分析结果:
用户可以在登录状态下将商品添加到购物车,购物车数据可以存储在
用户可以在未登录状态下将商品添加到购物车,购物车数据可以存储在
这幅图主要描述了两个功能:新增商品到购物车、查询购物车。
新增商品:
无论哪种新增,完成后都需要查询购物车列表:
我们点击某个商品进入详情页的时候发现出错,之前还可以的,为什么现在不行了?
原因是,商品详情的数据需要调用商品服务(ly-item)的数据,需要给商品详情服务授权访问商品服务才可以!
接下来,跟之前一样,在商品详情服务加入相关授权配置和代码即可
<dependency>
<groupId>com.leyougroupId>
<artifactId>ly-client-authartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
/**
* 商品详情微服务
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableScheduling //开启定时任务
public class LyPageApplication {
public static void main(String[] args) {
SpringApplication.run(LyPageApplication.class,args);
}
}
ly:
static:
itemDir: D:\leyou_projects\bk_project\soft\nginx-1.16.0\html\item\ #静态页服务器地址
itemTemplate: item #模板名称
jwt:
pubKeyPath: D:\studyProject\rsa\ras-key.pub # 公钥地址
cookie:
cookieName: LY_TOKEN # cookie名称
app:
serviceName: page-service
secret: page-service
package com.leyou.page.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
/**
* 读取Jwt相关配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
private String pubKeyPath;//公钥路径
private PublicKey publicKey;//公钥
private CookiePojo cookie = new CookiePojo();
private AppTokenPojo app = new AppTokenPojo();
@Data
public class CookiePojo{
private String cookieName;
}
@Data
public class AppTokenPojo{
private String serviceName;
private String secret;
}
/**
* 读取公钥
*/
@PostConstruct
public void initMethod() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
package com.leyou.page.scheduler;
import com.leyou.auth.client.AuthClient;
import com.leyou.page.config.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时申请服务token任务
*/
@Component
@Slf4j
public class AppTokenScheduler {
@Autowired
private AuthClient authClient;
@Autowired
private JwtProperties jwtProps;
//添加一个成员变量,用于服务token的共享
private String appToken;
/**
* 给外部获取服务token
* @return
*/
public String getAppToken(){
return this.appToken;
}
/**
* token刷新间隔
*/
private static final long TOKEN_REFRESH_INTERVAL = 86400000L; //24小时
/**
* token获取失败后重试的间隔
*/
private static final long TOKEN_RETRY_INTERVAL = 10000L;
/**
* 每隔1天调用1次
*/
@Scheduled(fixedRate = TOKEN_REFRESH_INTERVAL)
public void appToken(){
while (true) {
try {
//远程调用授权中心,申请服务token
String appToken = authClient.authorization(
jwtProps.getApp().getServiceName(),
jwtProps.getApp().getSecret()
);
//给成员变量赋值
this.appToken = appToken;
log.error("【申请服务证书】"+jwtProps.getApp().getServiceName()+"申请成功!");
//如果调用成功,则退出循环
break;
} catch (Exception e) {
e.printStackTrace();
//如果调用失败,等待10秒后重试
log.error("【申请服务证书】"+jwtProps.getApp().getServiceName()+"申请失败,10秒后重试");
try {
Thread.sleep(TOKEN_RETRY_INTERVAL);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
}
package com.leyou.page.feign;
import com.leyou.common.constants.LyConstants;
import com.leyou.page.scheduler.AppTokenScheduler;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Feign的拦截器
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
@Autowired
private AppTokenScheduler appTokenScheduler;
/**
* 拦截的业务逻辑
* @param template
*/
@Override
public void apply(RequestTemplate template) {
//在请求头中加入服务token
template.header(LyConstants.APP_TOKEN_HEADER,appTokenScheduler.getAppToken());
}
}
首先分析一下未登录购物车的数据结构。
我们看下页面展示需要什么数据:
因此每一个购物车信息,都是一个对象,包含:
{
skuId:2131241,
title:"小米6",
image:"",
price:190000,
num:1,
ownSpec:"{"机身颜色":"陶瓷黑尊享版","内存":"6GB","机身存储":"128GB"}"
}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[
{...},{...},{...}
]
知道了数据结构,下一个问题,就是如何保存购物车数据。前面我们分析过,可以使用Localstorage来实现。Localstorage是web本地存储的一种,那么,什么是web本地存储呢?
web本地存储主要有两种方式:
语法非常简单:
localStorage.setItem("key","value"); // 存储数据
localStorage.getItem("key"); // 获取数据
localStorage.removeItem("key"); // 删除数据
注意:localStorage和SessionStorage都只能保存字符串。
不过,在我们的common.js中,已经对localStorage进行了简单的封装:
示例:
添加购物车需要知道购物的数量,所以我们需要获取数量大小。我们在Vue中定义num,保存数量:
然后将num与页面的input框绑定,同时给+
和-
的按钮绑定事件:
编写方法:
现在点击加入购物车会跳转到购物车成功页面。
不过我们不这么做,我们绑定点击事件,然后实现添加购物车功能。
addCart方法中判断用户的登录状态:
addCart(){
ly.http.get("/auth/verify").then(res=>{
// 已登录发送信息到后台,保存到redis中
}).catch(()=>{
// 未登录保存在浏览器本地的localStorage中
})
}
<script>
var itemVm = new Vue({
el:"#itemApp",
data:{
ly,
specialSpecJson,
paramsMap,
indexes,
skus,
num:1,//购买商品的数量
},
//计算属性
computed:{
//定义一个selectedSku对象,用于存储当前选择的Sku
selectedSku(){
/**
let curSku = {};
//1)获取当前选择的每个参数下标,转换为0_1_2格式
//Object.values():取出指定对象的所有制
let curIndexes = Object.values(this.indexes).join("_"); //格式: 0_1_2
//2)遍历所有skus,判断他们的indexes属性是否一致,如果一致,则取出当前Sku对象
this.skus.forEach(sku=>{
if(sku.indexes==curIndexes){
curSku = sku;
}
})
return curSku;
*/
let curIndexes = Object.values(this.indexes).join("_"); //格式: 0_1_2
//find: 该方法是数组的方法,用于在数组中根据条件来查询指定元素,返回符合条件的元素
return this.skus.find(sku=>sku.indexes==curIndexes);
},
//定义一个images属性,用于来存储当前选中的Sku的所有图片
images(){
return this.selectedSku.images.split(",") || [];
},
curOwnSpec(){
}
},
methods:{
//添加数量
increment(){
this.num++;
},
//减少数量
decrement(){
if(this.num>1){
this.num--;
}
},
//添加购物车
addCart(){
//判断用户是否登录
ly.http.get('/auth/verify').then(resp=>{
//已登录
}).catch(e=>{
//未登录
//1.取出本地购物车数据
let carts = ly.store.get('LY_CART') || [];
//2.判断本地购物车列表中是否存在当前购买的商品
let cart = carts.find(cart=>cart.skuId==this.selectedSku.id);
if(cart){
//3.如果存在,则修改数量即可
cart.num += this.num;
}else{
//4.如果不存在,添加该商品到本地购物车
carts.push({
skuId:this.selectedSku.id,
title:this.selectedSku.title,
image:this.images[0],
price:this.selectedSku.price,
num:this.num,
ownSpec:this.selectedSku.ownSpec,
});
}
//把更新后的购物车数据保存到本地缓存中
ly.store.set('LY_CART',carts);
//跳转到购物车列表页面
window.location.href="http://www.leyou.com/cart.html";
});
},
},
components:{
lyTop: () => import('/js/pages/top.js')
}
});
</script>
结果:
添加完成后,页面会跳转到购物车结算页面:cart.html
页面加载时,就应该去查询购物车。
<script type="text/javascript">
var cartVm = new Vue({
el: "#cartApp",
data: {
ly,
carts:[],//存储所有购物车
},
created(){
this.loadCarts();
},
methods:{
//判断用户是否登录方法
verifyUser(){
return ly.http.get('/auth/verify');
},
//查询购物车
loadCarts(){
this.verifyUser().then(resp=>{
//已经登录
}).catch(e=>{
//未登录
//1.取出本地localStorage数据
let carts = ly.store.get('LY_CART') || [];
//2.赋值给data
this.carts = carts;
})
},
},
components: {
shortcut: () => import("/js/pages/shortcut.js")
}
})
</script>
刷新页面,查看控制台Vue实例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDPn8tMD-1660816274782)(assets/1600138808551.png)]
接下来,我们在页面中展示carts的数据:
<div class="cart-item-list">
<div class="cart-body">
<div class="cart-list">
<ul class="goods-list yui3-g" v-for="(cart,index) in carts" :key="index">
<li class="yui3-u-1-24">
<input type="checkbox" name="" value="" />
li>
<li class="yui3-u-11-24">
<div class="good-item">
<div class="item-img"><img :src="cart.image" width="80px" height="80px"/>div>
<div class="item-msg">
<span style="line-height:70px ">
{{cart.title}}
<span v-for="(v,k,i) in JSON.parse(cart.ownSpec)" :key="i">
{{k}}:{{v}}
span>
span>
div>
div>
li>
<li class="yui3-u-1-8"><span style="line-height:70px " class="price">{{ly.formatPrice(cart.price)}}span>li>
<li class="yui3-u-1-8" style="padding-top: 20px">
<a href="javascript:void(0)" class="increment mins">-a>
<input autocomplete="off" type="text" v-model="cart.num" minnum="1" class="itxt" />
<a href="javascript:void(0)" class="increment plus">+a>
li>
<li class="yui3-u-1-8"><span style="line-height:70px " class="sum">{{ly.formatPrice(cart.price*cart.num)}}span>li>
<li class="yui3-u-1-8">
<a href="#none">删除a><br />
<a href="#none">移到我的关注a>
li>
ul>
div>
div>
div>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iT8srPcq-1660816274782)(assets/1600139722488.png)]
要注意,价格的展示需要进行格式化,这里使用的是我们在common.js中定义的formatPrice方法
效果:
我们给页面的 +
和 -
绑定点击事件,修改num 的值:
两个事件:
//添加数量
increment(cart){
//添加数量
cart.num++;
//判断用户是否登录
ly.http.get('/auth/verify').then(resp=>{
//已经登录
}).catch(e=>{
//未登录
//把更新的数量保存到LocalStorage
ly.store.set('LY_CART',this.carts);
})
},
//减少数量
decrement(cart){
if(cart.num<=1){
return;
}
//减少数量
cart.num--;
//判断用户是否登录
ly.http.get('/auth/verify').then(resp=>{
//已经登录
}).catch(e=>{
//未登录
//把更新的数量保存到LocalStorage
ly.store.set('LY_CART',this.carts);
})
}
给删除按钮绑定事件:
点击事件中删除商品:
//删除购物车的商品
delCart(index){
ly.http.get('/auth/verify').then(resp=>{
//已登录
}).catch(e=>{
//未登录
//从carts中删除指定index的元素
/**
* splice(): 从数组中删除指定元素
* 参数一:删除元素的起始下标
* 参数二:删除的元素个数
*
* 例如: [28,54,53,21] 1,2 ->[28,21]
*/
this.carts.splice(index,1);
//写回本地缓存
ly.store.set('LY_CART',this.carts);
});
}