zoukankan      html  css  js  c++  java
  • 畅购商城(十):购物车

    好好学习,天天向上

    本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

    OAuth2.0对接用户微服务

    上一篇文章中提到过,访问资源服务的时候,需要携带令牌去进行权限校验。那么用户微服务也是资源服务,所以需要对其进行配置,首先添加OAuth2.0的依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    

    然后把之前导出的public.key放到用户微服务的resources目录下。最后添加一个配置类即可,配置类上需要添加@EnableResourceServer注解,然后继承自ResourceServerConfigurerAdapter

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        private static final String PUBLIC_KEY = "public.key";//公钥
    
        /***
         * 定义JwtTokenStore
         * @param jwtAccessTokenConverter
         * @return
         */
        @Bean
        public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        /***
         * 定义JJwtAccessTokenConverter
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setVerifierKey(getPublicKey());
            return converter;
        }
        /**
         * 获取非对称加密公钥 Key
         * @return 公钥 Key
         */
        private String getPublicKey() {
            Resource resource = new ClassPathResource(PUBLIC_KEY);
            try {
                InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
                BufferedReader br = new BufferedReader(inputStreamReader);
                return br.lines().collect(Collectors.joining("
    "));
            } catch (IOException ioe) {
                return null;
            }
        }
    
        /***
         * Http安全配置,对每个到达系统的http请求链接进行校验
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //所有请求必须认证通过
            http.authorizeRequests()
                    //下边的路径放行
                    .antMatchers("/user/add,user/load/*").permitAll() //配置 /user/add /user/load/*不需要权限
                    .anyRequest().authenticated();    //其他地址需要认证授权
        }
    
    }
    

    这样配置类就配置好了,在添加了@EnableGlobalMethodSecurity注解后,就可以在指定的方法上面添加注解来进行权限控制。

    比如:

    @PreAuthorize("hasAnyAuthority('admin')")	//表示该方法只有admin才能访问
    @PutMapping(value = "/{id}")
    public Result update(@RequestBody User user, @PathVariable String id) {
    ………………
    

    这样用户微服务就配置好了。

    网关配置

    访问微服务的JWT令牌是放在请求头中一个叫“Authorization”的参数中。以“bearer”开头。因为请求是通过网关转发给相应的微服务,所以可以对网关进行配置。之前在网关微服务中写了一个过滤器叫AuthorizeFilter,里面的filter()方法写的是分别从请求头、参数、Cookie中获取token信息,然后进行校验。现在稍微修改一下,现在不校验了,只判断有无token信息并进行简单处理。

    还有一点就是有些请求比如登录注册等不需要token的就直接放行。

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    	…………
    	if (needlessToken(request.getURI().toString())) {
    		return chain.filter(exchange);	//如果是不需要token的请求就直接放行
        }
        //还是没有Token就拦截
        if (StringUtils.isEmpty(token)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        } else {
            if (!hasTokenInHeader) {
                if (!(token.startsWith("brarer ") || token.startsWith("Bearer "))) {
                    token = "Bearer " + token;
                }
                request.mutate().header("Authorization",token);
            }
        }
        //Token不为空就校验Token
        // try {
        //     JwtUtil.parseJWT(token);
        // } catch (Exception e) {
        //     //报异常说明Token是错误的,拦截
        //     response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //     return response.setComplete();
        // }
        return chain.filter(exchange);
    }
    
    //判断指定的uri是否不需要token就可以访问,true表示不需要
    public boolean needlessToken(String uri) {
        String[] uris = new String[]{
                "/api/user/add",
                "/api/user/login"
        };
        for (String s : uris) {
            if (s.equals(uri)) {
                return true;
            }
        }
        return false;
    }
    

    代码的意思就是如果请求头中有token的信息就不去管它,如果token在参数或者Cookie中,就看是不是以“**Bearer **”开头,不是的话就添加“Bearer ”,然后存入请求头中。如果是指定的不需要token的请求就直接放行。为什么现在不校验呢?因为校验的工作交给对应的微服务去处理了,网关不负责。

    OAuth2.0从数据库加载数据

    在之前的配置中,客户端的信息是配置在内存中的。用户也是在内存中指定的。现在来改造一下,从数据库中加载数据。首先是客户端信息,修改AuthorizationServerConfig中的configure()方法。

    // 客户端信息配置
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).clients(clientDetails());
    }
    //客户端配置
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }
    

    我们给客户端配置了一个JdbcClientDetailsService

    然后就可以从oauth_client_details中加载数据了。

    咦?好像没有在任何地方指定这张表呀,怎么就能从这张表里面加载数据???前面不是配了个JdbcClientDetailsService么。点进去看看

    哦~ 原来是在JdbcClientDetailsService的内部指定好了呀。


    现在就是从数据库中加载了客户端的信息,那怎么加载用户的信息呢?这里使用Feign接口去调用用户微服务查询出用户的信息。

    //用户微服务中的UserController
    @GetMapping({"/{id}","/load/{id}"})
    public Result<User> findById(@PathVariable String id) {
        //调用UserService实现根据主键查询User
        User user = userService.findById(id);
        return new Result<User>(true, StatusCode.OK, "查询成功", user);
    }
    

    我们给这个方法配置一个“load/{id}”的路径。之前已经配置过这个路径放行了。

    然后在用户的api微服务中添加一个Feign接口。

    @FeignClient("user")
    @RequestMapping("/user")
    public interface UserFeign {
    
        //根据ID查询User数据
        @GetMapping({"/load/{id}"})
        Result<User> findById(@PathVariable String id);
    
    }
    

    这样在认证微服务的UserDetailsServiceImplloadUserByUsername()方法中就可以调用Feign去数据库中查询用户信息了。

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /*客户端信息认证*/
        //取出身份,如果身份为空说明没有认证
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
        if(authentication==null){
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if(clientDetails!=null){
                //秘钥
                String clientSecret = clientDetails.getClientSecret();
                //数据库查找方式
                return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }
    
        /*用户信息认证*/
        if (StringUtils.isEmpty(username)) {
            return null;
        }
        com.robod.user.pojo.User user = userFeign.findById(username).getData();	//查询用户
        if (user == null ) {
            return null;
        }
        //根据用户名查询用户信息
        String pwd = user.getPassword();
        //创建User对象
        String permissions = "goods_list,seckill_list";
        UserJwt userDetails = new UserJwt(username,pwd,
                                           AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
        return userDetails;
    }
    

    这样就OK了,来测试一下。

    这个robod用户是保存在数据库中的,可以正常登录,说明我们的配置是没有问题的。

    微服务之间的令牌传递

    在实现购物车功能之前,要先明确一个问题,只有登录过的用户才可以访问自己的购物车。所以我们在访问微服务的时候必须要携带令牌,但是还涉及到微服务之间的Feign接口调用,令牌该怎么传递过去呢?令牌不是放在名为“Authorization”的请求头中吗,所以加一个过滤器在Feign调用前调用,将当前请求的请求头信息封装到Feign请求的请求头中。但是呢,这个过滤器是很多微服务共用的,所以可以将这个过滤器放在common工程中,哪个微服务需要就直接注入到Spring容器中。

    public class FeignHeaderInterceptor implements RequestInterceptor {
    
        //Feign调用前调用
        @Override
        public void apply(RequestTemplate template) {
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (requestAttributes != null) {
                HttpServletRequest request = requestAttributes.getRequest();
                Enumeration<String> headerNames = request.getHeaderNames();//所有请求头的名字集合
                if (headerNames != null) {
                    while (headerNames.hasMoreElements()) {
                        String headerName = headerNames.nextElement();
                        String headerValue = request.getHeader(headerName);
                        template.header(headerName,headerValue);
                    }
                }
            }
        }
    }
    

    如果哪个微服务想要调用的话,就直接在启动类中注入即可。

    @Bean
    public FeignHeaderInterceptor feignHeaderInterceptor() {
        return new FeignHeaderInterceptor();
    }
    

    这样就可以实现令牌在不同微服务之间的传递。

    购物车

    现在就可以来实现购物车的功能了,因为购物车毕竟是用来下单的,所以就将购物车功能写在订单微服务中。创建一个订单微服务changgou-service-order以及一个订单的api工程changgou-service-order-api。

    从令牌中获取用户名

    添加到购物车的流程就是用户从前端将商品的id和数量以及令牌传过来。然后解析令牌,拿到用户名。然后拿着商品的id用Feign调用Goods微服务将Sku和Spu查询出来,然后封装成一个OrderItem对象,存入Reids中。这个时候Feign接口的调用就涉及到微服务之间的令牌传递问题了,把FeignHeaderInterceptor注入即可。

    这里还有一个问题,就是令牌解析,很简单,使用公钥解析即可,封装一个工具类com.robod.order.utils.TokenDecodeUtil

    @Component
    public class TokenDecodeUtil {
    
        //公钥路径
        private static final String PUBLIC_KEY_PATH = "public.key";
    
        //公钥的内容
        private static String publicKey="";
    
        /***
         * 获取用户信息
         * @return
         */
        public Map<String,String> getUserInfo() {
            //获取授权信息
            OAuth2AuthenticationDetails authentication = 
                (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
            //令牌解码
            return decodeToken(authentication.getTokenValue());
        }
    
        /***
         * 读取令牌数据
         */
        public Map<String,String> decodeToken(String token){
            //校验Jwt
            Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(getPubKey()));
    
            //获取Jwt原始内容
            String claims = jwt.getClaims();
            return JSON.parseObject(claims,Map.class);
        }
    
        /**
         * 获取非对称加密公钥 Key
         * @return 公钥 Key
         */
        public String getPubKey() {
            if(!StringUtils.isEmpty(publicKey)){
                return publicKey;
            }
            Resource resource = new ClassPathResource(PUBLIC_KEY_PATH);
            try {
                InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
                BufferedReader br = new BufferedReader(inputStreamReader);
                publicKey = br.lines().collect(Collectors.joining("
    "));
                return publicKey;
            } catch (IOException ioe) {
                return null;
            }
    
        }
    
    }
    

    准备工作做好以后就可以编写相应的逻辑了。

    添加到购物车

    //     CartController
    @GetMapping("/add")
    public Result add(long id,int num) {
        String username = tokenDecodeUtil.getUserInfo().get("username");
        cartService.add(id,num,username);
        return new Result(true, StatusCode.OK,"成功添加到购物车");
    }
    //-----------------------------------------------------------------------
    //     CartServiceImpl
    @Override
    public void add(long id, int num, String username) {
        BoundHashOperations boundHashOperations = redisTemplate.boundHashOps("Cart_" + username);
        if (num <= 0){
            boundHashOperations.delete(id);
            Long size = boundHashOperations.size();
            if (size == null || size<=0) {
                redisTemplate.delete("Cart_" + username);
            }
            return;
        }
        Sku sku = skuFeign.findById(id).getData();
        if (sku == null) {
            throw new RuntimeException("未查询到商品信息");
        }
        Spu spu = spuFeign.findById(sku.getSpuId()).getData();
        if (spu == null) {
            throw new RuntimeException("数据库中数据异常");
        }
        OrderItem orderItem = createOrderItem(spu,sku,num);
        boundHashOperations.put(id,orderItem);
    }
    
    private OrderItem createOrderItem(Spu spu,Sku sku,int num) {
        OrderItem orderItem = new OrderItem();
        orderItem.setCategoryId1(spu.getCategory1Id());
        orderItem.setCategoryId2(spu.getCategory2Id());
        orderItem.setCategoryId3(spu.getCategory3Id());
        orderItem.setSpuId(spu.getId());
        orderItem.setSkuId(sku.getId());
        orderItem.setName(sku.getName());
        orderItem.setNum(num);
        orderItem.setPrice(sku.getPrice());
        orderItem.setMoney(num * sku.getPrice());
        orderItem.setImage(spu.getImage());
        return orderItem;
    }
    

    但是现在访问的话还存在问题,FeignHeaderInterceptor里面ServletRequestAttributes的数据

    这是因为现在的feign的隔离策略是THREAD(线程池隔离),这时候使用Feign的时候是单独开启一个线程的,不是之前的线程,所以获取不到数据。要想使用同一个线程去使用feign,可以把隔离策略设置成SEMAPHORE(信号量隔离)。在order微服务的配置文件中添加一段配置

    #hystrix 配置
    hystrix:
      command:
        default:
          execution:
            isolation:
              thread:
                timeoutInMilliseconds: 10000
              strategy: SEMAPHORE
    

    这两种隔离策略的区别是

    线程池隔离 信号量隔离
    线程 与调用线程非相同线程 与调用线程相同(jetty线程)
    开销 排队、调度、上下文开销等 无线程切换,开销低
    异步 支持 不支持
    并发支持 支持(最大线程池大小) 支持(最大信号量上限)

    这样就可以正常地添加数据到购物车了。

    查询购物车

    查询购物车的功能很简单,前端携带着令牌向服务器发送请求,Controller调用相应的方法解析令牌拿到用户名,然后调用Service层从Redis中获取到对应的数据。

    //	CartController
    @GetMapping("/list")
    public Result<List<OrderItem>> list() {
        String username = tokenDecodeUtil.getUserInfo().get("username");
        List<OrderItem> orderItems = cartService.list(username);
        return new Result<>(true,StatusCode.OK,"查询购物车成功",orderItems);
    }
    //--------------------------------------------------------------------------
    //     CartServiceImpl
    @Override
    public List<OrderItem> list(String username) {
        return (List<OrderItem>) redisTemplate.boundHashOps("Cart_" + username).values();
    }
    

    权限控制

    虽然购物车的功能已经实现了,但是还存在一个问题,就是没有对用户进行权限校验,我们可以去限制某个方法只能由拥有特定权限的用户去访问,要是不进行权限控制的话,任何人都可以访问就会很不安全。这里我准备对购物车用到的四个方法添加“USER”权限的控制。

    首先为订单微服务和商品微服务添加OAuth2.0的依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    

    然后将之前提取出来的public.key文件添加到这两个微服务的resources目录下。最后添加资源服务的配置类

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        private static final String PUBLIC_KEY = "public.key";//公钥
    
        /***
         * 定义JwtTokenStore
         * @param jwtAccessTokenConverter
         * @return
         */
        @Bean
        public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        /***
         * 定义JJwtAccessTokenConverter
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setVerifierKey(getPublicKey());
            return converter;
        }
    
        /**
         * 获取非对称加密公钥 Key
         * @return 公钥 Key
         */
        private String getPublicKey() {
            Resource resource = new ClassPathResource(PUBLIC_KEY);
            try {
                InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
                BufferedReader br = new BufferedReader(inputStreamReader);
                return br.lines().collect(Collectors.joining("
    "));
            } catch (IOException ioe) {
                return null;
            }
        }
    
        /***
         * Http安全配置,对每个到达系统的http请求链接进行校验
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //所有请求必须认证通过
            http.authorizeRequests()
                    .anyRequest().permitAll();//.authenticated();    //其他地址需要认证授权
        }
    
    }
    

    这里限制所有的请求都必须通过验证。然后我们在相应的方法上添加注解即可。

    //  SkuController
    @GetMapping("/{id}")
    @PreAuthorize("hasAnyAuthority('USER')")
    public Result<Sku> findById(@PathVariable Long id){
    
    //  SpuController
    @GetMapping("/{id}")
    @PreAuthorize("hasAnyAuthority('USER')")
    public Result<Spu> findById(@PathVariable Long id){
        
    //  CartController
    @GetMapping("/add")
    @PreAuthorize("hasAnyAuthority('USER')")
    public Result add(long id,int num) {
    
    @GetMapping("/list")
    @PreAuthorize("hasAnyAuthority('USER')")
    public Result<List<OrderItem>> list() {
    

    这四个方法限制了只有拥有USER权限才可以访问,如果没有相应的权限请求就会被拒绝。

    订单微服务对接网关

    现在就差最后一步了,就是将订单微服务对接到网关,然后就可以通过网关将请求转发到订单微服务了。

    在网关微服务的配置文件中添加订单微服务的路由配置

    spring:
      cloud:
          routes:
            - id: changgou_order_route
              uri: http://localhost:18089
              predicates:
                -Path=/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,
                /api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,
                /api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**
              filters:
                - StripPrefix=1
    

    这样就OK了。

    总结

    原本的代码中,OAuth2.0是从内存中获取数据,文章的开头先是改了代码从数据库中获取数据。然后实现了购物车的功能并实现了权限控制。最后将订单微服务对接到了网关中,实现了通过网关去访问相应的微服务。

    如果我的文章对你有些帮助,不要忘了点赞收藏转发关注。要是有什么好的意见欢迎在下方留言。让我们下期再见!

  • 相关阅读:
    (转)深入理解JavaScript 模块模式
    (转)Javascript匿名函数的写法、传参、递归
    (转)javascript匿名函数的写法、传参和递归
    (转)初探Backbone
    (转)android平台phonegap框架实现原理
    (转)PhoneGap工作原理及需改进的地方
    (转)JQM 日期插件 mobiscroll Demo
    (转)jQuery Mobile 移动开发中的日期插件Mobiscroll 2.3 使用说明
    [题解] [笔记]期望&洛谷P3232
    [笔记] [题解] 状压$DP$&洛谷P1433
  • 原文地址:https://www.cnblogs.com/robod/p/13574083.html
Copyright © 2011-2022 走看看