• 【Spring Cloud】Spring Cloud Oauth2 + Gateway 微服务权限管理方案


    项目架构

    本文采用 Eureka 作为注册中心,Spring Cloud Gateway 作为网关服务,JWT 令牌库使用 nimbus-jose-jwt

    将服务分为以下几个层次:

    • security-gateway:网关层,负责接收所有网络请求、转发以及权限鉴定
    • security-auth:认证层,负责对登录用户进行认证
    • security‐discovery:注册中心
    • security-api:资源层,提供被访问的资源,用户被鉴权之后才可被访问

    这样的设计使得各个服务各司其职,认证层进行认证,网关进行转发和鉴权,资源服务只专注于自己的业务逻辑,无需关心权限。也就是说安全校验逻辑只存在于认证服务和网关服务中。

    微服务

    权限数据库设计

    在前文【Spring Security + Redis + JWT 实现动态权限管理】的基础上,构建用户组-用户-角色-资源的关系进行权限控制。与前文区别在于,最终用户拥有的权限 = 用户组对应的权限 + 用户本身对应的权限。

    之所以设计用户组是为了便于管理庞大的用户数量,通过用户组可以给用户统一进行授权,同一个用户组内的用户具有公共的权限,可以减少赋权的工作量,同时允许用户拥有自己单独的权限,可以做到因人而异,更加具有适应性。

    数据库表设计

    具体实现

    认证服务

    pom.xml 添加相关依赖,主要包括 Spring Security、Oauth2、JWT、Redis、eureka等相关依赖。

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-oauth2artifactId>
            <version>${spring-cloud-starter-oauth2.version}version>
        dependency>
    
        <dependency>
            <groupId>com.nimbusdsgroupId>
            <artifactId>nimbus-jose-jwtartifactId>
            <version>${nimbus-jose-jwt.version}version>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
    
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
    
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>
    
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
    
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    配置文件,主要配置 eureka 信息

    server:
      port: 9401
    spring:
      application:
        name: security-auth
      datasource:
        username: root
        password: "root"
        url: jdbc:mysql://localhost:3306/Security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
    
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      configuration:
        map-underscore-to-camel-case: true
    #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8848/eureka/
      instance:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    需要自定义 UserDetailsService ,将用户信息和权限注入进来,为后面的认证做准备(UserMapper、SysRoleMapper接口可参考前文所述)

    @Service
    public class UserDetailServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserMapper sysUserMapper;
    
        @Autowired
        private SysRoleMapper sysRoleMapper;
    
        //自定义的登录逻辑
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            SysUser user = sysUserMapper.selectByName(username);
            //根据用户名去数据库进行查询,如不存在则抛出异常
            if (user == null){
                throw new UsernameNotFoundException("用户不存在");
            }
            List<GrantedAuthority> authorities = new ArrayList<>();
            // 使用用户、角色、资源、用户组建立关系,使用角色控制权限, 用户权限 = 用户个人权限+用户组权限
            // 查询用户对应的权限
            List<String> codeList = sysRoleMapper.selectUserRole(user.getUsername());
            codeList.forEach(code ->{
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code);
                authorities.add(simpleGrantedAuthority);
            });
            return new User(username, user.getPassword(), authorities);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    配置认证服务相关配置信息,采用账号密码模式进行认证

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private UserDetailServiceImpl userDetailService;
    
        @Resource
        private JwtTokenEnhancer jwtTokenEnhancer;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private JwtAccessTokenConverter accessTokenConverter;
    
        @Resource
        private DataSource dataSource;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Bean
        public ClientDetailsService jdbcClientDetailsService() {
            ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
            ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
            return clientDetailsService;
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.allowFormAuthenticationForClients()
                    .passwordEncoder(passwordEncoder)
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()");
        }
    
        // 设置客户端信息从数据库中读取
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(jdbcClientDetailsService());
        }
    
        @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(userDetailService) // 配置加载用户信息的服务
                    .accessTokenConverter(accessTokenConverter)
                    .tokenEnhancer(enhancerChain);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    设置 token 的方式为 JWT,并自定义 JWT 内部的其他信息

    @Configuration
    public class TokenConfig {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    JWT 增强器,自定义 JWT 内部信息

    @Component
    public class JwtTokenEnhancer implements TokenEnhancer {
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
            String name = oAuth2Authentication.getName();
            Map<String, Object> info = new HashMap<>();
            // 把用户名设置到JWT中
            info.put("name", name);
            ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
            return oAuth2AccessToken;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    暴露公钥接口以便验证签名是否合法

    @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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    配置 Spring Security 信息,开放公钥接口并且设置允许表单形式登录

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailServiceImpl userDetailService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                    .antMatchers("/rsa/publicKey").permitAll()
                    .anyRequest().authenticated().and()
                    .formLogin();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 自定义数据库登录逻辑
            auth.userDetailsService(userDetailService)
                    .passwordEncoder(passwordEncoder);
        }
    
        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    网关

    pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webfluxartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-configartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-oauth2-resource-serverartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-oauth2-clientartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>
    
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-oauth2-joseartifactId>
        dependency>
    
        <dependency>
            <groupId>com.nimbusdsgroupId>
            <artifactId>nimbus-jose-jwtartifactId>
            <version>${nimbus-jose-jwt.version}version>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
    
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
    
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    配置文件主要配置路由转发规则以及 Oauth2 中 RSA 公钥

    server:
      port: 9201
    spring:
      application:
        name: security-gateway
      datasource:
        username: root
        password: "root"
        url: jdbc:mysql://localhost:3306/Security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
      cloud:
        gateway:
          routes: #配置路由路径
            - id: security-api
              uri: lb://security-api
              predicates:
                - Path=/api/**
              filters:
                - StripPrefix=1
            - id: security-auth
              uri: lb://security-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"
    
    eureka:
      client:
        service-url:  # eureka的地址信息
          defaultZone: http://localhost:8848/eureka/
      #  将ip配置到eureka里面,不给就是host名会配置到那个里面
      instance:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
    
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      configuration:
        map-underscore-to-camel-case: true
    #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    对网关服务进行安全信息配置,并使用注解开启

    @Configuration
    @EnableWebFluxSecurity
    public class ResourceServerConfig {
    
        @Autowired
        private AuthorizationManager authorizationManager;
    
        @Autowired
        private WhiteUrlsConfig whiteUrlsConfig;
    
        @Autowired
        private RestAccessDeniedHandler restAccessDeniedHandler;
    
        @Autowired
        private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    
        @Autowired
        private WhiteUrlsRemoveJwtFilter whiteUrlsRemoveJwtFilter;
    
        @Bean
        public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
            http.oauth2ResourceServer().jwt()
                    .jwtAuthenticationConverter(jwtAuthenticationConverter());
            //自定义处理JWT请求头过期或签名错误的结果
            http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
            //对白名单路径,直接移除JWT请求头
            http.addFilterBefore(whiteUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
            http.authorizeExchange()
                    .pathMatchers(ArrayUtil.toArray(whiteUrlsConfig.getUrls(), String.class)).permitAll() // 白名单配置
                    .anyExchange().access(authorizationManager)                        // 鉴权管理器配置
                    .and().exceptionHandling()
                    .accessDeniedHandler(restAccessDeniedHandler)                      // 处理未授权异常
                    .authenticationEntryPoint(restAuthenticationEntryPoint)            // 处理未认证异常
                    .and().csrf().disable();
            return http.build();
        }
    
        // JWT解析器
        @Bean
        public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
            JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
            jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
            jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(CommonConstant.AUTHORITY_CLAIM_NAME);
            JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
            jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
            return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    设置 jwtGrantedAuthoritiesConverter.setAuthorityPrefix()的内容可以视 sys_role 表 code 字段的具体情况而定,能够与Spring Security默认的角色标识是 ROLE 开头而Oauth2默认的角色标识是 SCOPE 匹配即可。

    CommonConstant.AUTHORITY_CLAIM_NAME 常量的内容是"authorities"

    白名单过滤器,过滤白名单内的请求,可以直接放行

    @Data
    @Component
    @EqualsAndHashCode(callSuper = false)
    @ConfigurationProperties(prefix="secure.ignore")
    public class WhiteUrlsConfig {
        private List<String> urls;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    同时,白名单内的请求可以去掉 JWT 请求头,减少负载信息

    @Component
    public class WhiteUrlsRemoveJwtFilter implements WebFilter {
    
        @Autowired
        private WhiteUrlsConfig whiteUrlsConfig;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            // 获取当前路径
            ServerHttpRequest request = exchange.getRequest();
            URI uri = request.getURI();
            PathMatcher pathMatcher = new AntPathMatcher();
            List<String> whiteUrls = whiteUrlsConfig.getUrls();
            for (String url : whiteUrls) {
                // 若为白名单路径则移除JWT请求头
                if (pathMatcher.match(url, uri.getPath())) {
                    request = exchange.getRequest().mutate().header("Authorization", "").build();
                    exchange = exchange.mutate().request(request).build();
                    return chain.filter(exchange);
                }
            }
            return chain.filter(exchange);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    实现ReactiveAuthorizationManager鉴权决策器接口实现具体鉴权功能

    @Component
    public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    
        private static final Logger logger = LoggerFactory.getLogger(AuthorizationManager.class);
    
        @Autowired
        private RedisUtils redisUtils;
    
        @Autowired
        private SysRoleMapper sysRoleMapper;
    
        @Override
        public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
            // 获取当前访问路径
            URI uri = authorizationContext.getExchange().getRequest().getURI();
            // 获取可访问当前路径的所有角色
            Object roles = redisUtils.hashGet(RedisConstant.RESOURCE_ROLES_PATH, uri.getPath());
            logger.info("current request path: {}", uri.getPath());
            List<String> authorities = roles == null ?
                    sysRoleMapper.selectUrlRole(uri.getPath()) : Convert.toList(String.class, roles);
            logger.info("current path authorities: {}", JSON.toJSON(authorities));
            //认证通过且角色匹配的用户可访问当前路径
            return mono
                    .filter(Authentication::isAuthenticated)
                    .flatMapIterable(Authentication::getAuthorities)
                    .map(GrantedAuthority::getAuthority)
                    .any(authorities::contains)
                    .map(AuthorizationDecision::new)
                    .defaultIfEmpty(new AuthorizationDecision(false));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    同时,为了便于资源服务使用,通过实现全局过滤器将 JWT 信息解析后“换成”用户个人信息并写入回请求头。

    @Component
    public class AuthGlobalFilter implements GlobalFilter, Ordered {
    
        private final 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    自定义无权限访问处理器(可参考前文实现)

    @Component
    public class RestAccessDeniedHandler implements ServerAccessDeniedHandler {
    
        @Override
        public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
            ServerHttpResponse response = serverWebExchange.getResponse();
            response.setStatusCode(HttpStatus.OK);
            response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            String body= JSONUtil.toJsonStr(CommonResultUtil.fail(ResponseCode.NO_PERMISSION));
            DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
            return response.writeWith(Mono.just(buffer));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注册中心

    pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
    
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    配置文件

    spring:
      application:
        name: security‐discovery
    server:
      port: 8848 #启动端口
    
    eureka:
      instance:
        hostname: localhost
      client:
        fetch-registry: false #是否从Eureka Server获取注册信息
        register-with-eureka: false #是否将自己注册到Eureka Server
        service-url:
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #服务地址
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    启动类

    @SpringBootApplication
    @EnableEurekaServer
    public class DiscoveryServer {
        public static void main(String[] args) {
            SpringApplication.run(DiscoveryServer.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    资源服务

    配置文件

    server:
      port: 9501
    spring:
      application:
        name: security-api
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8848/eureka/ #注册中心地
      instance:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    新建一个 controller 层测试效果

    @RestController
    @RequestMapping("/test")
    public class TestController {
    
        @GetMapping("/get")
        public String test(){
            return "hello, world!";
        }
    
        @GetMapping("/get2")
        public String test2(){
            return "hello, cloud!";
        }
    
        @GetMapping("/other")
        public String test3(){
            return "hello, other!";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    访问时,先使用密码模式获取 JWT 令牌,访问地址:http://localhost:9201/auth/oauth/token 即可获得返回的 token 信息

    获取token

    将返回的 token 放入请求头,访问有权限访问的接口即可正常访问

    有接口访问权限

    访问无权访问的接口,提示权限不足

    无接口访问权限

  • 相关阅读:
    html在线商城购物网站制作——基于HTML+CSS+JavaScript鲜花礼品电商网站
    sql小技巧:日期区间和格式化操作
    Python---if选择判断结构、嵌套结构(if elif else)
    AHB、APB详解和FCLK,PCLK,HCLK详解
    解决matlab报错“输入参数的数目不足”
    Servlet生命周期与线程安全
    Linux 服务器 Firewalld 防火墙配置端口转发
    2022年6月 电子学会青少年软件编程 中小学生Python编程 等级考试一级真题答案解析(判断题)
    spring service事务传播
    关于动态规划法
  • 原文地址:https://blog.csdn.net/qq_36748248/article/details/125916633