• Spring Security OAuth2.0认证授权四:分布式系统认证授权


    Spring Security OAuth2.0认证授权系列文章

    Spring Security OAuth2.0认证授权一:框架搭建和认证测试
    Spring Security OAuth2.0认证授权二:搭建资源服务
    Spring Security OAuth2.0认证授权三:使用JWT令牌

    前面几篇文章讲解了如何从头开始搭建认证服务和资源服务,从颁发普通令牌到颁发jwt令牌,最终完成了jwt令牌的颁发和校验。本篇文章将会讲解分布式环境下如何进行认证和授权。

    一、设计思路

    分布式授权图.png

    一般来说,一个典型的分布式系统架构如上图所示,这里进行一个简单的设计,来完成分布式系统下的认证和授权。

    整体设计思路是使用OAuth2.0颁发令牌,使用JWT对令牌签名并颁发JWT令牌给客户端。既然决定使用JWT令牌了,则不需要再调用认证服务器对令牌进行验证了,因为JWT本身就包含了所需要的信息,而且只要验签成功,则可认为令牌可信任且有效。

    如上所述,则可以如此设计:

    1. 用户请求登陆之后认证服务颁发令牌给用户,浏览器将令牌储存下来。
    2. 浏览器请求资源的的时候携带着令牌,网关拦截请求对令牌验证,验证的方法很简单,不请求认证服务而是直接使用密钥(对称或非对称)验签,只要验证成功则将jwt payload中的信息解析成明文放到请求头中转发请求到资源服务。
    3. 资源服务拿到明文信息,根据明文信息中的权限信息验证是否有权限访问该资源,有权限则返回资源信息,无权限则返回401。

    综上,整体思路就是网关认证,资源服务鉴权。

    典型的微服务架构下会有注册中心、网关等服务,接下来会依次介绍和搭建相关服务。

    二、注册中心搭建

    为了方便程序本地调试方便,这里使用eureka server作为服务注册中心,使用起来也非常简单

    1.添加maven依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
    

    2.新建启动类

    @SpringBootApplication
    @EnableEurekaServer
    public class RegisterServer {
        public static void main(String[] args) {
            SpringApplication.run(RegisterServer.class,args);
        }
    }
    

    3.新建配置文件

    spring:
      application:
        name: register-server
    
    server:
      port: 8765 #启动端口
    
    eureka:
      server:
        enable-self-preservation: false    #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
        eviction-interval-timer-in-ms: 10000 #清理间隔(单位毫秒,默认是60*1000)5秒将客户端剔除的服务在服务注册列表中剔除#
        shouldUseReadOnlyResponseCache: true #eureka是CAP理论种基于AP策略,为了保证强一致性关闭此切换CP 默认不关闭 false关闭
      client:
        register-with-eureka: false  #false:不作为一个客户端注册到注册中心
        fetch-registry: false      #为true时,可以启动,但报异常:Cannot execute request on any known server
        instance-info-replication-interval-seconds: 10
        serviceUrl:
          defaultZone: http://localhost:${server.port}/eureka/
      instance:
        hostname: ${spring.cloud.client.ip-address}
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
    

    然后启动启动类,访问浏览器,http://127.0.0.1:8765,出现如下页面即表示已经成功

    eureka server.png

    二、网关搭建

    这里选用spring cloud gateway作为网关(不是zuul)

    1.添加maven依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--gateway 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <!--actuator 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- jwt依赖 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
    </dependencies>
    

    2.新建启动类

    @SpringBootApplication
    public class GatewayServer {
    
        public static void main(String[] args) {
            SpringApplication.run(GatewayServer.class, args);
        }
    }
    

    3.新建配置文件

    server:
      port: 8761
    spring:
      cloud:
        gateway:
          routes:
            - id: resource_server
              uri: "lb://resource-server"
              predicates:
                - Path=/r**
      application:
        name: gateway-server
    
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8765/eureka
      instance:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${spring.cloud.client.ip‐address}:${spring.application.instance_id:${server.port}}
    

    如此,一个网关就已经搭建好了,但是还不具备我们想要的认证功能。

    4.添加token全局过滤器

    知识点有以下几点:

    • 全局过滤器要实现GlobalFilter接口
    • 为了实现token过滤器最先被调用,要实现Order接口并将优先级调到最大
    • 使用JwtHelper工具类对jwt验签,签名的key必须和认证中心中配置的key保持一致
    • 验签成功后将jwt中payload明文信息放到token-info的header值中传递给目标服务

    实现代码如下:

    @Component
    @Slf4j
    public class TokenFilter implements GlobalFilter, Ordered {
        private static final String BEAR_HEADER = "Bearer ";
        /**
         * 该值要和auth-server中配置的签名相同
         *
         * com.kdyzm.spring.security.auth.center.config.TokenConfig#SIGNING_KEY
         */
        private static final String SIGNING_KEY = "auth123";
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            //如果没有token,则直接返回401
            if(StringUtils.isEmpty(token)){
                return unAuthorized(exchange);
            }
            //验签并获取PayLoad
            String payLoad;
            try {
                Jwt jwt = JwtHelper.decodeAndVerify(token.replace(BEAR_HEADER,""), new MacSigner(SIGNING_KEY));
                payLoad = jwt.getClaims();
            } catch (Exception e) {
                log.error("验签失败",e);
                return unAuthorized(exchange);
            }
            //将PayLoad数据放到header
            ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
            builder.header("token-info", payLoad).build();
            //继续执行
            return chain.filter(exchange.mutate().request(builder.build()).build());
        }
    
        private Mono<Void> unAuthorized(ServerWebExchange exchange){
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
    
        /**
         * 将该过滤器的优先级设置为最高,因为只要认证不通过,就不能做任何事情
         *
         * @return
         */
        @Override
        public int getOrder() {
            return Ordered.HIGHEST_PRECEDENCE;
        }
    }
    

    三、资源服务修改

    原来资源服务已经集成了OAuth2.0、Spring Security、JWT等组件,根据现在的设计方案,需要删除OAuth2.0和JWT组件,只留下Spring Security组件。

    1.移除OAuth2.0、JWT组件

    这里要删除maven依赖,同时将相关配置删除

    第一步,删除maven依赖,直接将以下两个依赖移除就好

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

    第二步,删除相关配置

    将ResouceServerConfig、TokenConfig两个类直接删除 即可。

    2.添加过滤器

    这里需要使用过滤器做,首先写一个过滤器,实现OncePerRequestFilter接口,该过滤器的作用就是获取网关传过来的token-info明文数据,封装成JwtTokenInfo对象,并将该相关信息添加到SpringSecurity上下文以备之后的鉴权使用。

    代码实现如下:

    @Component
    @Slf4j
    public class AuthFilterCustom extends OncePerRequestFilter {
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            ObjectMapper objectMapper = new ObjectMapper();
            String tokenInfo=request.getHeader("token-info");
            if(StringUtils.isEmpty(tokenInfo)){
                log.info("未找到token信息");
                filterChain.doFilter(request,response);
                return;
            }
            JwtTokenInfo jwtTokenInfo = objectMapper.readValue(tokenInfo, JwtTokenInfo.class);
            log.info("tokenInfo={}",objectMapper.writeValueAsString(jwtTokenInfo));
            List<String> authorities1 = jwtTokenInfo.getAuthorities();
            String[] authorities=new String[authorities1.size()];
            authorities1.toArray(authorities);
            //将用户信息和权限填充 到用户身份token对象中
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(jwtTokenInfo.getUser_name(),null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            //将authenticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            filterChain.doFilter(request,response);
        }
    }
    

    3.将过滤器注册到过滤器链

    修改WebSecurityConfig类,使用如下方法注册过滤器:

    .addFilterAfter(authFilterCustom, BasicAuthenticationFilter.class)//添加过滤器
    

    同时,一定要关闭session功能,否则会出现上下文缓存问题

    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session
    

    完整代码如下:

        @Autowired
        private AuthFilterCustom authFilterCustom;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf()
                    .disable()
                    .authorizeRequests()
    //                .antMatchers("/r/r1").hasAuthority("p2")
    //                .antMatchers("/r/r2").hasAuthority("p2")
                    .antMatchers("/**").authenticated()//所有的请求必须认证通过
                    .anyRequest().permitAll()//其它所有请求都可以随意访问
                    .and()
                    .addFilterAfter(authFilterCustom, BasicAuthenticationFilter.class)//添加过滤器
            .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session
    
        }
    

    四、其他注意事项

    认证服务auth-server以及资源服务resource-server、网关服务gateway-server都要集成eureka client组件

    五、测试

    测试前需要将各个服务依次启动起来:

    第一步,获取token

    这里使用password模式直接获取token,POST请求如下接口:

    http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

    即可获取token。

    第二步,访问资源

    通过网关请求资源服务的r1接口,GET请求如下接口:

    http://127.0.0.1:8761/r1

    需要带上Header,key为Authorization,value格式如下:

    Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE2MTAzNzI5MzUsImF1dGhvcml0aWVzIjpbInAxIiwicDIiXSwianRpIjoiOWQzMzRmZGMtOTcwZC00YmJkLWI2MmMtZDU4MDZkNTgzM2YwIiwiY2xpZW50X2lkIjoiYzEifQ.gZraRNeX-o_jKiH7XQgg3TlUQBpxUcXa2-qR_Treu8U
    

    如果相应结果如下,则表示测试通过

    访问资源r1
    

    否则,会返回401状态码。

    六、项目源代码

    项目源代码:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0

    我的博客原文地址:https://blog.kdyzm.cn/post/30

  • 相关阅读:
    OAuth2在微服务架构中的应用
    使用SpringSecurity体验OAuth2 (入门2)
    SpringSecurity的配置分析
    SpringSecurity在Springboot下使用的初步体验
    Spring框架中的org.springframework.context.annotation.Import注解类
    使用SpringSecurity体验OAUTH2之一 (入门1)
    5. SpringBoot —— Actuator简介
    无题
    C# 委托的本质
    json 字符串 反序列化
  • 原文地址:https://www.cnblogs.com/kuangdaoyizhimei/p/14275559.html
Copyright © 2020-2023  润新知