关于java:Spring-Security-Resource-Server的使用

10次阅读

共计 6889 个字符,预计需要花费 18 分钟才能阅读完成。

一、背景

在前一节咱们学习了 Spring Authorization Server 的应用,此处咱们简略的记录下 Spring 资源服务器的应用。

二、需要

资源服务器提供 2 个资源 ,userInfohello
userInfo:资源是受爱护的资源,须要 user.userInfo 权限才能够拜访。
hello:资源是公开资源,不要权限即可拜访。

三、剖析

1、如何验证资源服务器中拜访的令牌是无效的?

此处只思考 JWT 的令牌。

令牌是 受权服务器颁发 的,且进行了签名操作,因而资源服务器对令牌的验证,就须要受权服务器的 JWK 信息,此处能够配置 JwtDecoder 来实现,并且填写好jwk set uri

2、令牌是从申请中那个中央来的?
令牌能够从申请的 request header或者 request query param 中获取,因而就须要配置 BearerTokenResolver来实现。

3、令牌中的权限字段,默认会加上 SCOPE_ 前缀,想去掉如何操作。
配置 JwtAuthenticationConverter 对象。

4、如果像向 JWT 的 claim 中增加值如何操作?
通过 JwtDecoder#setClaimSetConverter来操作。此处也能够实现删除 claim 的中内容。

5、如何验证 JWT 是否非法?
通过 JwtDecoder#setJwtValidator办法来操作。

6、如何设置从受权服务器获取 JWK 的超时工夫?
通过 JwkSetUriJwtDecoderBuilder#restOperations来操作。

四、资源服务器认证流程


1、申请会被 BearerTokenAuthenticationFilter 拦截器拦挡,并从中解析出 token 进去,如果没有解析进去,则由下一个过滤器解决。解析进去则构建一个 BearerTokenAuthenticationToken 对象。
2、下一步将 HttpServletRequest 传递给 AuthenticationManagerResolver 对象,由它抉择出 AuthenticationManager 对象,而后将 BearerTokenAuthenticationToken传递给 AuthenticationManager 对象进行认证。AuthenticationManager对象的实现,取决于咱们的 token 对象是 JWT 还是 opaque token
3、验证失败

  1. 清空 SecurityContextHolder 对象。
  2. 交由 AuthenticationFailureHandler 对象解决。

4、验证胜利

  1. Authentication 对象设置到 SecurityContextHolder 中。
  2. 交由余下的过滤器持续解决。

五、实现资源服务器

1、引入 jar 包

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

2、资源服务器配置

package com.huan.study.resource.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collections;

/**
 * 资源服务器配置
 *
 * @author huan.fu 2021/7/16 - 下午 5:00
 */
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {private static final Logger log = LoggerFactory.getLogger(ResourceServerConfig.class);

    @Autowired
    private RestTemplateBuilder restTemplateBuilder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()
                // 对于 userInfo 这个 api 须要 s
                .antMatchers("/userInfo").access("hasAuthority('user.userInfo')")
                .and()
                .oauth2ResourceServer()
                .jwt()
                // 解码 jwt 信息
                .decoder(jwtDecoder(restTemplateBuilder))
                // 将 jwt 信息转换成 JwtAuthenticationToken 对象
                .jwtAuthenticationConverter(jwtAuthenticationConverter())
                .and()
                // 从 request 申请那个中央中获取 token
                .bearerTokenResolver(bearerTokenResolver())
                // 此时是认证失败
                .authenticationEntryPoint((request, response, exception) -> {
                    // oauth2 认证失败导致的,还有一种可能是非 oauth2 认证失败导致的,比方没有传递 token,然而拜访受权限爱护的办法
                    if (exception instanceof OAuth2AuthenticationException) {OAuth2AuthenticationException oAuth2AuthenticationException = (OAuth2AuthenticationException) exception;
                        OAuth2Error error = oAuth2AuthenticationException.getError();
                        log.info("认证失败, 异样类型:[{}], 异样:[{}]", exception.getClass().getName(), error);
                    }
                    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                    response.setContentType(MediaType.APPLICATION_JSON.toString());
                    response.getWriter().write("{\"code\":-3,\"message\":\" 您无权限拜访 \"}");
                })
                // 认证胜利后,无权限拜访
                .accessDeniedHandler((request, response, exception) -> {log.info("您无权限拜访, 异样类型:[{}]", exception.getClass().getName());
                    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                    response.setContentType(MediaType.APPLICATION_JSON.toString());
                    response.getWriter().write("{\"code\":-4,\"message\":\" 您无权限拜访 \"}");
                })
        ;
    }

    /**
     * 从 request 申请中那个中央获取到 token
     */
    private BearerTokenResolver bearerTokenResolver() {DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
        // 设置申请头的参数,即从这个申请头中获取到 token
        bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.AUTHORIZATION);
        bearerTokenResolver.setAllowFormEncodedBodyParameter(false);
        // 是否能够从 uri 申请参数中获取 token
        bearerTokenResolver.setAllowUriQueryParameter(false);
        return bearerTokenResolver;
    }

    /**
     * 从 JWT 的 scope 中获取的权限 勾销 SCOPE_ 的前缀
     * 设置从 jwt claim 中那个字段获取权限
     * 如果须要同多个字段中获取权限或者是通过 url 申请获取的权限,则须要本人提供 jwtAuthenticationConverter()这个办法的实现
     *
     * @return JwtAuthenticationConverter
     */
    private JwtAuthenticationConverter jwtAuthenticationConverter() {JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 去掉 SCOPE_ 的前缀
        authoritiesConverter.setAuthorityPrefix("");
        // 从 jwt claim 中那个字段获取权限,模式是从 scope 或 scp 字段中获取
        authoritiesConverter.setAuthoritiesClaimName("scope");
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }

    /**
     * jwt 的解码器
     *
     * @return JwtDecoder
     */
    public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
        // 受权服务器 jwk 的信息
        NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://qq.com:8080/oauth2/jwks")
                // 设置获取 jwk 信息的超时工夫
                .restOperations(builder.setReadTimeout(Duration.ofSeconds(3))
                                .setConnectTimeout(Duration.ofSeconds(3))
                                .build())
                .build();
        // 对 jwt 进行校验
        decoder.setJwtValidator(JwtValidators.createDefault());
        // 对 jwt 的 claim 中增加值
        decoder.setClaimSetConverter(MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("为 claim 中减少 key", custom -> "值"))
        );
        return decoder;
    }
}

此处对资源服务器进行了很多的自定义操作,因而配置比拟长。

3、资源

1 个受爱护的资源和一个非受爱护的资源。

@RestController
public class UserController {

    /**
     * 这个是受爱护的资源,须要 user.userInfo 权限才能够拜访。*/
    @GetMapping("userInfo")
    public Map<String, Object> userInfo(@AuthenticationPrincipal Jwt principal) {return new HashMap<String, Object>(4) {{put("principal", principal);
            put("userInfo", "获取用户信息");
        }};
    }

    /**
     * 非受权限爱护的资源
     */
    @GetMapping("hello")
    public String hello() {return "hello 不要须要受爱护的资源";}
}

六、测试

1、拜访非受爱护的资源


能够看到不须要 token 即能够拜访。

2、拜访受爱护的资源


1、先不必 token 拜访,能够看到是回绝的。
2、而后通过受权服务器生成一个 token,受权服务器为上一篇文章应用的受权服务器。
3、通过 token 拜访后,能够返现能够拜访资源了。
4、演示能够向 token 的 claim 中增加值。
5、演示 userInfo 是须要 user.userInfo 权限的。

七、残缺代码

1、受权服务器,为上篇文章中的受权服务器
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server
2、资源服务器
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/resource-server

八、参考文档

1、https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver

正文完
 0