Spring Security Resource Server的使用

一、背景

前一節(jié)我們學(xué)習(xí)了 Spring Authorization Server的使用,此處我們簡(jiǎn)單的記錄下 Spring 資源服務(wù)器的使用。

二、需求

資源服務(wù)器提供2個(gè)資源 ,userInfohello。
userInfo:資源是受保護(hù)的資源,需要user.userInfo權(quán)限才可以訪(fǎng)問(wèn)。
hello:資源是公開(kāi)資源,不要權(quán)限即可訪(fǎng)問(wèn)。

三、分析

1、如何驗(yàn)證資源服務(wù)器中訪(fǎng)問(wèn)的令牌是有效的?

此處只考慮JWT的令牌。

令牌是授權(quán)服務(wù)器頒發(fā)的,且進(jìn)行了簽名操作,因此資源服務(wù)器對(duì)令牌的驗(yàn)證,就需要授權(quán)服務(wù)器的JWK信息,此處可以配置JwtDecoder來(lái)實(shí)現(xiàn),并且填寫(xiě)好jwk set uri。

2、令牌是從請(qǐng)求中那個(gè)地方來(lái)的?
令牌可以從請(qǐng)求的 request header或者request query param中獲取,因此就需要配置 BearerTokenResolver來(lái)實(shí)現(xiàn)。

3、令牌中的權(quán)限字段,默認(rèn)會(huì)加上SCOPE_前綴,想去掉如何操作。
配置JwtAuthenticationConverter對(duì)象。

4、如果像向JWT的claim中增加值如何操作?
通過(guò) JwtDecoder#setClaimSetConverter來(lái)操作。此處也可以實(shí)現(xiàn)刪除claim的中內(nèi)容。

5、如何驗(yàn)證JWT是否合法?
通過(guò) JwtDecoder#setJwtValidator方法來(lái)操作。

6、如何設(shè)置從授權(quán)服務(wù)器獲取JWK的超時(shí)時(shí)間?
通過(guò) JwkSetUriJwtDecoderBuilder#restOperations來(lái)操作。

四、資源服務(wù)器認(rèn)證流程

資源服務(wù)器認(rèn)證流程

1、請(qǐng)求會(huì)被 BearerTokenAuthenticationFilter 攔截器攔截,并從中解析出token出來(lái),如果沒(méi)有解析出來(lái),則由下一個(gè)過(guò)濾器處理。解析出來(lái)則構(gòu)建一個(gè)BearerTokenAuthenticationToken對(duì)象。
2、下一步將HttpServletRequest傳遞給AuthenticationManagerResolver對(duì)象,由它選擇出AuthenticationManager對(duì)象,然后將 BearerTokenAuthenticationToken傳遞給AuthenticationManager對(duì)象進(jìn)行認(rèn)證。AuthenticationManager對(duì)象的實(shí)現(xiàn),取決于我們的token對(duì)象是JWT還是opaque token
3、驗(yàn)證失敗

  1. 清空 SecurityContextHolder 對(duì)象。
  2. 交由AuthenticationFailureHandler對(duì)象處理。

4、驗(yàn)證成功

  1. Authentication對(duì)象設(shè)置到SecurityContextHolder中。
  2. 交由余下的過(guò)濾器繼續(xù)處理。

五、實(shí)現(xiàn)資源服務(wù)器

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、資源服務(wù)器配置

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;

/**
 * 資源服務(wù)器配置
 *
 * @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()
                // 對(duì)于 userInfo 這個(gè)api 需要 s
                .antMatchers("/userInfo").access("hasAuthority('user.userInfo')")
                .and()
                // 設(shè)置session是無(wú)狀態(tài)的
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .oauth2ResourceServer()
                .jwt()
                // 解碼jwt信息
                .decoder(jwtDecoder(restTemplateBuilder))
                // 將jwt信息轉(zhuǎn)換成JwtAuthenticationToken對(duì)象
                .jwtAuthenticationConverter(jwtAuthenticationConverter())
                .and()
                // 從request請(qǐng)求那個(gè)地方中獲取 token
                .bearerTokenResolver(bearerTokenResolver())
                // 此時(shí)是認(rèn)證失敗
                .authenticationEntryPoint((request, response, exception) -> {
                    // oauth2 認(rèn)證失敗導(dǎo)致的,還有一種可能是非oauth2認(rèn)證失敗導(dǎo)致的,比如沒(méi)有傳遞token,但是訪(fǎng)問(wèn)受權(quán)限保護(hù)的方法
                    if (exception instanceof OAuth2AuthenticationException) {
                        OAuth2AuthenticationException oAuth2AuthenticationException = (OAuth2AuthenticationException) exception;
                        OAuth2Error error = oAuth2AuthenticationException.getError();
                        log.info("認(rèn)證失敗,異常類(lèi)型:[{}],異常:[{}]", exception.getClass().getName(), error);
                    }
                    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                    response.setContentType(MediaType.APPLICATION_JSON.toString());
                    response.getWriter().write("{\"code\":-3,\"message\":\"您無(wú)權(quán)限訪(fǎng)問(wèn)\"}");
                })
                // 認(rèn)證成功后,無(wú)權(quán)限訪(fǎng)問(wèn)
                .accessDeniedHandler((request, response, exception) -> {
                    log.info("您無(wú)權(quán)限訪(fǎng)問(wèn),異常類(lèi)型:[{}]", exception.getClass().getName());
                    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                    response.setContentType(MediaType.APPLICATION_JSON.toString());
                    response.getWriter().write("{\"code\":-4,\"message\":\"您無(wú)權(quán)限訪(fǎng)問(wèn)\"}");
                })
        ;
    }

    /**
     * 從request請(qǐng)求中那個(gè)地方獲取到token
     */
    private BearerTokenResolver bearerTokenResolver() {
        DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
        // 設(shè)置請(qǐng)求頭的參數(shù),即從這個(gè)請(qǐng)求頭中獲取到token
        bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.AUTHORIZATION);
        bearerTokenResolver.setAllowFormEncodedBodyParameter(false);
        // 是否可以從uri請(qǐng)求參數(shù)中獲取token
        bearerTokenResolver.setAllowUriQueryParameter(false);
        return bearerTokenResolver;
    }

    /**
     * 從 JWT 的 scope 中獲取的權(quán)限 取消 SCOPE_ 的前綴
     * 設(shè)置從 jwt claim 中那個(gè)字段獲取權(quán)限
     * 如果需要同多個(gè)字段中獲取權(quán)限或者是通過(guò)url請(qǐng)求獲取的權(quán)限,則需要自己提供jwtAuthenticationConverter()這個(gè)方法的實(shí)現(xiàn)
     *
     * @return JwtAuthenticationConverter
     */
    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 去掉 SCOPE_ 的前綴
        authoritiesConverter.setAuthorityPrefix("");
        // 從jwt claim 中那個(gè)字段獲取權(quán)限,模式是從 scope 或 scp 字段中獲取
        authoritiesConverter.setAuthoritiesClaimName("scope");
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }

    /**
     * jwt 的解碼器
     *
     * @return JwtDecoder
     */
    public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
        // 授權(quán)服務(wù)器 jwk 的信息
        NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://qq.com:8080/oauth2/jwks")
                // 設(shè)置獲取 jwk 信息的超時(shí)時(shí)間
                .restOperations(
                        builder.setReadTimeout(Duration.ofSeconds(3))
                                .setConnectTimeout(Duration.ofSeconds(3))
                                .build()
                )
                .build();
        // 對(duì)jwt進(jìn)行校驗(yàn)
        decoder.setJwtValidator(JwtValidators.createDefault());
        // 對(duì) jwt 的 claim 中增加值
        decoder.setClaimSetConverter(
                MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("為claim中增加key", custom -> "值"))
        );
        return decoder;
    }
}

此處對(duì)資源服務(wù)器進(jìn)行了很多的自定義操作,因此配置比較長(zhǎng)。
資源服務(wù)器的session需要是無(wú)狀態(tài)的

3、資源

1個(gè)受保護(hù)的資源和一個(gè)非受保護(hù)的資源。

@RestController
public class UserController {

    /**
     * 這個(gè)是受保護(hù)的資源,需要 user.userInfo 權(quán)限才可以訪(fǎng)問(wèn)。
     */
    @GetMapping("userInfo")
    public Map<String, Object> userInfo(@AuthenticationPrincipal Jwt principal) {
        return new HashMap<String, Object>(4) {{
            put("principal", principal);
            put("userInfo", "獲取用戶(hù)信息");
        }};
    }

    /**
     * 非受權(quán)限保護(hù)的資源
     */
    @GetMapping("hello")
    public String hello() {
        return "hello 不要需要受保護(hù)的資源";
    }
}

六、測(cè)試

1、訪(fǎng)問(wèn)非受保護(hù)的資源

訪(fǎng)問(wèn)非受保護(hù)的資源

可以看到不需要token即可以訪(fǎng)問(wèn)。

2、訪(fǎng)問(wèn)受保護(hù)的資源

訪(fǎng)問(wèn)受保護(hù)的資源

1、先不用token訪(fǎng)問(wèn),可以看到是拒絕的。
2、然后通過(guò)授權(quán)服務(wù)器生成一個(gè)token,授權(quán)服務(wù)器為上一篇文章使用的授權(quán)服務(wù)器。
3、通過(guò)token訪(fǎng)問(wèn)后,可以返現(xiàn)可以訪(fǎng)問(wèn)資源了。
4、演示可以向token的claim中增加值。
5、演示 userInfo 是需要user.userInfo權(quán)限的。

七、完整代碼

1、授權(quán)服務(wù)器,為上篇文章中的授權(quán)服務(wù)器
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server
2、資源服務(wù)器
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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容