代码编织梦想

在springcloud微服务架构下,如何进行统一的认证、鉴权,一直是大家非常关心的问题,下面对微服务架构下的认证和鉴权继续聊聊,一是自己的再次思考总结,二是希望对小伙伴有所帮助。

1、方案思路

在springcloud微服务中,认证授权服务统一负责认证(用户登录),网关服务统一负责校验认证和鉴权,其他业务服务负责处理自己的业务逻辑。安全相关功能只存在于认证授权服务和网关服务中,其他业务服务只是提供业务处理而没有任何安全相关功能。

2、技术组件

采用Nacos作为注册中心和配置中心;
采用Spring Security + Oauth2作为安全框架;
采用Spring Cloud Gateway作为网关服务;
采用JWT操作令牌;

其中,nacos版本为2.0.4+,基础组件版本如下:

<properties>
    <spring-boot.version>2.7.0</spring-boot.version>
    <spring-cloud.version>2021.0.4</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
    <spring-cloud-starter-oauth2.version>2.2.5.RELEASE</spring-cloud-starter-oauth2.version>
    <nimbus-jose-jwt.version>9.23</nimbus-jose-jwt.version>
</properties>

3、服务划分

auth:认证服务,负责对登录用户进行认证,整合Spring Security + Oauth2 + Jwt;
gateway:网关服务,负责对请求进行动态路由、校验认证和鉴权,整合Spring Security + Oauth2 + Jwt;
api:受保护的api服务,用户通过校验认证和鉴权后可以访问该服务,不整合Spring Security + Oauth2 + Jwt;

4、auth服务实例

下面搭建认证服务,将会整合security+oauth2+jwt,并且网关服务的鉴权功能需要依赖认证服务。在该服务中client信息和user信息都是存在于内存中,后续有需要可持久化到数据库中。

在pom.xml文件中添加依赖

		<!--Spring Cloud & Alibaba -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <!-- 注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

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

        <!-- 配置中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <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>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

在bootstrap.yml文件中添加配置

server:
  port: 9401
spring:
  profiles:
    active: dev
  application:
    name: 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:

在jdk的bin目录下使用如下命令生成RSA证书jwt.jks,复制到resource目录下

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

添加认证服务相关配置类,需要配置加载用户信息及RSA的密钥对KeyPair

@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final PasswordEncoder passwordEncoder;
    private final CustomUserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;
    private final CustomTokenEnhancer customTokenEnhancer;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .secret(passwordEncoder.encode("1"))
                .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(customTokenEnhancer);
        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());
    }

}

添加Spring Security配置类,允许获取公钥接口的访问

@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();
    }

}

由于网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口将公钥暴露出来

@RestController
@RequiredArgsConstructor
public class KeyPairController {

    private final 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();
    }

}

添加一个资源类,初始化的时候把资源与角色匹配关系缓存的redis中,方便网关服务进行鉴权的时候获取

@Component
@RequiredArgsConstructor
public class ResourceService {

    private Map<String, List<String>> resourceRolesMap;

    private final 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);
    }
}

其他相关类不再描述,可参考大佬博文。

5、gateway服务实例

下面搭建网关服务,它将作为oauth2的资源服务、客户端服务使用,对访问业务服务的请求进行统一的校验认证和鉴权,校验通过进行动态路由。

在pom.xml文件中添加依赖

		<!--Spring Cloud & Alibaba -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <!-- 注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

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

        <!-- 配置中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <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>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

在bootstrap.yml文件中添加配置

server:
  port: 9201
spring:
  profiles:
    active: dev
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes: #配置路由路径
        - id: oauth2-api-route
          uri: lb://api
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=1
        - id: oauth2-auth-route
          uri: lb://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注解开启

@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

    private final AuthorizationManager authorizationManager;
    private final IgnoreUrlsConfig ignoreUrlsConfig;
    private final RestAccessDeniedHandler restAccessDeniedHandler;
    private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        //自定义处理JWT请求头过期或签名错误的结果
        http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
        //对白名单路径,直接移除JWT请求头
        http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        http.authorizeExchange()
                .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
                .anyExchange().access(authorizationManager)//鉴权管理器配置
                .and().exceptionHandling()
                .accessDeniedHandler(restAccessDeniedHandler)//处理未授权
                .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接口

@Component
@RequiredArgsConstructor
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final 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中获取到用户信息

@Slf4j
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @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();
            log.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;
    }
}

其他相关类不再描述,可参考大佬博文。

6、api服务实例

下面搭建一个api服务,不整合任何安全框架,全靠网关来保护它

在pom.xml文件中添加依赖

		<!--Spring Cloud & Alibaba -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <!-- 注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

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

        <!-- 配置中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

在bootstrap.yml文件中添加配置

server:
  port: 9601
spring:
  profiles:
    active: dev
  application:
    name: msbd-api
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

添加一个测试接口

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello World.";
    }

}

7、演示

下面演示微服务中认证、鉴权功能,所有请求均通过网关访问。需要依次启动nacos服务、redis服务、auth服务、gateway服务、api服务。

nacos服务界面如下
在这里插入图片描述
使用密码模式获取token令牌,访问地址:http://localhost:9201/auth/oauth/token
在这里插入图片描述
未携带token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
在这里插入图片描述
携带token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
在这里插入图片描述
携带无访问权限的token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
在这里插入图片描述

携带过期token访问api服务的hello接口,访问地址:http://localhost:9201/api/hello
在这里插入图片描述
在微服务中,不应该把重复校验认证和鉴权的功能集成到每个业务服务中,应该在认证服务做统一认证,在网关中做统一校验,这样才是优雅的微服务架构的安全解决方案。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/leijie0322/article/details/128490985

SpringCloud+Spring Security+OAuth2 + JWT + Gateway讲解-爱代码爱编程

项目简介 本登录系统是一个适应前后端分离并支持传统PC登录的全方位微服务登录架构基于Spring Boot 2.2.8.、 Spring Cloud Hoxton.SR5 和 Spring Cloud Alibaba 2.2.1深度定制Spring Security,基于RBAC、jwt和oauth2的无状态统一权限认证的单点登录、单点登出、续签等

nacos 负载策略_Springcloud + nacos + gateway 负载均衡(ribbon)-爱代码爱编程

what is robbon? Ribbon是客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用 什么叫负载均衡 负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行

SpringCloud+Nacos+Gateway-爱代码爱编程

目录 一.Nacos安装和启动-Linux和window1. 下载2. Linux安装3. 运行可能会出现的问题二.SpringCloud服务注册和发现1.新建项目2.导入依赖3.启动4.可能出现的问题三.Gateway配置1.创建项目2.初始化项目3.可能出现的问题 一.Nacos安装和启动-Linux和window 1. 下载 http

【教程】SpringCloud+Nacos+Feign+Gateway ( 八 ) Nacos-Gateway 网关 限流-爱代码爱编程

【教程】SpringCloud+Nacos+Feign+Gateway ( 八 ) Nacos-Gateway 网关 限流   从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,

springCloud+nacos简单搭建-爱代码爱编程

1.nacos安装搭建   参考文章:http://www.bubuko.com/infodetail-3682508.html 官网:              nacos: https://nacos.io/zh-cn/docs/quick-start.html 默认端口:8848  2.搭建springcloud+nacos项目 1、前置

SpringBoot + SpringCloud+Nacos项目搭建-爱代码爱编程

版本选择 spring官网 (**20210715修改)**经后面折腾用最新版本 boot 2.5.2 和 cloud 2020.0.3,在项目启动时会报No active profile set, falling back to default profiles 的错。 最终选择boot版本为:2.3.12.RELEASE cloud版本为 H

Springcloud security+Nacos+Spring Boot Admin+Gateway框架搭建-爱代码爱编程

当前架构不断演进,分布式架构的重要性越开越高,本文就记录一下整个搭建过程,本次主要利用springcloud自带的分布式特性,由于nacos可以支持动态刷新以及拥有可视化界面,方便服务上下线管理,故采用nacos提代eureka以及config,利用springboot admin配合acturaror对各微服务进行监控,同时利用nacos的动态刷新配合g

SpringCloud+nacos+gateway+feign+seata-爱代码爱编程

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 大数据系列文章目录Seata官网 服务器版本: nacos-server-2.0.4seata-server-1.4.2代码使用版本: spring-boot-starter-parent:2.1.6.RELEASEspring-cloud-d

SpringCloud+Nacos搭建使用-爱代码爱编程

Nacos【Dynamic Naming and Configuration Service】提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 1.搭建Nocos服务 01.下载服务 下载地址:Releases · alibaba/nacos · GitHub  在window环境学习我下载了zip包

springcloud+nacos-爱代码爱编程

Springcloud 学习 1.从简单demo 搭建开始cloud_gateWay 网关配置文件: bootstrap.yml问题一: bootstrap.yml和 application.yml 文件区别。问题二:lb://xx 客户端负载均衡。cloud_order配置文件: bootstrap.yml思考 1.从简单demo 搭建开始

Spring Cloud+Nacos+feign-爱代码爱编程

Spring Cloud+Nacos+Fegin 使用feign调用其他服务,提前是先要把其他服务(服务端和消费端)注册到Nacos上使用Nacos作为服务注册中心 1.pom.xml中添加依赖 <dependency> <groupId>org.springframework.cloud</

springcloud + oauth2.0 + shiroredis + jwt + gateway + nacos + nginx + ant-design-vue 电商 java 项目_年少有为2025的博客-爱代码爱编程

该项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现,采用 Docker 容器化部署。 前台商城系统:首页、商品推荐、商品搜索、商品展示、购物车、订单自取和外卖流程、会员中心、点单、积分签到,领劵中心,营销活动等。 后台管理系统:商品管理、订单管理、会员管理、营销管理、运营管理、内容管理、统计报表、

springcloud+nacos+gateway+oauth2小聚会-爱代码爱编程

在微服务工程中,Nacos作为目前主流的注册中心和配置中心,Spring Cloud Gateway作为目前主流的网关,下面引入spring security+oauth2+jwt作为认证和授权中心,进行简单的聊聊。 一、s