spring-security-oauth2-authorization-server

旧依赖的移除

长久以来,使用Spring Security整合oauth2,都是使用Spring Security Oauth2这个系列的包:

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>${spring.security.oauth2.version}</version>
</dependency>

然而,这个包现在已经被Spring官方移除了,现在实现相同的功能主要使用这几个Maven依赖:

<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>

可以看出除了Spring Security核心依赖,只是多了一个资源服务器的依赖。而之前使用的认证服务器,变成了一个新的项目,依赖如下:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.3.1</version>
</dependency>

这个资源服务器的依赖只到了0.3.1的版本号,还不到1.0.0,所以也算是一个新项目。不过我尝试了下,基本上已经可以用作生产环境了,只是这个包对于token持久化的支持,只支持内存储存和SQL储存,暂时并未提供之前常用的redis持久化,不知道这是不是Spring官方针对OAuth2这么多年混乱的标准作出的回应,统一使用JWT作为token确实是不需要服务端对token进行存储了。但是这里完全可以使用常规的Opaque token进行认证,需要实现RegisteredClientRepository、OAuth2AuthorizationService和OAuth2AuthorizationConsentService这几个类。

资源服务器的配置

对于资源服务器,主要作用是提供一些用户登录后需要的资源。调用接口就可以得到这些资源了。

Maven依赖

必要的Maven依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>Code-Resources</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Code-Resources</name>
    <description>Code-Resources</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.6</spring-boot.version>
    </properties>

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.6</version>
                <configuration>
                    <mainClass>com.example.code.resources.CodeResourcesApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

安全配置类

只需要对所有的接口开启认证:

package com.example.code.resources.config;

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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests().anyRequest().authenticated()
                .and().cors()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().oauth2ResourceServer().jwt();
        return httpSecurity.build();
    }
}

由于前后端分离,所以将服务端的session策略设置为无状态。

配置文件

jwt模式

配置文件需要指定认证服务器的地址和认证的方式,在jwt模式下,资源服务器会请求认证服务器的/oauth2/jwks端点,拿到公钥以后对jwt进行验证。

server:
  port: 9600
spring:
  datasource:
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth_demo
  application:
    name: Code-Resources
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:9500
  jackson:
    default-property-inclusion: non_null

opaquetoken模式

也可以选择opaquetoken这种常规的redis验证方式。如果选用Opaque token模式,相对应的端点就是/oauth2/introspect。

资源提供

写一个controller,要求一定的权限。对于这个权限信息,在生成的access_token,即jwt的playload部分里本身是包含的,而资源服务器在校验权限时,会通过网络请求认证服务器获取认证服务器那边的公钥:

public Resource retrieveResource(URL url) throws IOException {
    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
    ResponseEntity<String> response = this.getResponse(url, headers);
        if (response.getStatusCodeValue() != 200) {
            throw new IOException(response.toString());
            } else {
                return new Resource((String)response.getBody(), "UTF-8");
            }
}

private ResponseEntity<String> getResponse(URL url, HttpHeaders headers) throws IOException {
     try {
         RequestEntity<Void> request = new RequestEntity(headers, HttpMethod.GET, url.toURI());
             return this.restOperations.exchange(request, String.class);
         } catch (Exception var4) {
             throw new IOException(var4);
         }
}

公钥可以鉴别jwt的playload部分有没有被篡改过。

package com.example.code.resources.controller;

import com.example.code.resources.entity.ClientEntity;
import com.example.code.resources.entity.TokenEntity;
import com.example.code.resources.entity.UserClientEntity;
import com.example.code.resources.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@RestController
public class UserController {

    JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @RequestMapping("/getResources")
    @CrossOrigin
    @PreAuthorize("hasAuthority('SCOPE_message.read')")
    public Map<String, Object> getResources() {
        HashMap<String, Object> map = new HashMap<>();
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        String userSql = "select * from oauth2_user where username = ?";
        UserEntity user = jdbcTemplate.queryForObject(userSql, new BeanPropertyRowMapper<>(UserEntity.class), username);
        String queryClientSql = "select * from oauth2_authorization_consent where principal_name = ?";
        UserClientEntity userClientEntity = jdbcTemplate.queryForObject(queryClientSql, new BeanPropertyRowMapper<>(UserClientEntity.class), username);
        String clientSql = "select * from oauth2_registered_client where id = ?";
        Optional<UserClientEntity> userClientEntityOptional = Optional.ofNullable(userClientEntity);
        if (userClientEntityOptional.isPresent()) {
            ClientEntity clientEntity = jdbcTemplate.queryForObject(clientSql, new BeanPropertyRowMapper<>(ClientEntity.class), userClientEntityOptional.get().getRegisteredClientId());
            map.put("clientInfo", clientEntity);
        }
        String tokenSql = "select * from oauth2_authorization where access_token_value = ?";
        Jwt jwt = (Jwt) authentication.getCredentials();
        String token = jwt.getTokenValue();
        TokenEntity tokenEntity = jdbcTemplate.queryForObject(tokenSql, new BeanPropertyRowMapper<>(TokenEntity.class), token);
        map.put("tokenInfo", tokenEntity);
        map.put("userInfo", user);
        return map;
    }

}

认证服务器的配置

认证服务器主要负责access_token的生成、refresh_token的生成和通过refresh_token换取access_token的逻辑。在jwt下,access_token一旦签发就无法管理,即便使用refresh_token换取了新的access_token,那旧的access_token仍然是可用的。

Maven配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>code</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>code</name>
    <description>code</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <!--spring-authorization-server依赖-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
            <version>0.3.1</version>
        </dependency>

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.2</version>
                <configuration>
                    <mainClass>com.example.code.authorization.CodeAuthorizationApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

主要的依赖只需要一个认证服务器,不需要Spring Security的核心包。

跨域配置

一般架构使用前后端分离,所以设置跨域:

package com.example.code.authorization.config;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomerCorsFilter extends org.springframework.web.filter.CorsFilter {
    public CustomerCorsFilter() {
        super(configurationSource());
    }

    private static UrlBasedCorsConfigurationSource configurationSource() {
        CorsConfiguration corsConfig = new CorsConfiguration();
        List<String> allowedHeaders = Arrays.asList("x-auth-token", "content-type", "X-Requested-With", "XMLHttpRequest","Access-Control-Allow-Origin","Authorization","authorization");
        List<String> exposedHeaders = Arrays.asList("x-auth-token", "content-type", "X-Requested-With", "XMLHttpRequest","Access-Control-Allow-Origin","Authorization","authorization");
        List<String> allowedMethods = Arrays.asList("POST", "GET", "DELETE", "PUT", "OPTIONS");
        List<String> allowedOrigins = List.of("*");
        corsConfig.setAllowedHeaders(allowedHeaders);
        corsConfig.setAllowedMethods(allowedMethods);
        corsConfig.setAllowedOriginPatterns(allowedOrigins);
        corsConfig.setExposedHeaders(exposedHeaders);
        corsConfig.setMaxAge(36000L);
        corsConfig.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);
        return source;
    }
}

配置文件

不需要额外的配置。

server:
  port: 9500

spring:
  application:
    name: Code-Authorization
  datasource:
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth_demo

认证服务器配置类

对于配置文件,如果不计划开启oidc协议,那使用open id环绕的代码是可以删除的。为什么使用oidc,本质只是为了实现一种规范,调取/userinfo接口去获取用户的一些登录信息,同时获取id_token来解析一些用户信息。

package com.example.code.authorization.config;

import com.example.code.authorization.entity.UserEntity;
import com.example.code.authorization.service.UserService;
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.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OidcClientRegistrationEndpointConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

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.*;

@Configuration
public class SecurityConfig {


    JdbcTemplate jdbcTemplate;

    UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Bean
    public JwtEncoder jwtEncoder() {
        return new NimbusJwtEncoder(jwkSource());
    }

    @Bean
    public OAuth2TokenGenerator<?> tokenGenerator() {
        JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder());
        jwtGenerator.setJwtCustomizer(jwtCustomizer());
        OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
        OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
        return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            JwsHeader.Builder headers = context.getHeaders();
            JwtClaimsSet.Builder claims = context.getClaims();
            Map<String, Object> map = claims.build().getClaims();
            if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
                // Customize headers/claims for access_token
//                headers.header("customerHeader", "这是一个自定义header");
//                claims.claim("customerClaim", "这是一个自定义Claim");
                String username = (String) map.get("sub");
                String sql = "select avatar, url from oauth_demo.oauth2_user where username = ?";
                UserEntity userEntity = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
                Optional<UserEntity> userEntityOptional = Optional.ofNullable(userEntity);
                if (userEntityOptional.isPresent()) {
                    claims.claim("url", userEntityOptional.get().getUrl());
                    claims.claim("avatar", userEntityOptional.get().getAvatar());
                }
            }
        };
    }

    /**
     * 端点的 Spring Security 过滤器链
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {


        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();

        /*
        open id
         */
        authorizationServerConfigurer
                .oidc(oidc -> {
                            // 用户信息
                            oidc.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userInfoMapper(oidcUserInfoAuthenticationContext -> {
                                String username = oidcUserInfoAuthenticationContext.getAuthorization().getPrincipalName();
                                String sql = "select url from oauth_demo.oauth2_user where username = ?";
                                UserEntity userEntity = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
                                Optional<UserEntity> userEntityOptional = Optional.ofNullable(userEntity);
                                Map<String, Object> claims  = new HashMap<>();
                                if (userEntityOptional.isPresent()) {
                                    claims.put("url", userEntity.getUrl());
                                }
                                claims.put("sub", username);
                                return new OidcUserInfo(claims);
                            }));
                            // 客户端注册
                            oidc.clientRegistrationEndpoint(Customizer.withDefaults());
                        }
                );

        http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        /*
        open id
         */

        RequestMatcher endpointsMatcher = authorizationServerConfigurer
                .getEndpointsMatcher();

        http
                .requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .userDetailsService(userService)
                .csrf(AbstractHttpConfigurer::disable)
                .apply(authorizationServerConfigurer);


        //未通过身份验证时重定向到登录页面授权端点
        http.exceptionHandling((exceptions) -> exceptions
                .authenticationEntryPoint(
                        new LoginUrlAuthenticationEntryPoint("/login"))
        );

        return http.build();
    }

    /**
     * 用于身份验证的 Spring Security 过滤器链
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                //表单登录处理从授权服务器过滤器链
                .formLogin(Customizer.withDefaults());

        return http.build();
    }


    /**
     * 返回注册客户端资源,注意这里采用的是内存模式,后续可以改成jdbc模式。RegisteredClientRepository用于管理客户端的实例。
     * @return
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
//        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
//                .clientId("messaging-client")
//                .clientSecret(passwordEncoder().encode("secret"))
//                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
//                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//                .redirectUri("http://www.baidu.com")
//                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
//                .scope(OidcScopes.OPENID)
//                .scope("message.read")
//                .scope("message.write")
//                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
//                .tokenSettings(TokenSettings.builder()
//                        // token有效期100分钟
//                        .accessTokenTimeToLive(Duration.ofMinutes(100L))
//                        // 使用默认JWT相关格式
//                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
//                        // 开启刷新token
//                        .reuseRefreshTokens(true)
//                        // refreshToken有效期120分钟
//                        .refreshTokenTimeToLive(Duration.ofMinutes(120L))
//                        .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
//                )
//                .build();

        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
//        RegisteredClient client = registeredClientRepository.findByClientId("messaging-client");
//        Optional<RegisteredClient> clientOptional = Optional.ofNullable(client);
//        if (clientOptional.isEmpty()) {
//            registeredClientRepository.save(registeredClient);
//        }
        return registeredClientRepository;
    }

    @Bean
    public OAuth2AuthorizationService authorizationService() {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository());
    }

    /**
     * 授权确认信息处理服务
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService() {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository());
    }


    /**
     * 生成jwk资源,com.nimbusds.jose.jwk.source.JWKSource用于签署访问令牌的实例。
     * @return
     */
    @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);
    }

    /**
     * 生成密钥对,启动时生成的带有密钥的实例java.security.KeyPair用于创建JWKSource上述内容
     * @return
     */
    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;
    }

    /**
     * ProviderSettings配置 Spring Authorization Server的实例
     * @return
     */
    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().build();
    }
}

UserService

用户登录时使用:

package com.example.code.authorization.service;

import com.example.code.authorization.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Optional;

@Component
public class UserService implements UserDetailsService {

    JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String sql = "select id, username, password from oauth_demo.oauth2_user where username = ?";
        UserEntity user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
        Optional<UserEntity> userOptional = Optional.ofNullable(user);
        if (userOptional.isEmpty()) {
            throw new UsernameNotFoundException("user is not exist");
        }
        return new User(userOptional.get().getUsername(), userOptional.get().getPassword(), true,true,true,true, Collections.emptyList());
    }
}

关于OIDC

OIDC是在OAuth2协议基础上的一个认证层,通过使用access_token调用/userinfo接口获取一些用户信息。这个/userinfo接口只能给open_id的scope请求调用,其他scope的请求是无法调用这个接口的。同时,生成access_token请求的响应相对于普通的响应,会多出一个id_token,这个id_token可以用于解析身份信息:

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
    return context -> {
        JwsHeader.Builder headers = context.getHeaders();
        JwtClaimsSet.Builder claims = context.getClaims();
        Map<String, Object> map = claims.build().getClaims();
        if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
                // Customize headers/claims for access_token
//              headers.header("customerHeader", "这是一个自定义header");
//              claims.claim("customerClaim", "这是一个自定义Claim");
            String username = (String) map.get("sub");
            String sql = "select avatar, url from oauth_demo.oauth2_user where username = ?";
            UserEntity userEntity = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
            Optional<UserEntity> userEntityOptional = Optional.ofNullable(userEntity);
            if (userEntityOptional.isPresent()) {
                claims.claim("url", userEntityOptional.get().getUrl());
                claims.claim("avatar", userEntityOptional.get().getAvatar());
            }
        }
    };
}

对id_token的解析是直接在前端进行的,这个token不能用于后端接口的权限验证,作用仅仅只是储存一些信息,例如用户性别、头像等信息:

parseJwt(token) {
    let strings = token.split("."); //截取token,获取载体
    this.jwt = JSON.parse(window.atob(strings[1].replace(/-/g, "+").replace(/_/g, "/")))
},
posted @ 2022-09-29 13:49  imissinstagram  Views(7978)  Comments(1Edit  收藏  举报