当前位置:   article > 正文

OAuth2.0 - 使用 SpringGateWay 网关实现统一鉴权_spring gateway 鉴权

spring gateway 鉴权

一、OAuth2.0

上篇文章我们学习了将用户信息存储至数据库中,前面的学习大家肯定已经对Oauth2.0已经有了自己认识,从前面我们可以了解到了,在用户认证时可以得到自己的用户信息及权限信息,然后权限的校验还是在资源服务实现的,从前面讲解SpringSecurity单体环境的时候我们要做统一的动态鉴权操作,需要实现FilterInvocationSecurityMetadataSource 接口,在其中进行地址的动态角色权限的返回。现在服务的大环境下,每个业务服务都可以称为一个资源服务,那众多的资源服务,每个都实现这个相同的逻辑,有点显得程序的冗余了。

这个时候我们就可以从服务的统一入口gateway网关处寻找突破口了,我们完全可以将每次请求,地址鉴权的操作移交到gateway网关处进行统一拦截处理,并且还可以将Jwt的校验也在gateway网关中进行,现在就不显得程序的冗余了,因为我们只在网关做了改动,其他业务服务不做权限的校验了。

但是这样还是有一个问题,从整体上看我们的服务是安全的,但是每个业务服务之间还是裸奔的形式,一旦黑客绕过了网关,那我们的业务服务就没有安全可言了。而且还有一个问题就是有的接口我们不想配制在网关的统一把控,要通过SpringSecurity的权限注解进行操作,这个时候不就是不生效了吗。

面对上面问题,我们既要保证每个服务是安全的,也要允许每个服务拥有自定义权限的操作,这个时候我们可以就选择对当网关校验成功后,在header中再添加一个Token,这个Token可以还是一个JWT,不过可以把过期时长设置的短一些,也可以是Base64加密之后的字符,业务服务接收到请求,通过继承BasicAuthenticationFilter 过滤器,将用户角色权限信息添加到UsernamePasswordAuthenticationToken 中,交给SpringSecurity管理,这样每个服务就拥有自定义权限的功能,就算绕过网关,还是需要token的校验。

上面已经保证了服务的安全,但是给用户的Jwt是通过对称加密的也有点不太安全,我们可以采用RSA非对称加密的方式生成JWT,认证服务用私钥加密,网关通过公钥校验解析数据,然后转发到每个微服务的Token,如果采用Jwt就可以使用对称加密的方式,减少性能的开销。

总结上面的过程,就是下面我画的这个流程图:

注意:这里为了方便大家的理解,把认证服务和网关分离开来了,正常大家微服务项目中,认证服务也要经过网关的。
在这里插入图片描述
下面我们结合前面文章的内容,继续修改项目,实现上面所说的功能,其中网关转发到业务服务,业务服务进行token的解析,由于篇幅,这里就不做展示了,因为这个和前面讲解SpringSecurity整合JWT完全就是一个类型,这里就没必要再拿出来说一遍了,大家可以看下这篇博客:

https://blog.csdn.net/qq_43692950/article/details/122396221

二、认证服务

认证服务前面是通过对称加密生成的JWT,这里我们要修改为RSA非对称加密的方式。

首先生成密钥,可以通过JDK自带的keytool工具进行生成,在cmd中输入以下命令:

keytool -genkey -alias bxc -keyalg RSA -keypass 123456 -keystore bxc.jks -storepass 123456
  • 1

在这里插入图片描述
上面提示的信息可填写也可以不填,最后的一定要填,不然一直循环。

在当前目录就可以看到生成的jks文件了:
在这里插入图片描述
将上面生成的文件复制到认证服务的resources下:
在这里插入图片描述
在开始前我们还要对资源服务再引入一个依赖,就是nimbus,也是一个JWT的生成库,要比jjwt更加强大,有兴趣的可以学习下

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>8.16</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

下面修改TokenConfig,修改为使用非对称密钥:

@Configuration
public class TokenConfig {

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair()); //非对称秘钥
        return converter;
    }

    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("bxc.jks"), "123456".toCharArray());
        return factory.getKeyPair("bxc", "123456".toCharArray());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

下面还要编写一个公钥的接口,既然是公钥,就要公开,当然也可以使用keytoolopenssl生成公钥放在资源服务的resources下,通过本地读取的方式获取公钥,这里先提供一个公钥的接口:

@RestController
public class KeyController {

    @Autowired
    KeyPair keyPair;

    //获取公钥
    @GetMapping("/key/public-key")
    public Map<String, Object> getPublicKey() {
        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

将该接口放行,不需要认证,修改WebSecurityConfigconfigure(HttpSecurity http),将/key/**规则的直接permitAll()即可

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/key/**").permitAll()
            .antMatchers("/login/*").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

到这认证服务就已经修改完成,重启服务,先看下获取的JWT:
在这里插入图片描述
可以明显看到使用RAS非对称生成的JWT要比对称的内容要长,下面再看下公钥的样子:
在这里插入图片描述
下面我们就可以去实现网关的逻辑了。

三、GateWay网关

新建一个SpringBoot的module,在依赖中添加GateWay 和 Oauth2.0 的依赖:

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

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

修改配制文件,转发到资源服务,这里我直接配制的地址转发,由于篇幅这里没有继承注册中心,对网关不了解的,可以看下本专栏关于GateWay网关的讲解。

server:
  port: 80

spring:
  application:
    name: gateway
  cloud:
    gateway:
      httpclient:
        connect-timeout: 2000
        response-timeout: 10s
      routes:
        - id: resuorce
          uri: http://127.0.0.1:8080
          order: 0
          predicates:
            - Path=/resuorce/**
          filters:
            - StripPrefix=1 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

下面我们先创建两个Handler,一个无权访问的,一个登录失效的:

@Component
public class AccessDeniedHandler implements ServerAccessDeniedHandler {
    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
        JSONObject params = new JSONObject();
        params.put("code", 403);
        params.put("msg", "权限不足!");

        ServerHttpResponse response = serverWebExchange.getResponse();

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        Mono<Void> ret = null;
        try {
            ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
        } catch (UnsupportedEncodingException e0) {
            e0.printStackTrace();
        }
        return ret;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
@Component
public class LoginLoseHandler extends HttpBasicServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        JSONObject params = new JSONObject();
        params.put("code", 401);
        params.put("msg", "登录失效!");

        ServerHttpResponse response = exchange.getResponse();

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
        Mono<Void> ret = null;
        try {
            ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
        } catch (UnsupportedEncodingException e0) {
            e0.printStackTrace();
        }
        return ret;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

下面创建一个AuthManagerHandler类进行统一权限的控制,其中这块在注释中也写的比较明确了,就看你们的逻辑自行发挥

@Component
public class AuthManagerHandler implements ReactiveAuthorizationManager<AuthorizationContext> {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
        ServerHttpRequest request = object.getExchange().getRequest();
        String requestUrl = request.getPath().pathWithinApplication().value();
        //这里可以根据requestUrl查询redis,或者数据库得到requestUrl所需的角色,放入roles中
       List<String> roles = new ArrayList<>();
        if (antPathMatcher.match("/resuorce/admin/**",requestUrl)){
            roles.add("ROLE_admin");
        }else {
            roles.add("ROLE_common");
        }
        roles.add("ROLE_admin");
        return authentication
                .filter(a -> a.isAuthenticated())
                .flatMapIterable(a -> a.getAuthorities())
                .map(g -> g.getAuthority())
                .any(c -> {
                    if (roles.contains(String.valueOf(c))) {
                        return true;
                    }
                    return false;
                })
                .map(hasAuthority -> new AuthorizationDecision(hasAuthority))
                .defaultIfEmpty(new AuthorizationDecision(false));
    }

    @Override
    public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
        return null;
    }
}

  • 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

编写接受JWT,并将其转化放入header传入资源服务过滤器:

@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityGlobalFilter implements WebFilter {

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        String token = request.getHeaders().getFirst("Authorization");
        if (StringUtils.isEmpty(token)) {
            JSONObject jsonObject = setResultErrorMsg(401, "登录失效");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toString().getBytes());
            return response.writeWith(Mono.just(buffer));
        }
        // 解析JWT获取jti,以jti为key判断redis的黑名单列表是否存在,存在则拦截访问

        token = token.replace("Bearer ", "");
        String payload = "";
        try {
            payload = StrUtil.toString(JWSObject.parse(token).getPayload());
        } catch (Exception e) {
            JSONObject jsonObject = setResultErrorMsg(401, "登录失效");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toString().getBytes());
            return response.writeWith(Mono.just(buffer));
        }
        if (StringUtils.isEmpty(payload)) {
            JSONObject jsonObject = setResultErrorMsg(401, "登录失效");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toString().getBytes());
            return response.writeWith(Mono.just(buffer));
        }

        //给header里面添加值
        JSONObject jsonObject = JSONUtil.parseObj(payload);
        String base64 = Base64.encode(jsonObject.toString());
        ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("token", base64).build();
        ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
        return chain.filter(build);
    }

    private JSONObject setResultErrorMsg(Integer code, String msg) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", code);
        jsonObject.put("message", msg);
        return jsonObject;
    }
}
  • 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

下面就要创建ResourceServerConfig,对上面的进行整合及,JWT的配制:

@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

    @Autowired
    AuthManagerHandler authManagerHandler;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;

    @Autowired
    LoginLoseHandler loginLoseHandler;

    @Autowired
    SecurityGlobalFilter securityGlobalFilter;


    // security的鉴权排除列表
    private static final String[] excludedAuthPages = {
            "/auth/**"
    };


    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
          http.oauth2ResourceServer().jwt()
              .jwtAuthenticationConverter(jwtAuthenticationConverter())
              //.publicKey(rsaPublicKey()) // 本地获取公钥
              .jwkSetUri("http://localhost:8020/key/public-key")  // 远程获取公钥
              .and()
              .accessDeniedHandler(accessDeniedHandler)
              .authenticationEntryPoint(loginLoseHandler)
              .and().authorizeExchange()
              .pathMatchers(excludedAuthPages).permitAll()  //无需进行权限过滤的请求路径
              .pathMatchers(HttpMethod.OPTIONS).permitAll() //o
              .pathMatchers("/**").access(authManagerHandler)
              .anyExchange().authenticated()
              .and()
              .addFilterAfter(securityGlobalFilter, SecurityWebFiltersOrder.FIRST)
              .cors().disable().csrf().disable();
        return http.build();
    }


    @Bean
    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

        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
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

启动网关服务

四、测试

如果不携带JWT访问:
在这里插入图片描述
登录获取JWT
在这里插入图片描述
访问资源服务接口:
在这里插入图片描述
访问/common/**,应该无权访问:
在这里插入图片描述
通过Debug也可以看出,权限校验统一在AuthManagerHandler中了:
在这里插入图片描述
在这里插入图片描述
喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/68914
推荐阅读
相关标签
  

闽ICP备14008679号