SpringSecurity最新学习,spring-security-oauth2-authorization-server【spring-security-oauth2升级】

视频学习地址: https://www.bilibili.com/video/BV1Wy411B7xK


这篇文章主要是简单的进行一个SpringSecurity的入门,基于它提供一个客户端的oauth2的认证,包括JWT和Opaque,也可以看成是spring-security-oauth2的升级,因为spring-security-oauth2已不再维护了。


效果展示

在这里插入图片描述
在这里插入图片描述


认证规范有很多,这里我用的是客户端认证


一、理论


假如最终目的是请求 /hello 接口,那基于oauth2的访问流程看起来就是这样的

在这里插入图片描述


JWT和Opaque

简单理解成 Jwt是公开的,直接在线就可以解析看到里面的数据(但不能修改里面的数据,所以它是安全的),Opaque一个不透明的token,在看起来就是一个字符串


二、服务端


2-1、依赖引入


<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.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.4.1</version>
</dependency>

SpringBoot的版本用的是 2.7.17


2-2、一个简单的OpaqueToken生成


配置文件添加

对应源码目录: com.xdx97.config.oauth1

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;

import java.time.Duration;
import java.util.UUID;

/**
 * oauth2服务器配置
 */
@Configuration
public class OpaqueTokenSimpleConfig {


    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {


        http.apply(new OAuth2AuthorizationServerConfigurer());

        http.csrf().disable()
                .authorizeHttpRequests(authorizeRequests ->
                    authorizeRequests
                    .antMatchers("/oauth2/*").permitAll()
                    .antMatchers("/introspect/*").permitAll()
                    .anyRequest().authenticated()
                )
                .formLogin();

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {


        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("xdx97")
                .clientSecret("{noop}xdx97")
                .clientAuthenticationMethods(authMethods -> {
                    authMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
                })
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientSettings(ClientSettings.builder()
                    .requireAuthorizationConsent(true)
                    .requireProofKey(false)
                    .build())
                .tokenSettings(TokenSettings.builder()
                    .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                    .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                    .accessTokenTimeToLive(Duration.ofHours(1))
                    .reuseRefreshTokens(true)
                    .build())
                .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
}

测试请求

curl --location --request POST 'http://127.0.0.1:12345/oauth2/token?client_id=xdx97&client_secret=xdx97&grant_type=client_credentials'

在这里插入图片描述


2-3、一个简单的JwtToken生成


配置文件添加

对应源码目录: com.xdx97.config.oauth2

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

@Configuration
public class JwtTokenSimpleConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

        http.apply(new OAuth2AuthorizationServerConfigurer());
        http.csrf().disable()
                .authorizeHttpRequests(authorizeRequests ->
                    authorizeRequests
                    .antMatchers("/oauth2/*").permitAll()
                    .antMatchers("/introspect/*").permitAll()
                    .antMatchers("/issuer/*").permitAll()
                    .anyRequest().authenticated()
                )
                .formLogin();

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {


        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("xdx97")
                .clientSecret("{noop}xdx97")
                .clientAuthenticationMethods(authMethods -> {
                    authMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
                })
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 必须要配置一个重定向地址
                .redirectUri("xxxxxxx")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(false).build())
                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(1)).build())
                .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
}

测试请求

curl --location --request POST 'http://127.0.0.1:12345/oauth2/token?client_id=xdx97&client_secret=xdx97&grant_type=client_credentials'

在这里插入图片描述


因为Jwt是明文的,可以解析出来看看

在这里插入图片描述


三、资源端


测试用的HelloController

@RestController
public class HelloController {

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

3-1、依赖引入

<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.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.14</version>
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>oauth2-oidc-sdk</artifactId>
    <version>9.14</version>
</dependency>

3-2、简单OpaqueToken校验


配置文件添加

对应源码目录: com.xdx97.config.oauth1 (和上面 authService对应)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;


@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OpaqueResourceSimpleConfig {


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf().disable()
        .authorizeRequests(authorizeRequests ->
            authorizeRequests.anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2ResourceServer ->
            oauth2ResourceServer.opaqueToken(opaqueToken ->
                    opaqueToken.introspector(opaqueTokenIntrospector())
            )
        );

        return http.build();
    }

    public NimbusOpaqueTokenIntrospector opaqueTokenIntrospector() {
        return new NimbusOpaqueTokenIntrospector(
                "http://localhost:12345/oauth2/introspect",
                "xdx97",
                "xdx97"
        );
    }
}

测试

  1. token 取自上一个接口
  2. token要加一个前缀
curl --location --request GET 'http://127.0.0.1:12346/hello' \
--header 'Authorization: Bearer 4KtwpET1hhzMKghkK7Z9oUwcULewPIRF5mgy7cI7GZBRKuEW12NewAJ2YwAxJfT1tpNLWRui_jELQo_S48YlWLIdVXyggt2y27DbsdbpQxpHPgr5f5dTFXFWGzKKbLJR'

在这里插入图片描述


如果一直访问401,可以看看下面 N-2


3-3、简单Jwt校验


配置文件添加

对应源码目录: com.xdx97.config.oauth1 (和上面 authService对应)

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import javax.annotation.Resource;
import java.time.Duration;

@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSimpleConfig {


    @Resource
    private RestTemplateBuilder restTemplateBuilder;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf().disable()
            .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
            .oauth2ResourceServer()
            .jwt()
            .decoder(jwtDecoder(restTemplateBuilder));
        return http.build();
    }

    public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
        // 授权服务器 jwk 的信息
        NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://127.0.0.1:12345/oauth2/jwks")
                // 设置获取 jwk 信息的超时时间
                .restOperations(
                    builder.setReadTimeout(Duration.ofSeconds(3))
                            .setConnectTimeout(Duration.ofSeconds(3))
                            .build()
                )
                .build();
        // 对jwt进行校验
        decoder.setJwtValidator(JwtValidators.createDefault());
        return decoder;
    }
}

测试,和3-2一模一样的


四、进一步体验


上面已经简单使用了SpringSecurity的oauth2客户端认证功能,但还存在一些问题,下面来一一学习


4-1、关于 scope


SpringSecurity认证可以区分更细粒度,比如我们把 /hello 开头的接口定义为, hello资源,只有拥有这个资源的权限才可以访问,而不单单只是有个token


改造Controller

@RestController
@PreAuthorize("hasAuthority('SCOPE_hello')")
public class HelloController {

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

旧版本使用 @PreAuthorize(“#oauth2.hasScope(‘hello’)”)


服务端改造

在这里插入图片描述
在这里插入图片描述


获取token的时候也要加上scope
在这里插入图片描述


在早期的SpringSecurity Oauth2 校验scope是不需要带 SCOPE_ 前缀的,在新版中如果你想去除也可以

private JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
    // 去掉 SCOPE_ 的前缀
    authoritiesConverter.setAuthorityPrefix("");
    converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
    return converter;
}

在这里插入图片描述


4-2、Opaque 认证的client_id、client_secret 处理


在上面演示的时候,有一段代码是,这是去资源端请求认证服务解析 token

public NimbusOpaqueTokenIntrospector opaqueTokenIntrospector() {
    return new NimbusOpaqueTokenIntrospector(
            "http://localhost:12345/oauth2/introspect",
            "xdx97",
            "xdx97"
    );
}

如果我们的资源要被多个不同的客户端访问,该怎么办呢?上面的 clientId 和clientSecret 写死了

目前我有一个办法就是,写一个 filter,这两个参数要么让用户传递过来,要么从数据库中取(可以在生成token的时候,把token存到Redis、 id和secret作为值)

  1. 写一个Filter(就是Java中的Filter)获取到了 id、secret后就存在ThreadLocal中
  2. 重写 OpaqueTokenIntrospector ,从上下文中获取id、secret

也可以使用Jwt,Jwt因为是明文的所以不需要id和secret


OpaqueTokenIntrospector 代码如下, Filter自己实现这个很简单

public class CustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final String introspectionUri;

    public CustomOpaqueTokenIntrospector(String introspectionUri) {
        this.introspectionUri = introspectionUri;
    }
    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        // TODO 改为从 ThreadLocal 中获取
        String clientId = "";
        String clientSecret ="";

        System.out.println(clientId);
        System.out.println(clientSecret);

        clientId = clientId == null ? "xdx97" : clientId;
        clientSecret = clientSecret == null ? "xdx97" : clientSecret;

        NimbusOpaqueTokenIntrospector delegate = new NimbusOpaqueTokenIntrospector(
                introspectionUri,
                clientId,
                clientSecret
        );

        return delegate.introspect(token);
    }
}

完整目录在 resource-service: com.xdx97.config.oauth3


4-3、自定义获取token


默认获取token是在 header中,还要拼接一个前缀 Bearer, 假如想改为从 url中获取 access_token, 只需要重写BearerTokenResolver

public class CustomUriBearerTokenResolver implements BearerTokenResolver {

    private static final String BEARER_TOKEN_PARAM = "access_token"; // URI 中 Token 的参数名

    @Override
    public String resolve(HttpServletRequest request) {
        // 从 URI 参数中获取 Token
        String token = request.getParameter(BEARER_TOKEN_PARAM);
        return (token != null && !token.isEmpty()) ? token : null;
    }
}

在这里插入图片描述


4-4、自定义个性参数 (重要)


上面的流程完成了授权和鉴权,但我们拿不到有用的参数,何为有用的参数比如: 用户的 userId
不管是Opaque还是Jwt都可以在里面设置一些我们自己的参数——把参数放到 【claims】


Opaque

@Bean
public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> tokenCustomizer() {
    return context -> {

        OAuth2TokenClaimsSet.Builder claims = context.getClaims();
        // 将权限信息放入jwt的claims中,这里可以注入一个 Mapper去查询数据库 (可以从 context中拿到 client_id)
        claims.claim("companyId","自定义参数");
    };
}

Jwt

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
    return context -> {
        
        JwtClaimsSet.Builder claims = context.getClaims();
        // 将权限信息放入jwt的claims中,这里可以注入一个 Mapper去查询数据库 (可以从 context中拿到 client_id)
        claims.claim("companyId", "自定义参数");
    };
}

在这里插入图片描述


这个参数也很好拿到,直接存到上下文了,资源端从上下文就可以获取到
在这里插入图片描述


4-5、自定义注册RegisteredClient


InMemoryRegisteredClientRepository 支持List集合,可以把数据库的全部的客户端都查询出来构造一个List RegisteredClient丢进去


N、其它


N-1、oauth2/token 接口404


使用 post请求


N-2、使用Opaque访问 401 问题排查


按照上述配置应该不会 401,但防止配错了需要排除问题,我自己断点调试,发现 resourceService,请求 authService的HTTP如下

import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

public class IntrospectionTest {


    public static  void main(String[] args) {
        String introspectionEndpoint = "http://localhost:12345/oauth2/introspect";
        String clientId = "xdx97";
        String clientSecret = "xdx97";
        String token = "7LdsIQlDLI7c2Ugw9yAAEbitc97SmZDhCobLEsQbq3HiJJ1GJgA8VpNvCgcfXhF6GmzcWaViicKjZPNOdznSufTP9OBlLXjXE806O9yT7nZyfEdnt6FvYIx9ttpOZRX1";

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setBasicAuth(clientId, clientSecret);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("token", token);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        ResponseEntity<String> response = restTemplate.exchange(introspectionEndpoint, HttpMethod.POST, request, String.class);

        System.out.println(response.getBody());
    }
}

通过 active 来判断是否校验通过

{
    "active": true,
    "sub": "xdx97",
    "aud": [
        "xdx97"
    ],
    "nbf": 1719137941,
    "iss": "http://127.0.0.1:12345",
    "exp": 1719141541,
    "iat": 1719137941,
    "jti": "f75bb022-fa4b-453b-9d3f-8576465a1cb0",
    "client_id": "xdx97",
    "token_type": "Bearer"
}

N-3、Opaque服务器重启和Jwt服务器重启会不会丢失


不透明token,如果重启服务端会丢失,Jwt则不会


N-4、建议


如果不熟悉的小伙伴,建议不要修改代码的内容,直接复制运行,运行成功后再去修改


N-5、源码


关注公众号:小道仙97
回复关键字:SpringSecurityDemo

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值