-->
侧边栏壁纸
博主头像
断钩鱼 博主等级

行动起来,活在当下

  • 累计撰写 28 篇文章
  • 累计创建 34 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

SpringAuhtorizationServer

halt
2022-01-05 / 0 评论 / 0 点赞 / 3267 阅读 / 0 字

Spring Authorization Server

由于Spring Security OAuth2.0 被弃用,并且宣布Security将不再提供授权服务器, 但是springcloudoauth2.0还没有标记,经过社区的反馈,提供了一个由Spring官方主导,社区驱动的授权服务 spring-authorization-server,目前版本是 0.2.1,目前没有太多的文档只能参考提供的示例

SpirngAuthorizationServer 时基于 OAuth2.1 协议的 其中弃用了OAuth2.0 的用户名密码模式,并且支持 OpenID Connect 1.0 ,目前的阶段是正在向推进对 OpenID Connect 1.0 的支持

  • OAuth2.0 的概念

    https://baike.baidu.com/item/OAuth2.0/6788617

  • JWT、JWS、JWK 的概念

    JWT:指的是 JSON Web Token,由 header.payload.signture 组成。不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的。
    JWS:指的是签过名的JWT,即拥有签名的JWT。
    JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。

代码地址: https://e.coding.net/duangouyu/demo/SpringAuthorizationServer.git

搭建 SpringAuthorizationServer

pom.xml

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>top.mengshuo</groupId>
    <artifactId>oauth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth-server</name>
    <description>oauth-server</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</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-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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.2.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

spring:
  application:
    name: authorization-server
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/authorization_server?serverTimezone=GMT%2B8&characterEncoding=UTF-8
    username: root
    password: root
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML
    encoding: UTF-8
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false
management:
  endpoints:
    web:
      exposure:
        include: '*'

找到sql并执行

一共三个表:

oauth2_authorization:这个表存放客户端请求授权的 code 码信息 等

oauth2_authorization_consent:这个表存放用户对于客户端的授权信息,授予了那些客户端那些权限,如果时不需要用户二次确认的客户端不会再次存放信息

oauth2_registered_client:oauth2 客户端信息

sql位置

添加认证服务器核心配置

package top.mengshuo.core.config.oauth2;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
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.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
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.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.core.context.SecurityContextHolder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.ObjectUtils;
import top.mengshuo.common.constants.OAuth2Scopes;
import top.mengshuo.common.utils.RsaUtils;

import javax.annotation.Resource;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Set;

/**
 * AuthorizationServer 配置类
 *
 * @author mengshuo
 * @since 2021-12-27
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {

    @Resource
    @Lazy
    private OAuth2AuthorizationConsentService auth2AuthorizationConsentService;


    /**
     * 初始化oauth2 默认配置 <br/>
     * 使用 @Import(OAuth2AuthorizationServerConfiguration.class) 也可以
     *
     * @param http security配置
     * @return res
     * @throws Exception ex
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 设置token扩展
        // 这里如果是有 openid scope 的时候 会调用两次
        // 第一次 access_token OAuth2TokenType.ACCESS_TOKEN 第二次 id_token OidcParameterNames.ID_TOKEN
        // 可以通过 context.getTokenType() 来获取token类型,分别做不同的定制
        http.setSharedObject(OAuth2TokenCustomizer.class, (OAuth2TokenCustomizer<JwtEncodingContext>) context -> {
            // 获取请求认证的信息
            OAuth2ClientAuthenticationToken authenticationToken =
                    ((OAuth2ClientAuthenticationToken) SecurityContextHolder.getContext().getAuthentication());
            if (!ObjectUtils.isEmpty(authenticationToken)) {
                // 获取客户端信息
                RegisteredClient client = authenticationToken.getRegisteredClient();
                // 如果时合法的客户端
                if (!ObjectUtils.isEmpty(client)) {
                    // 获取客户端可申请的权限信息
                    Set<String> clientScopes = client.getScopes();
                    // 获取用户所授予的权限信息
                    OAuth2AuthorizationConsent authority =
                            this.auth2AuthorizationConsentService.findById(client.getId(), String.valueOf(authenticationToken.getPrincipal()));
                    if (!ObjectUtils.isEmpty(authority) && !ObjectUtils.isEmpty(clientScopes)) {
                        // 获取授予的权限
                        Set<String> scopes = authority.getScopes();

                        // 如果是 access_token 并且 用户授权了获取权限信息(这和scope需要提取成常量) 则将权限信息写入 access_token
                        if (scopes.contains(OAuth2Scopes.AUTHORITIES) && OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                            // 写入自定义信息
                            context.getClaims().claim("authority", context.getPrincipal().getAuthorities().toString());
                        }
                    }
                }
            }
        });

        // 设置授权服务器的默认配置 此配置用来拦截匹配 oauth2 默认的一些端点
//        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        // 设置自定义授权端点配置
        authorizationServerConfigurer.authorizationEndpoint(point -> point.consentPage("/consent"));
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        http
                .requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer);

        // 开启form 登录
        return http.formLogin(Customizer.withDefaults()).build();
    }

    /**
     * 默认的security配置 因为上面的 oauth2 已经使用了 .anyRequest() 无法再进行匹配 所以需要新的bean
     *
     * @param http http
     * @return res
     * @throws Exception ex
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

        // 拦截所有请求、启用表单登录并使用默认登录配置
        return http
                // 设置 userDetailsService 用于加载用户信息并进行认证
                .userDetailsService(userDetailsService())
                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .build();
    }


    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.builder()
                .username("mengshuo")
                .password("$2a$10$5b8EIc5Ehoz4oCUqMlK7N.NdIcGzcVLzmk7y8Z3XJ.BvcZ7ZHUuw.")
                .authorities("test:add", "test:del").build();
        return new InMemoryUserDetailsManager(userDetails);
    }

    /**
     * 对客户端信息的持久化 <br/>
     * oauth2_registered_client 对应这个表
     *
     * @param jdbcTemplate 数据库操作
     * @return 存储位置实例
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
//        // 添加一个客户端
//        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
//                .clientId("mengshuo")
//                .clientSecret(passwordEncoder().encode("mengshuo"))
//                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
//                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//                .redirectUri("https://baidu.com")
//                .scope(OidcScopes.OPENID)
//                .scope("authorities")
//                // token 配置
//                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(120)).refreshTokenTimeToLive(Duration.ofMinutes(150)).build())
//                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
//                .build();

        // 客户端信息 crud
        JdbcRegisteredClientRepository repository = new JdbcRegisteredClientRepository(jdbcTemplate);
//        // 设置自定义密码加密器
//        JdbcRegisteredClientRepository.RegisteredClientParametersMapper mapper = new JdbcRegisteredClientRepository.RegisteredClientParametersMapper();
//        // 这里在 0.2.1 版本弃用了 可以在注册客户端时使用密码加密器手动加密密码并保存
//        // 如果这里不设置那么将是明文密码,但是验证时会调用加密器解码所以需要手动加密
//        mapper.setPasswordEncoder(passwordEncoder());
//        // 保存自定义编码器配置
//        repository.setRegisteredClientParametersMapper(mapper);
//        // 添加客户端
//        repository.save(registeredClient);
        return repository;
    }


    /**
     * 对认证信息进行持久化 <br/>
     * 对应 oauth2_authorization 表
     *
     * @param jdbcTemplate               jdbcTemplate
     * @param registeredClientRepository 客户端存储操作
     * @return res
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 对授权信息的持久化 记录了用户的授权信息 <br/>
     * 如果客户端无需用户确认授权的话, <br/>
     * 设置 {@link ClientSettings.Builder#requireAuthorizationConsent(boolean)} 为 false, <br/>
     * 此表不会记录信息 <br/>
     * <p>
     * 对应 oauth2_authorization_consent 表
     *
     * @param jdbcTemplate               jdbcTemplate
     * @param registeredClientRepository 客户端存储操作
     * @return res
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 设置 jwk 密钥 这些密钥可以放在配置文件中
     * 也可以放入配置中心 例如 nacos ,从 nacos 获取配置信息,然后再进行初始化,
     * 同时添加监听,监听到变更后可以通过 {@link org.springframework.cloud.context.refresh.ContextRefresher#refresh} 方法进行动态刷新
     *
     * @return res
     * @throws InvalidKeySpecException ex
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() throws InvalidKeySpecException {
        RSAPublicKey publicKey = (RSAPublicKey) RsaUtils.getPublicKey("""
                -----BEGIN PUBLIC KEY-----
                MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjVn/SelmYCM94bHp2q5
                2EXLO/erHuvEJndfj8G3ryRbSHL42DjOWHzaBudNBEmfnXPc46FVP7FVh75AaL86
                OCwtvza39SSYBRmQ4qAuDd6nKLRnQfpsOa0glsC1jrAlfh3HHw3o86syCSkRDlsS
                a//0M1/PdsHA7B+n24KcPfU1jbLNWIYJcfBcHkuXvQkNKT58XoTOHmdyNlBLOShm
                MebhZOOfJtZwbahAMwmj/Mhd7f8uxCf4DYhl5mjdb79XXribJ90PVqsISeFuCsUn
                w2K/UOlImvdA60tUcWLpcBHdMASJT6hcGhu/np/sU7/rU3LEVAqsESzMG3wzswdV
                2wIDAQAB
                -----END PUBLIC KEY-----
                """);

        RSAPrivateKey privateKey = (RSAPrivateKey) RsaUtils.getPrivateKey("""
                -----BEGIN PRIVATE KEY-----
                MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDONWf9J6WZgIz3
                hsenarnYRcs796se68Qmd1+PwbevJFtIcvjYOM5YfNoG500ESZ+dc9zjoVU/sVWH
                vkBovzo4LC2/Nrf1JJgFGZDioC4N3qcotGdB+mw5rSCWwLWOsCV+HccfDejzqzIJ
                KREOWxJr//QzX892wcDsH6fbgpw99TWNss1Yhglx8FweS5e9CQ0pPnxehM4eZ3I2
                UEs5KGYx5uFk458m1nBtqEAzCaP8yF3t/y7EJ/gNiGXmaN1vv1deuJsn3Q9WqwhJ
                4W4KxSfDYr9Q6Uia90DrS1RxYulwEd0wBIlPqFwaG7+en+xTv+tTcsRUCqwRLMwb
                fDOzB1XbAgMBAAECggEBALahEC9ivMS92JulMBBzcpM8BSpgSJsDHl8fHHCalg80
                +O+qfAAsF3zBXuv8UPa6KfZuVjT4fyMA8QpfEnZy3qI/ZmjSkow307e4k9xTF1bL
                WmqvlEAYBV/zmEhL9sCf+yS+RMuZKzcb6R251FRNjnw+XU5ezcSVB0NZKfY8sBoi
                CU1aIRG3oxCvDoWLnKULTFFAO8pEkw6rJiA0wn8kAbWgilT5R9OAKeQkuDGPJ4xs
                BMyUjZR9ZiVEK/FmY5/I0O88HdkKquOdOF37EQCpX2yP4uU3wkII0Yj89DwXH7To
                D3a6bkp7hA7KxI8aSpB+BApT2VXu6IaBxDu2baYZg4kCgYEA9kRmOr2epyGLgTLK
                PNCIOK45PPisIrRzIRg1lJyebtwRK9GmJGCO0aH7NNNgVKwSvIt0xmK4WysQz8wn
                KYSzSXgf02Hddei+pSpryf4YLffSP9jl8y95YV6VzDVKZdZfpEpPnlOT7piwg36h
                XOeRFVWA6EUFRl96u5zpUp8f91UCgYEA1lu3LpadG30KiRoh/E+ReUXd6hd5Yzrt
                rzFHA5EVJBBj182OzBsE6e+z7GE0GYWg/PnCIAseI4s2XNMyfsZl6mDRC4gXDgd2
                SEQt/M7ejvj7jpQr3CCDMJgHXTE3w3XQLUkKfaTPQ5LGap3rfZihMInbGlBtlcM4
                wQ8M/eTOuG8CgYEA4ZTGEAihy3Zu02oy1oIuRb1RsQgYpbGlxCro6biNZ/8tu3XP
                OoM4T86QzVLSar00bIFR9md3eAt62t2nAeEMWcAvZvG+asNH3wN8uQqIG5NmhBWq
                jZhvF0IM9YHbJG26LkPjqqPkXip/hfP426FCxMgNzLVsn4nWYwTYtVYNcgECgYEA
                yyOxjkIHcxRllpYgXPaeuxm9+ujyDVq8AsmlLbkhGsM5izpvN/fnCTczADEB6anc
                bcDW+fzvO+niw8cV1FR6IspLcS0wBAiITGXoWutEuKM16eF7Sym4iaWZXPSWjvIo
                LbJJcdZs4PHIfSNZFvY80z3hWKedok6Wi0aTHkzmppsCgYADlaigi0Pk+yFsJWg7
                V8FgGOKduez1lqWb993S1Y3HNAZp42IKDC++8ickfPaHCvmktVD6ZwodOcKBCo5J
                enkEG2TQZo/T39Uf2k7/rIWe6PVyb9DkhpLI5MqEuFVnkBLJX8Ug4RNNMYUJz84a
                dtYVz7PRa9t4w3G9oEuV6c+dqQ==
                -----END PRIVATE KEY-----
                """);

        RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID("123456").build();

        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, context) -> jwkSelector.select(jwkSet);
    }

    /**
     * jwt 解码器 暂时没有用到
     *
     * @return 解码器
     * @throws InvalidKeySpecException ex
     */
    @Bean
    public JwtDecoder jwtDecoder() throws InvalidKeySpecException {
        RSAPublicKey publicKey = (RSAPublicKey) RsaUtils.getPublicKey("""
                -----BEGIN PUBLIC KEY-----
                MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjVn/SelmYCM94bHp2q5
                2EXLO/erHuvEJndfj8G3ryRbSHL42DjOWHzaBudNBEmfnXPc46FVP7FVh75AaL86
                OCwtvza39SSYBRmQ4qAuDd6nKLRnQfpsOa0glsC1jrAlfh3HHw3o86syCSkRDlsS
                a//0M1/PdsHA7B+n24KcPfU1jbLNWIYJcfBcHkuXvQkNKT58XoTOHmdyNlBLOShm
                MebhZOOfJtZwbahAMwmj/Mhd7f8uxCf4DYhl5mjdb79XXribJ90PVqsISeFuCsUn
                w2K/UOlImvdA60tUcWLpcBHdMASJT6hcGhu/np/sU7/rU3LEVAqsESzMG3wzswdV
                2wIDAQAB
                -----END PUBLIC KEY-----
                """);
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }

    /**
     * 服务器端点相关配置 可以设置各个接口的url <br/>
     * 默认端点 {@link org.springframework.security.oauth2.server.authorization.config.ProviderSettings#builder()}
     *
     * @return res
     */
    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().tokenEndpoint("/oauth2/token").issuer("http://127.0.0.1:8080").build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();
    }

}

自定义授权页面 (可选)

添加controller

因为需要用户的一些信息

package top.mengshuo.authorization.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
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.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import top.mengshuo.authorization.entity.User;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author mengshuo
 * @since 2021-12-31
 */
@Controller
public class AuthController {

    @Resource
    private OAuth2AuthorizationConsentService consentService;

    @Resource
    private RegisteredClientRepository registeredClientRepository;

    /**
     * 授权确认页面
     * 授权码颁发流程 {@link OAuth2AuthorizationEndpointFilter#doFilterInternal} 
     *
     * @param state     会话标识
     * @param clientId  客户端id
     * @param model     model
     * @param reqScopes 客户端想要获取的权限名称
     * @return res
     */
    @GetMapping("/consent")
    public String consent(String state, @RequestParam("client_id") String clientId, Model model, @RequestParam("scope") String reqScopes) {

        // 获取认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        org.springframework.security.core.userdetails.User principal =
                (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
        // 获取用户信息 模拟从数据库获取
        User userInfo = new User("mengshuo","","halt");
        // 获取客户端信息
        RegisteredClient client = this.registeredClientRepository.findByClientId(clientId);
        if (client == null) {
            throw new RuntimeException("client_id 不合法");
        }
        // 获取已经授予的权限
        OAuth2AuthorizationConsent consent = this.consentService.findById(client.getId(), principal.getUsername());

        // 设置用户信息
        model.addAttribute("nickname", userInfo.getNickname());
        // 设置客户端名称
        model.addAttribute("clientName", client.getClientName());
        // 当用户已经授权过
        if (consent != null) {
            // 设置已经授予的权限
            Set<String> authScope = consent.getScopes();
            model.addAttribute("authScope", authScope);
            // 设置请求的数据
            // 格式化scopes 并去除已经授予的权限
            List<String> reqScope = Arrays.stream(reqScopes.split(" ")).filter(s -> !authScope.contains(s)).collect(Collectors.toList());
            model.addAttribute("reqScope", reqScope);
        } else {
            // 格式化scope并保存 此时客户端没有拿到任何授权
            model.addAttribute("reqScope", reqScopes.split(" "));
        }

        model.addAttribute("state", state);
        model.addAttribute("clinetId", clientId);

        return "consent";
    }
}

添加页面

<!DOCTYPE html>
<html lang="en" class="">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
          integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <title>Consent required</title>
    <link type="text/css" rel="stylesheet" charset="UTF-8"
          href="https://translate.googleapis.com/translate_static/css/translateelement.css">
</head>
<body>
<div class="container">
    <div class="py-5"><h1 class="text-center">确认您的授权</h1></div>
    <div class="row">
        <div class="col text-center">
            <p>
                <span class="font-weight-bold" th:text="${nickname}"></span> 您好,应用:
                <span class="font-weight-bold text-primary" th:text="${clientName}"></span>
                向您获取以下权限
            </p>
        </div>
    </div>
    <div class="row pb-3">
        <div class="col text-center"><p>请您确认是否授权</p></div>
    </div>
    <div class="row">
        <div class="col text-center">
            <form method="post" action="./oauth2/authorize">
                <input type="hidden" name="client_id" th:value="${clinetId}">
                <input type="hidden" name="state" th:value="${state}">

                <div th:each="scope: ${reqScope}">
                    <!--  openid 默认授予 -->
                    <div th:if="${scope eq 'openid' }" class="form-group form-check py-1">
                        <input type="hidden" name="openid" value="openid"/>
                    </div>
                    <div th:unless="${scope eq 'openid' }" class="form-group form-check py-1">
                        <input class="form-check-input" type="checkbox" name="scope" th:value="${scope}"
                               th:id="${scope}"/>
                        <label class="form-check-label" th:for="${scope}" th:text="${scope}"></label>
                    </div>
                </div>
                <div th:if="${authScope != null && authScope.size() > 0}">
                    <p>
                        以下是您已经授予
                        <span th:text="${clientName}" class="font-weight-bold text-primary"></span>
                        的权限:
                    </p>
                    <div th:each="scope: ${authScope}">
                        <div class="form-group form-check py-1">
                            <input class="form-check-input" type="checkbox" name="scope" th:value="${scope}"
                                   th:id="${scope}" checked disabled/>
                            <label class="form-check-label" th:for="${scope}" th:text="${scope}"></label>
                        </div>
                    </div>
                </div>
                <div class="form-group pt-3">
                    <button class="btn btn-primary btn-lg" type="submit">确认授权</button>
                </div>
                <div class="form-group">
                    <button class="btn btn-link regular" type="reset">取消</button>
                </div>
            </form>
        </div>
    </div>
    <div class="row pt-4">
        <div class="col text-center"><p><small>需要您的同意才能提供访问权限<br>如果您不批准,
            单击“取消”,在这种情况下,不会与应用共享任何信息。</small></p></div>
    </div>
</div>
<div class="goog-te-spinner-pos">
    <div class="goog-te-spinner-animation">
        <svg xmlns="http://www.w3.org/2000/svg" class="goog-te-spinner" width="96px" height="96px" viewBox="0 0 66 66">
            <circle class="goog-te-spinner-path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33"
                    r="30"></circle>
        </svg>
    </div>
</div>
</body>
</html>

测试

测试授权码类型

  1. 访问 http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=mengshuo&redirect_uri=https://baidu.com&scope=openid%20authority
  2. 用code换取token: http://127.0.0.1:8080/oauth2/token?grant_type=authorization_code&code=
  3. 刷新token: http://127.0.0.1:8080/oauth2/token?grant_type=refresh_token&refresh_token=

换取token和刷新token都需要添加请求头 Authorization : Basic Base64编码的 clientId:secret

确认授权

授权码

换取token

资源服务器

pom.xml

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>top.mengshuo</groupId>
    <artifactId>oauth-resource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth-resource</name>
    <description>oauth-resource</description>
    <properties>
        <java.version>17</java.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.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

核心配置

package top.mengshuo.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;

/**
 * @author mengshuo
 * @since 2021-12-23
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class ResourcesServerConfiguration extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭 csrf
                .csrf(CsrfConfigurer::disable)
                // 关闭登录
                .formLogin(FormLoginConfigurer::disable)
                // 关闭session创建
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 需要认证的接口
                .authorizeRequests(authReq -> authReq.antMatchers("/v1/**").authenticated())
                .authorizeRequests(authReq->authReq.anyRequest().permitAll())
                // 启用jwt令牌的支持
                .oauth2ResourceServer(
                        resourceServer ->
                                resourceServer.jwt()
                                        .jwtAuthenticationConverter(converter())
                );

    }

    /**
     * 认证信息转换器 因为
     *
     * @return UsernamePasswordAuthenticationToken
     */
    private Converter<Jwt, ? extends AbstractAuthenticationToken> converter() {
        return source -> {
            // 获取 jwt token 中的权限信息
            String authority = source.getClaimAsString("authority");
            // 解析 jwt token 中的权限信息
            String[] authorityArray = authority.substring(1).substring(0, authority.length() - 2).trim().replaceAll(" ", "").split(",");
            // 创建并返回一个 username password 认证信息类
            return new UsernamePasswordAuthenticationToken(source.getClaimAsString("sub"), null, AuthorityUtils.createAuthorityList(authorityArray));
        };
    }

    /**
     * 构建jwt解码器
     *
     * @return jwt解码器
     */
    @Bean
    public JwtDecoder jwtDecoder() {
        RSAPublicKey publicKey = null;
        try {
            publicKey = (RSAPublicKey) RsaUtils.getPublicKey("""
                    -----BEGIN PUBLIC KEY-----
                    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjVn/SelmYCM94bHp2q5
                    2EXLO/erHuvEJndfj8G3ryRbSHL42DjOWHzaBudNBEmfnXPc46FVP7FVh75AaL86
                    OCwtvza39SSYBRmQ4qAuDd6nKLRnQfpsOa0glsC1jrAlfh3HHw3o86syCSkRDlsS
                    a//0M1/PdsHA7B+n24KcPfU1jbLNWIYJcfBcHkuXvQkNKT58XoTOHmdyNlBLOShm
                    MebhZOOfJtZwbahAMwmj/Mhd7f8uxCf4DYhl5mjdb79XXribJ90PVqsISeFuCsUn
                    w2K/UOlImvdA60tUcWLpcBHdMASJT6hcGhu/np/sU7/rU3LEVAqsESzMG3wzswdV
                    2wIDAQAB
                    -----END PUBLIC KEY-----
                    """);
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }

        return NimbusJwtDecoder.withPublicKey(publicKey).build();
//        return NimbusJwtDecoder.withJwkSetUri(oAuth2ResourceServerProperties.getJwt().getJwkSetUri()).build();
    }
}

Controller

package top.mengshuo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author mengshuo
 * @since 2021-12-27
 */
@RestController
public class TestController {
    @PreAuthorize("hasAuthority('test:add')")
    @GetMapping("/v1/add")
    public String add() {
        return "add";
    }

    @PreAuthorize("hasAuthority('test:del')")
    @GetMapping("/v1/del")
    public String del() {

        return "del";
    }

    @GetMapping("/test")
    public String test() {

        return "test";
    }
}

测试

  • 通过上面拿到了 token来访问资源服务器

    添加请求头 Authorization : Bearer token

    访问测试

0
博主关闭了所有页面的评论