微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

# 微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!

最近发现了一个很好的微服务权限解决方案,可以通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权。此方案为目前最新方案,仅支持Spring Boot 2.2.0、Spring Cloud Hoxton 以上版本,本文将详细介绍该方案的实现,希望对大家有所帮助!

# 前置知识

我们将采用Nacos作为注册中心,Gateway作为网关,使用nimbus-jose-jwtJWT库操作JWT令牌,对这些技术不了解的朋友可以看下下面的文章。

  • Spring Cloud Gateway:新一代API网关服务
  • Spring Cloud Alibaba:Nacos 作为注册中心和配置中心使用

# 应用架构

我们理想的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

相关服务划分:

  • micro-oauth2-gateway:网关服务,负责请求转发和鉴权功能,整合Spring Security+Oauth2;
  • micro-oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合Spring Security+Oauth2;
  • micro-oauth2-api:受保护的API服务,用户鉴权通过后可以访问该服务,不整合Spring Security+Oauth2。

# 方案实现

下面介绍下这套解决方案的具体实现,依次搭建认证服务、网关服务和API服务。

# micro-oauth2-auth

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

  • pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖;
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>8.16</version>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
  • 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
  • 创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息;
    /**
     * 用户管理业务类
     * Created by macro on 2020/6/19.
     */
    @Service
    public class UserServiceImpl implements UserDetailsService {
    
        private List<UserDTO> 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<UserDTO> 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<TokenEnhancer> 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<String, Object> 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<String, Object> 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<String, List<String>> resourceRolesMap;
        @Autowired
        private RedisTemplate<String,Object> 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的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作。

  • pom.xml中添加相关依赖,主要是Gateway、Oauth2和JWT相关依赖;
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>8.16</version>
        </dependency>
    </dependencies>
  • 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"
  • 对网关服务进行配置安全配置,由于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<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> 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<AuthorizationContext> {
        @Autowired
        private RedisTemplate<String,Object> redisTemplate;
    
        @Override
        public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
            //从Redis中获取当前路径可访问角色列表
            URI uri = authorizationContext.getExchange().getRequest().getURI();
            Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
            List<String> 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<Void> 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服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它。

  • pom.xml中添加相关依赖,就添加了一个web依赖;
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
  • 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-authmicro-oauth2-gatewaymicro-oauth2-api服务;

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

  • 使用获取到的JWT令牌访问需要权限的接口,访问地址:http://localhost:9201/api/hello

  • 使用获取到的JWT令牌访问获取当前登录用户信息的接口,访问地址:http://localhost:9201/api/user/currentUser

  • 当JWT令牌过期时,使用refresh_token获取新的JWT令牌,访问地址:http://localhost:9201/auth/oauth/token

  • 使用没有访问权限的andy账号登录,访问接口时会返回如下信息,访问地址:http://localhost:9201/api/hello

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SpringCloud是一个基于Spring Boot的开源微服务框架。SpringCloud GatewaySpringCloud生态中的一个组件,提供了一种基于路由的API网关解决方案。JWT是JSON WEB Token的缩写,是一种用于身份认证和授权的开放标准。OAuth2是一种授权框架,用于向用户授权第三方应用访问他们的资源。 在微服务架构中,每个服务都是独立的,网关作为服务的入口,可以实现对外的请求过滤和路由。SpringCloud Gateway使用HttpClient进行内部请求的调度和路由。同时,它还提供了一些高阶的路由和过滤功能,如重定向、URL重写、限流、熔断、重试等。 JWT是一种轻量级的认证方案,通过在HTTP请求中添加一个JSON WEB Token,实现对用户进行身份认证和授权。JWT的使用极大地简化了认证过程,前后端可以通过JWT判断用户的身份和权限OAuth2为开发者提供了一种授权框架,可以授权第三方应用获取用户访问他们的资源。OAuth2支持多种授权类型,如授权码模式、密码模式、客户端模式和隐式模式。使用OAuth2,可以更好地保护用户的隐私和安全。 综上所述,SpringCloud Gateway、JWT和OAuth2都是现代化的解决方案,对于设计和开发微服务架构的应用程序来说,它们都是必不可少的组件。有了它们,开发人员可以更好的搭建分布式架构,确保数据安全性、隐私安全性和服务的可靠性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值