IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> (一)学习spring-cloud:2021之spring-authorization-server -> 正文阅读

[Java知识库](一)学习spring-cloud:2021之spring-authorization-server

1. 前言

1.1?为什么使用spring-authorization-server?

????????真实原因:原先是因为个人原因,需要研究新版鉴权服务,看到了spring-authorization-server,使用过程中,想着能不能整合新版本cloud,因此此处先以springboot搭建spring-authorization-server,后续再替换为springcloud。

? ? ? ? 官方原因:原先使用Spring Security OAuth,而该项目已经逐渐被淘汰,虽然网上还是有不少该方案,但秉着技术要随时代更新,从而使用spring-authorization-server

2.项目迭代历程

  1. 以springboot搭建spring-authorization-server(即认证与资源服务器)

  2. 升级为springcloud2021.0.x框架,引入nacos作为注册中心

  3. 引入sentinel,进行限流降级熔断配置

  4. 引入nacos配置中心

  5. 升级sentinel,配合nacos,使项目能动态配置落地所有Feign的降级配置

  6. 引入gateway网关,swagger文档工具

  7. 待续

3.项目构建

????????

3.1 以springboot搭建spring-authorization-server(即认证与资源服务器)

? ? ? ? 先进行认证服务器相关配置

? ? ? ? pom.xml引入依赖

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

<!-- 此依赖是个人公共依赖,你们引入其他具体依赖即可 -->
<dependency>
    <groupId>com.xxxx.iov</groupId>
    <artifactId>iov-cloud-framework-web</artifactId>
    <version>2.0.0-SNAPSHOT</version>
    <exclusions>
        <!-- 这里是因为公共依赖中的web版本太低,所以移除 -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </exclusion>
    </exclusions>
</dependency>

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

<!-- hutool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.0</version>
</dependency>

<!-- fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.39</version>
</dependency>

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

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

<!-- security-cas -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>

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

<!-- 数据连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.9</version>
</dependency>

<!-- 数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

<!-- guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

? ? ? ? 创建自定义登录页面 login.html (可不要,使用自带的登录界面)

<!DOCTYPE html>
<html lang="en"
      xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="utf-8">
    <meta name="author" content="test">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="description" content="This is a login page template based on Bootstrap 5">
    <title>Login Page</title>
    <style>
        .is-invalid {
            color: red;
        }

        .invalid-feedback {
            color: red;
        }

        .mb-3 {
            margin-bottom: 3px;
        }
    </style>
    <script th:inline="javascript">
        /*<![CDATA[*/
        // const baseURL = /*[[@{/}]]*/ ''; /*]]>*/
        if (window !== top) {
            top.location.href = location.href;
        }
    </script>
</head>
<body class="hold-transition login-page">
<div class="login-box">
    <div class="card">
        <div class="card-body login-card-body">
            <p class="login-box-msg">Sign in to start your session</p>
            <div th:if="${param.error}" class="alert alert-error">
                Invalid username and password.
            </div>
            <div th:if="${param.logout}" class="alert alert-success">
                You have been logged out.
            </div>
            <form th:action="@{/login}" method="post" id="loginForm">
                <div class="input-group mb-3">
                    <input type="text" class="form-control" value="zxg" name="username" placeholder="Email"
                           autocomplete="off">
                </div>
                <div class="input-group mb-3">
                    <input type="password" id="password" name="password" value="123" class="form-control"
                           maxlength="25" placeholder="Password"
                           autocomplete="off">
                </div>
                <div class="row">
                    <div class="col-4">
                        <button type="submit" id="submitBtn">Sign In</button>
                    </div>
                </div>
            </form>
            <p class="mb-1">
                <a href="javascript:void(0)">I forgot my password</a>
            </p>
            <p class="mb-0">
                <a href="javascript:void(0)" class="text-center">Register a new membership</a>
            </p>
        </div>
    </div>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js"></script>

<script th:inline="javascript">

    $(function () {
        var encrypt = new JSEncrypt();

        $.validator.setDefaults({
            submitHandler: function (form) {
                console.log("Form successful submitted!");
                form.submit();
            }
        });

    });
</script>
</body>
</html>

创建自定义授权页面 consent.html(可不要,可使用自带的授权页面)

<!DOCTYPE html>
<html lang="en">
<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>授权页面</title>
    <style>
        body {
            background-color: aliceblue;
        }
    </style>
   <script>
      function cancelConsent() {
         document.consent_form.reset();
         document.consent_form.submit();
      }
   </script>
</head>
<body>
<div class="container">
    <div class="py-5">
        <h1 class="text-center text-primary">用户授权确认</h1>
    </div>
    <div class="row">
        <div class="col text-center">
            <p>
                应用
                <a href="https://felord.cn"><span class="font-weight-bold text-primary" th:text="${clientName}"></span></a>
                想要访问您的账号
                <span class="font-weight-bold" th:text="${principalName}"></span>
            </p>
        </div>
    </div>
    <div class="row pb-3">
        <div class="col text-center"><p>上述应用程序请求以下权限<br/>请审阅以下选项并勾选您同意的权限</p></div>
    </div>
    <div class="row">
        <div class="col text-center">
            <form name="consent_form" method="post" action="/oauth2/authorize">
                <input type="hidden" name="client_id" th:value="${clientId}">
                <input type="hidden" name="state" th:value="${state}">

                <div th:each="scope: ${scopes}" class="form-group form-check py-1">
                    <input class="form-check-input"
                           type="checkbox"
                           name="scope"
                           th:value="${scope.scope}"
                           th:id="${scope.scope}">
                    <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
                    <p class="text-primary" th:text="${scope.description}"></p>
                </div>

                <p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已对上述应用授予以下权限:</p>
                <div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
                    <input class="form-check-input"
                           type="checkbox"
                           th:id="${scope.scope}"
                           disabled
                           checked>
                    <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
                    <p class="text-primary" th:text="${scope.description}"></p>
                </div>

                <div class="form-group pt-3">
                    <button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
                        同意授权
                    </button>
                </div>
                <div class="form-group">
                    <button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
                        取消授权
                    </button>
                </div>
            </form>
        </div>
    </div>
    <div class="row pt-4">
        <div class="col text-center">
            <p>
                <small>
                    需要您同意并提供访问权限。
                    <br/>如果您不同意,请单击<span class="font-weight-bold text-primary">取消授权</span>,将不会为上述应用程序提供任何您的信息。
                </small>
            </p>
        </div>
    </div>
</div>
</body>
</html>

? ? ? ? 修改配置文件?application.yml(配置内容可自行简略)

server:
  port: 9000

spring:
  application:
    name: authorization-server
  thymeleaf:
    cache: false
  datasource:
    url: jdbc:mysql://192.168.1.69:3306/test
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:9000  #认证中心端点,作为资源端的配置
          
application:
  security:
    excludeUrls: #excludeUrls中存放白名单地址
      - "/favicon.ico" 

# mybatis plus配置
mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  global-config:
    # 关闭MP3.0自带的banner
    banner: false
    db-config:
      #主键类型  0:"数据库ID自增", 1:"不操作", 2:"用户输入ID",3:"数字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake";
      id-type: AUTO
      #字段策略
      insert-strategy: not_null
      update-strategy: not_null
      select-strategy: not_null
      #驼峰下划线w转换
      table-underline: true
      # 逻辑删除配置
      # 逻辑删除全局值(1表示已删除,这也是Mybatis Plus的默认配置)
      logic-delete-value: 1
      # 逻辑未删除全局值(0表示未删除,这也是Mybatis Plus的默认配置)
      logic-not-delete-value: 0
  configuration:
    #驼峰
    map-underscore-to-camel-case: true
    #打开二级缓存
    cache-enabled: true
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志

????????新增认证服务器配置文件 AuthorizationServerConfig

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    /**
     * 自定义授权页面
     * 使用系统自带的即不用
     */
    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    /**
     * 自定义UserDetailsService
     */
    @Autowired
    private UserService userService;


    /**
     *
     * 使用默认配置进行form表单登录
     * OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();

        authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));

        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        http
                .requestMatcher(endpointsMatcher)
                .userDetailsService(userService)
                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer);
        return http.formLogin(Customizer.withDefaults()).build();
    }

    /**
     * 注册客户端应用
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        // Save registered client in db as if in-jdbc
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("zxg")
                .clientSecret("123")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 回调地址
                .redirectUri("http://www.baidu.com")
                // scope自定义的客户端范围
                .scope(OidcScopes.OPENID)
                .scope("message.read")
                .scope("message.write")
                // client请求访问时需要授权同意
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                // token配置项信息
                .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();

        // Save registered client in db as if in-memory
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(registeredClient);
        return registeredClientRepository;
    }

    /**
     * 授权服务:管理OAuth2授权信息服务
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

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

    /**
     * 加载JWK资源
     * JWT:指的是 JSON Web Token,不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的
     * JWS:指的是签过名的JWT,即拥有签名的JWT
     * JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = JwksUtils.generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    /**
     * 配置 OAuth2.0 提供者元信息
     */
    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
    }

}

????????新增Security的配置文件WebSecurityConfig

@Configuration
@EnableWebSecurity(debug = true) //开启Security
public class WebSecurityConfig {
    @Autowired
    private ApplicationProperties properties;

    /**
     * 设置加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
//        // 将密码加密方式采用委托方式,默认以BCryptPasswordEncoder方式进行加密,兼容ldap,MD4,MD5等方式
//        return PasswordEncoderFactories.createDelegatingPasswordEncoder();

        // 此处我们使用明文方式 不建议这样
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 使用WebSecurity.ignoring()忽略某些URL请求,这些请求将被Spring Security忽略
     */
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return new WebSecurityCustomizer() {
            @Override
            public void customize(WebSecurity web) {
                // 读取配置文件application.security.excludeUrls下的链接进行忽略
                web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(new String[]{}));
            }
        };
    }

    /**
     * 针对http请求,进行拦截过滤
     *
     * CookieCsrfTokenRepository进行CSRF保护的工作方式:
     *      1.客户端向服务器发出GET请求,例如请求主页
     *      2.Spring发送 GET 请求的响应以及 Set-cookie 标头,其中包含安全生成的XSRF令牌
     */
    @Bean
    public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.antMatchers("/login").permitAll()
                                .anyRequest().authenticated()
                )

                //使用默认登录页面
                //.formLogin(withDefaults())

                //设置form登录,设置且放开登录页login
                .formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll())

                // Spring Security CSRF保护
                .csrf(csrfToken -> csrfToken.csrfTokenRepository(new CookieCsrfTokenRepository()))
                
//                 //开启认证服务器的资源服务器相关功能,即需校验token
//                .oauth2ResourceServer()
//                .accessDeniedHandler(new SimpleAccessDeniedHandler())
//                .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
//                .jwt()
        ;
        return httpSecurity.build();
    }

}

????????新增读取application配置的类 ApplicationProperties

/**
* 此步主要是获取配置文件中配置的白名单,可自行舍去或自定义实现其他方式
**/
@Data
@Component
@ConfigurationProperties("application")
public class ApplicationProperties {
    private final Security security = new Security();

    @Data
    public static class Security {
        private Oauth2 oauth2;
        private List<String> excludeUrls = new ArrayList<>();

        @Data
        public static class Oauth2 {
            private String issuerUrl;

        }
    }
}

????????新增 JwksUtils 类和 KeyGeneratorUtils,这两个类作为JWT对称加密

public final class JwksUtils {

    private JwksUtils() {
    }

    /**
     * 生成RSA加密key (即JWK)
     */
    public static RSAKey generateRsa() {
        // 生成RSA加密的key
        KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
        // 公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 构建RSA加密key
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    /**
     * 生成EC加密key (即JWK)
     */
    public static ECKey generateEc() {
        // 生成EC加密的key
        KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
        // 公钥
        ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
        // 私钥
        ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
        // 根据公钥参数生成曲线
        Curve curve = Curve.forECParameterSpec(publicKey.getParams());
        // 构建EC加密key
        return new ECKey.Builder(curve, publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    /**
     * 生成HmacSha256密钥
     */
    public static OctetSequenceKey generateSecret() {
        SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
        return new OctetSequenceKey.Builder(secretKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }
}


class KeyGeneratorUtils {

    private KeyGeneratorUtils() {
    }

    /**
     * 生成RSA密钥
     */
    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;
    }

    /**
     * 生成EC密钥
     */
    static KeyPair generateEcKey() {
        EllipticCurve ellipticCurve = new EllipticCurve(
                new ECFieldFp(
                        new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
                new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
                new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
        ECPoint ecPoint = new ECPoint(
                new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
                new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
        ECParameterSpec ecParameterSpec = new ECParameterSpec(
                ellipticCurve,
                ecPoint,
                new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
                1);

        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
            keyPairGenerator.initialize(ecParameterSpec);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * 生成HmacSha256密钥
     */
    static SecretKey generateSecretKey() {
        SecretKey hmacKey;
        try {
            hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return hmacKey;
    }
}

? ? ? ??新建 ConsentController,编写登录和认证页面的跳转

? ? ? ? 如果在上面没有使用自定义的登录和授权页面,下面的跳转方法按需舍去

@Slf4j
@Controller
public class ConsentController {

    private final RegisteredClientRepository registeredClientRepository;
    private final OAuth2AuthorizationConsentService authorizationConsentService;

    public ConsentController(RegisteredClientRepository registeredClientRepository,
                             OAuth2AuthorizationConsentService authorizationConsentService) {
        this.registeredClientRepository = registeredClientRepository;
        this.authorizationConsentService = authorizationConsentService;
    }

    @ResponseBody
    @GetMapping("/favicon.ico")
    public String faviconico(){
        return "favicon.ico";
    }

    @GetMapping("/login")
    public String loginPage(){
        return "login";
    }

    @GetMapping(value = "/oauth2/consent")
    public String consent(Principal principal, Model model,
                          @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
                          @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
                          @RequestParam(OAuth2ParameterNames.STATE) String state) {

        // Remove scopes that were already approved
        Set<String> scopesToApprove = new HashSet<>();
        Set<String> previouslyApprovedScopes = new HashSet<>();
        RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
        OAuth2AuthorizationConsent currentAuthorizationConsent =
                this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
        Set<String> authorizedScopes;
        if (currentAuthorizationConsent != null) {
            authorizedScopes = currentAuthorizationConsent.getScopes();
        } else {
            authorizedScopes = Collections.emptySet();
        }
        for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
            if (authorizedScopes.contains(requestedScope)) {
                previouslyApprovedScopes.add(requestedScope);
            } else {
                scopesToApprove.add(requestedScope);
            }
        }

        model.addAttribute("clientId", clientId);
        model.addAttribute("state", state);
        model.addAttribute("scopes", withDescription(scopesToApprove));
        model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
        model.addAttribute("principalName", principal.getName());

        return "consent";
    }

    private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
        Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
        for (String scope : scopes) {
            scopeWithDescriptions.add(new ScopeWithDescription(scope));

        }
        return scopeWithDescriptions;
    }

    public static class ScopeWithDescription {
        private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
        private static final Map<String, String> scopeDescriptions = new HashMap<>();
        static {
            scopeDescriptions.put(
                    "message.read",
                    "This application will be able to read your message."
            );
            scopeDescriptions.put(
                    "message.write",
                    "This application will be able to add new messages. It will also be able to edit and delete existing messages."
            );
            scopeDescriptions.put(
                    "other.scope",
                    "This is another scope example of a scope description."
            );
        }

        public final String scope;
        public final String description;

        ScopeWithDescription(String scope) {
            this.scope = scope;
            this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
        }
    }

}

? ? ? ??新建 UserController,User,UserService等标准的自定义用户业务,此处仅放出UserServiceImpl

@RequiredArgsConstructor
@Slf4j
@Component
class UserServiceImpl implements UserService {
    private final UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername,username));
        return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>());
    }
}

????????启动项目,如下图

????????认证服务器整体结构图

----------------------------------------------------------------------------------------------------------------------------------------------------------

????????资源服务器相关配置

????????pom.xml引入资源服务器相关依赖

<!-- resource-server资源服务器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

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

????????新增配置文件 application.yaml

server:
  port: 9003
spring:
  application:
    name: resource
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:9000
feign:
  client:
    config:
      default: #配置超时时间
        connect-timeout: 10000
        read-timeout: 10000

????????新增资源服务器配置文件 ResourceServerConfiguration

@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启鉴权服务
public class ResourceServerConfiguration {

    @Bean
    public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 所有请求都进行拦截
        httpSecurity.authorizeRequests().anyRequest().authenticated();
        // 关闭session
        httpSecurity.sessionManagement().disable();
        // 配置资源服务器的无权限,无认证拦截器等 以及JWT验证
        httpSecurity.oauth2ResourceServer()
                .accessDeniedHandler(new SimpleAccessDeniedHandler())
                .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                .jwt();
        return httpSecurity.build();
    }

}

????????新增相关无认证无权限统一拦截回复 SimpleAccessDeniedHandler 和 SimpleAuthenticationEntryPoint

/**
 * 携带了token 而且token合法 但是权限不足以访问其请求的资源 403
 * @author zxg
 */
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed("无权访问"));
        PrintWriter printWriter = response.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}


/**
 * 在资源服务器中 不携带token 或者token无效  401
 * @author zxg
 */
@Slf4j
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if (response.isCommitted()){
            return;
        }

        Throwable throwable = authException.fillInStackTrace();

        String errorMessage = "认证失败";

        if (throwable instanceof BadCredentialsException){
            errorMessage = "错误的客户端信息";
        }else {
            Throwable cause = authException.getCause();

            if (cause instanceof JwtValidationException) {
                log.warn("JWT Token 过期,具体内容:" + cause.getMessage());
                errorMessage = "无效的token信息";
            } else if (cause instanceof BadJwtException){
                log.warn("JWT 签名异常,具体内容:" + cause.getMessage());
                errorMessage = "无效的token信息";
            } else if (cause instanceof AccountExpiredException){
                errorMessage = "账户已过期";
            } else if (cause instanceof LockedException){
                errorMessage = "账户已被锁定";
//            } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){
//                response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"无效的客户端")));
//            } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){
//                response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("无效的类型")));
//            } else if (cause instanceof UnauthorizedClientException) {
//                response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未经授权的客户端")));
            } else if (throwable instanceof InsufficientAuthenticationException) {
                String message = throwable.getMessage();
                if (message.contains("Invalid token does not contain resource id")){
                    errorMessage = "未经授权的资源服务器";
                }else if (message.contains("Full authentication is required to access this resource")){
                    errorMessage = "缺少验证信息";
                }
            }else {
                errorMessage = "验证异常";
            }
        }

        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage));
        PrintWriter printWriter = response.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}

????????新增 ResourceController 进行接口测试

@Slf4j
@RestController
public class ResourceController {

    /**
     * 测试Spring Authorization Server,测试权限
     */
    @PreAuthorize("hasAuthority('SCOPE_message.read')")
    @GetMapping("/getTest")
    public String getTest(){
        return "getTest";
    }

    /**
     * 默认登录成功跳转页为 /  防止404状态
     *
     * @return the map
     */
    @GetMapping("/")
    public Map<String, String> index() {
        return Collections.singletonMap("msg", "login success!");
    }

    @GetMapping("/getResourceTest")
    public SingleResultBundle<String> getResourceTest(){
        return SingleResultBundle.success("这是resource的测试方法 getResourceTest()");
    }
}

????????启动项目,效果如下

????????项目总体结构如下

????????测试认证鉴权

#调用 /oauth2/authorize ,获取code
http://127.0.0.1:9000/oauth2/authorize?client_id=zxg&response_type=code&scope=message.read&redirect_uri=http://www.baidu.com
#会判断是否登录,若没有,则跳转到登录页面,如下图1
#登录完成后,会提示是否授权,若没有,则跳转到授权界面,如下图2
#授权成功后,跳转到回调地址,并带上code,如图3

????????打开postman,进行获取access_token

#访问 /oauth2/token 地址
#在Authorization中选择Basic Auth模式,填入对应客户端,其会在header中生成Authorization,如下图右侧

? ? ? ? 返回结果如下

????????调用ResourceController中的接口,测试token是否生效

总结

至此,spring-authorization-server的基础使用已完成,总体上和原Spring Security OAuth大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。由于不太会写文章,我就直接贴出代码,代码中我有加上注释,所以上述文章中,没有很具体的描述,基本就是个人开发流程,若文中有那里写不对,欢迎指教,不喜勿喷。

关于升级springcloud2021.0.x部分,请查看后续相关文章

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-06-01 15:02:20  更:2022-06-01 15:02:29 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 20:47:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码