相关推荐recommended
Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!
作者:mmseoamin日期:2024-04-01
  • micro-oauth2-gateway:网关服务,负责请求转发和鉴权功能,整合Spring Security+Oauth2;
  • micro-oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合Spring Security+Oauth2;
  • micro-oauth2-api:受保护的API服务,用户鉴权通过后可以访问该服务,不整合Spring Security+Oauth2。

    一、micro-oauth2-auth

    我们首先来搭建认证服务,它将作为Oauth2的认证服务使用,并且网关服务的鉴权功能也需要依赖它。

    1.配置
    • 在pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖;
      
          
              org.springframework.boot
              spring-boot-starter-web
          
          
              org.springframework.boot
              spring-boot-starter-security
          
          
              org.springframework.cloud
              spring-cloud-starter-oauth2
          
          
              com.nimbusds
              nimbus-jose-jwt
              8.16
          
          
          
              org.springframework.boot
              spring-boot-starter-data-redis
          
      

       

      • 在application.yml中添加相关配置,主要是Nacos和Redis相关配置;
        server:
          port: 9401
        spring:
          profiles:
            active: dev
          application:
            name: micro-oauth2-auth
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848
          jackson:
            date-format: yyyy-MM-dd HH:mm:ss
          redis:
            database: 0
            port: 6379
            host: localhost
            password: 
        management:
          endpoints:
            web:
              exposure:
                include: "*"
        • 使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可;

                          keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

          2.配置完成,开始代码环节
          • 创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息
            /**
             * 用户管理业务类
             * Created by macro on 2020/6/19.
             */
            @Service
            public class UserServiceImpl implements UserDetailsService {
                private List userList;
                @Autowired
                private PasswordEncoder passwordEncoder;
                @PostConstruct
                public void initData() {
                    String password = passwordEncoder.encode("123456");
                    userList = new ArrayList<>();
                    userList.add(new UserDTO(1L,"macro", password,1, CollUtil.toList("ADMIN")));
                    userList.add(new UserDTO(2L,"andy", password,1, CollUtil.toList("TEST")));
                }
                @Override
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                    List findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
                    if (CollUtil.isEmpty(findUserList)) {
                        throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
                    }
                    SecurityUser securityUser = new SecurityUser(findUserList.get(0));
                    if (!securityUser.isEnabled()) {
                        throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
                    } else if (!securityUser.isAccountNonLocked()) {
                        throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
                    } else if (!securityUser.isAccountNonExpired()) {
                        throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
                    } else if (!securityUser.isCredentialsNonExpired()) {
                        throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
                    }
                    return securityUser;
                }
            }
            • 添加认证服务相关配置Oauth2ServerConfig,需要配置加载用户信息的服务UserServiceImpl及RSA的钥匙对KeyPair;
              /**
               * 认证服务器配置
               * Created by macro on 2020/6/19.
               */
              @AllArgsConstructor
              @Configuration
              @EnableAuthorizationServer
              public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
                  private final PasswordEncoder passwordEncoder;
                  private final UserServiceImpl userDetailsService;
                  private final AuthenticationManager authenticationManager;
                  private final JwtTokenEnhancer jwtTokenEnhancer;
                  @Override
                  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                      clients.inMemory()
                              .withClient("client-app")
                              .secret(passwordEncoder.encode("123456"))
                              .scopes("all")
                              .authorizedGrantTypes("password", "refresh_token")
                              .accessTokenValiditySeconds(3600)
                              .refreshTokenValiditySeconds(86400);
                  }
                  @Override
                  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
                      TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
                      List delegates = new ArrayList<>();
                      delegates.add(jwtTokenEnhancer); 
                      delegates.add(accessTokenConverter());
                      enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器
                      endpoints.authenticationManager(authenticationManager)
                              .userDetailsService(userDetailsService) //配置加载用户信息的服务
                              .accessTokenConverter(accessTokenConverter())
                              .tokenEnhancer(enhancerChain);
                  }
                  @Override
                  public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
                      security.allowFormAuthenticationForClients();
                  }
                  @Bean
                  public JwtAccessTokenConverter accessTokenConverter() {
                      JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
                      jwtAccessTokenConverter.setKeyPair(keyPair());
                      return jwtAccessTokenConverter;
                  }
                  @Bean
                  public KeyPair keyPair() {
                      //从classpath下的证书中获取秘钥对
                      KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
                      return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
                  }
              }
              • 如果你想往JWT中添加自定义信息的话,比如说登录用户的ID,可以自己实现TokenEnhancer接口;
                /**
                 * JWT内容增强器
                 * Created by macro on 2020/6/19.
                 */
                @Component
                public class JwtTokenEnhancer implements TokenEnhancer {
                    @Override
                    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
                        Map info = new HashMap<>();
                        //把用户ID设置到JWT中
                        info.put("id", securityUser.getId());
                        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
                        return accessToken;
                    }
                }
                • 由于我们的网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来;
                  /**
                   * 获取RSA公钥接口
                   * Created by macro on 2020/6/19.
                   */
                  @RestController
                  public class KeyPairController {
                      @Autowired
                      private KeyPair keyPair;
                      @GetMapping("/rsa/publicKey")
                      public Map getKey() {
                          RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
                          RSAKey key = new RSAKey.Builder(publicKey).build();
                          return new JWKSet(key).toJSONObject();
                      }
                  }
                  • 不要忘了还需要配置Spring Security,允许获取公钥接口的访问;
                    /**
                     * SpringSecurity配置
                     * Created by macro on 2020/6/19.
                     */
                    @Configuration
                    @EnableWebSecurity
                    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                        @Override
                        protected void configure(HttpSecurity http) throws Exception {
                            http.authorizeRequests()
                                    .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                                    .antMatchers("/rsa/publicKey").permitAll()
                                    .anyRequest().authenticated();
                        }
                        @Bean
                        @Override
                        public AuthenticationManager authenticationManagerBean() throws Exception {
                            return super.authenticationManagerBean();
                        }
                        @Bean
                        public PasswordEncoder passwordEncoder() {
                            return new BCryptPasswordEncoder();
                        }
                    }
                    • 创建一个资源服务ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取。
                      /**
                       * 资源与角色匹配关系管理业务类
                       * Created by macro on 2020/6/19.
                       */
                      @Service
                      public class ResourceServiceImpl {
                          private Map> resourceRolesMap;
                          @Autowired
                          private RedisTemplate redisTemplate;
                          @PostConstruct
                          public void initData() {
                              resourceRolesMap = new TreeMap<>();
                              resourceRolesMap.put("/api/hello", CollUtil.toList("ADMIN"));
                              resourceRolesMap.put("/api/user/currentUser", CollUtil.toList("ADMIN", "TEST"));
                              redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap);
                          }
                      }

                      二、micro-oauth2-gateway

                      接下来我们就可以搭建网关服务了,它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作。

                      1.配置
                      • 在pom.xml中添加相关依赖,主要是Gateway、Oauth2和JWT相关依赖;
                        
                            
                                org.springframework.boot
                                spring-boot-starter-webflux
                            
                            
                                org.springframework.cloud
                                spring-cloud-starter-gateway
                            
                            
                                org.springframework.security
                                spring-security-config
                            
                            
                                org.springframework.security
                                spring-security-oauth2-resource-server
                            
                            
                                org.springframework.security
                                spring-security-oauth2-client
                            
                            
                                org.springframework.security
                                spring-security-oauth2-jose
                            
                            
                                com.nimbusds
                                nimbus-jose-jwt
                                8.16
                            
                        

                         

                        • 在application.yml中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置;
                          server:
                            port: 9201
                          spring:
                            profiles:
                              active: dev
                            application:
                              name: micro-oauth2-gateway
                            cloud:
                              nacos:
                                discovery:
                                  server-addr: localhost:8848
                              gateway:
                                routes: #配置路由规则
                                  - id: oauth2-api-route
                                    uri: lb://micro-oauth2-api
                                    predicates:
                                      - Path=/api/**
                                    filters:
                                      - StripPrefix=1
                                  - id: oauth2-auth-route
                                    uri: lb://micro-oauth2-auth
                                    predicates:
                                      - Path=/auth/**
                                    filters:
                                      - StripPrefix=1
                                discovery:
                                  locator:
                                    enabled: true #开启从注册中心动态创建路由的功能
                                    lower-case-service-id: true #使用小写服务名,默认是大写
                            security:
                              oauth2:
                                resourceserver:
                                  jwt:
                                    jwk-set-uri: 'http://localhost:9401/rsa/publicKey' #配置RSA的公钥访问地址
                            redis:
                              database: 0
                              port: 6379
                              host: localhost
                              password: 
                          secure:
                            ignore:
                              urls: #配置白名单路径
                                - "/actuator/**"
                                - "/auth/oauth/token"
                          2.配置完成,开始代码环节 
                          • 对网关服务进行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启;
                            /**
                             * 资源服务器配置
                             * Created by macro on 2020/6/19.
                             */
                            @AllArgsConstructor
                            @Configuration
                            @EnableWebFluxSecurity
                            public class ResourceServerConfig {
                                private final AuthorizationManager authorizationManager;
                                private final IgnoreUrlsConfig ignoreUrlsConfig;
                                private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
                                private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
                                @Bean
                                public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
                                    http.oauth2ResourceServer().jwt()
                                            .jwtAuthenticationConverter(jwtAuthenticationConverter());
                                    http.authorizeExchange()
                                            .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
                                            .anyExchange().access(authorizationManager)//鉴权管理器配置
                                            .and().exceptionHandling()
                                            .accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
                                            .authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
                                            .and().csrf().disable();
                                    return http.build();
                                }
                                @Bean
                                public Converter> jwtAuthenticationConverter() {
                                    JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
                                    jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
                                    jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
                                    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
                                    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
                                    return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
                                }
                            }
                            • 在WebFluxSecurity中自定义鉴权操作需要实现ReactiveAuthorizationManager接口;
                              /**
                               * 鉴权管理器,用于判断是否有资源的访问权限
                               * Created by macro on 2020/6/19.
                               */
                              @Component
                              public class AuthorizationManager implements ReactiveAuthorizationManager {
                                  @Autowired
                                  private RedisTemplate redisTemplate;
                                  @Override
                                  public Mono check(Mono mono, AuthorizationContext authorizationContext) {
                                      //从Redis中获取当前路径可访问角色列表
                                      URI uri = authorizationContext.getExchange().getRequest().getURI();
                                      Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
                                      List authorities = Convert.toList(String.class,obj);
                                      authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
                                      //认证通过且角色匹配的用户可访问当前路径
                                      return mono
                                              .filter(Authentication::isAuthenticated)
                                              .flatMapIterable(Authentication::getAuthorities)
                                              .map(GrantedAuthority::getAuthority)
                                              .any(authorities::contains)
                                              .map(AuthorizationDecision::new)
                                              .defaultIfEmpty(new AuthorizationDecision(false));
                                  }
                              }
                              • 这里我们还需要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息。
                                /**
                                 * 将登录用户的JWT转化成用户信息的全局过滤器
                                 * Created by macro on 2020/6/17.
                                 */
                                @Component
                                public class AuthGlobalFilter implements GlobalFilter, Ordered {
                                    private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
                                    @Override
                                    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                                        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
                                        if (StrUtil.isEmpty(token)) {
                                            return chain.filter(exchange);
                                        }
                                        try {
                                            //从token中解析用户信息并设置到Header中去
                                            String realToken = token.replace("Bearer ", "");
                                            JWSObject jwsObject = JWSObject.parse(realToken);
                                            String userStr = jwsObject.getPayload().toString();
                                            LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
                                            ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
                                            exchange = exchange.mutate().request(request).build();
                                        } catch (ParseException e) {
                                            e.printStackTrace();
                                        }
                                        return chain.filter(exchange);
                                    }
                                    @Override
                                    public int getOrder() {
                                        return 0;
                                    }
                                }
                                

                                三、micro-oauth2-api

                                最后我们搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它。

                                1.配置
                                • 在pom.xml中添加相关依赖,就添加了一个web依赖;
                                  
                                      
                                          org.springframework.boot
                                          spring-boot-starter-web
                                      
                                  
                                  • 在application.yml添加相关配置,很常规的配置;
                                  • server:
                                      port: 9501
                                    spring:
                                      profiles:
                                        active: dev
                                      application:
                                        name: micro-oauth2-api
                                      cloud:
                                        nacos:
                                          discovery:
                                            server-addr: localhost:8848
                                    management:
                                      endpoints:
                                        web:
                                          exposure:
                                            include: "*"
                                  • 创建一个测试接口,网关验证通过即可访问;
                                    /**
                                     * 测试接口
                                     * Created by macro on 2020/6/19.
                                     */
                                    @RestController
                                    public class HelloController {
                                        @GetMapping("/hello")
                                        public String hello() {
                                            return "Hello World.";
                                        }
                                    }
                                    • 创建一个LoginUserHolder组件,用于从请求的Header中直接获取登录用户信息;
                                      /**
                                       * 获取登录用户信息
                                       * Created by macro on 2020/6/17.
                                       */
                                      @Component
                                      public class LoginUserHolder {
                                          public UserDTO getCurrentUser(){
                                              //从Header中获取用户信息
                                              ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                                              HttpServletRequest request = servletRequestAttributes.getRequest();
                                              String userStr = request.getHeader("user");
                                              JSONObject userJsonObject = new JSONObject(userStr);
                                              UserDTO userDTO = new UserDTO();
                                              userDTO.setUsername(userJsonObject.getStr("user_name"));
                                              userDTO.setId(Convert.toLong(userJsonObject.get("id")));
                                              userDTO.setRoles(Convert.toList(String.class,userJsonObject.get("authorities")));
                                              return userDTO;
                                          }
                                      }
                                      • 创建一个获取当前用户信息的接口。
                                        /**
                                         * 获取登录用户信息接口
                                         * Created by macro on 2020/6/19.
                                         */
                                        @RestController
                                        @RequestMapping("/user")
                                        public class UserController{
                                            @Autowired
                                            private LoginUserHolder loginUserHolder;
                                            @GetMapping("/currentUser")
                                            public UserDTO currentUser() {
                                                return loginUserHolder.getCurrentUser();
                                            }
                                        }

                                        功能演示

                                        接下来我们来演示下微服务系统中的统一认证鉴权功能,所有请求均通过网关访问。

                                        • 在此之前先启动我们的Nacos和Redis服务,然后依次启动micro-oauth2-auth、micro-oauth2-gateway及micro-oauth2-api服务;

                                          Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!,第1张

                                          • 使用密码模式获取JWT令牌,访问地址:http://localhost:9201/auth/oauth/token

                                            Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!,第2张