一、背景

Spring Security 5中,当初曾经不提供了 受权服务器 的配置,然而 受权服务器 在咱们平时的开发过程中用的还是比拟多的。不过 Spring 官网提供了一个 由Spring官网主导,社区驱动的受权服务 spring-authorization-server,目前曾经到了 0.1.2 的版本,不过该我的项目还是一个实验性的我的项目,不可在生产环境中应用,此处来应用我的项目搭建一个简略的受权服务器。

二、前置常识

1、理解 oauth2 协定、流程。能够参考阮一峰的这篇文章
2、JWT、JWS、JWK的概念

JWT:指的是 JSON Web Token,由 header.payload.signture 组成。不存在签名的JWT是不平安的,存在签名的JWT是不可篡改的。
JWS:指的是签过名的JWT,即领有签名的JWT。
JWK:既然波及到签名,就波及到签名算法,对称加密还是非对称加密,那么就须要加密的 密钥或者公私钥对。此处咱们将 JWT的密钥或者公私钥对对立称为 JSON WEB KEY,即 JWK。

三、需要

1、 实现受权码(authorization-code)流程。

最平安的流程,须要用户的参加。

2、 实现客户端(client credentials)流程。

没有用户的参加,个别能够用于外部零碎之间的拜访,或者零碎间不须要用户的参加。

3、简化模式在新的 spring-authorization-server 我的项目中曾经被弃用了。
4、刷新令牌。
5、撤销令牌。
6、查看颁发的某个token信息。
7、查看JWK信息。

实现案例:
张三通过QQ登录的形式来登录CSDN网站。
登录后,CSDN就能够获取到QQ颁发的token,CSDN网站拿着token就能够获取张三在QQ资源服务器上的 个人信息 了。

角色剖析
张三: 用户即资源拥有者
CSDN:客户端
QQ:受权服务器
个人信息: 即用户的资源,保留在资源服务器中

四、外围代码编写

1、引入受权服务器依赖

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

2、创立受权服务器用户

张三通过QQ登录的形式来登录CSDN网站。

此处实现用户张三的创立,这个张三是受权服务器的用户,此处即QQ服务器的用户。

@EnableWebSecuritypublic class DefaultSecurityConfig {    @Bean    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {        http                .authorizeRequests(authorizeRequests ->                        authorizeRequests.anyRequest().authenticated()                )                .formLogin();        return http.build();    }    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    // 此处创立用户,张三。    @Bean    UserDetailsService users() {        UserDetails user = User.builder()                .username("zhangsan")                .password(passwordEncoder().encode("zhangsan123"))                .roles("USER")                .build();        return new InMemoryUserDetailsManager(user);    }}

3、创立受权服务器和客户端

张三通过QQ登录的形式来登录CSDN网站。

此处实现QQ受权服务器和客户端CSDN的创立。

package com.huan.study.authorization.config;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.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.Ordered;import org.springframework.core.annotation.Order;import org.springframework.jdbc.core.JdbcTemplate;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.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.core.AuthorizationGrantType;import org.springframework.security.oauth2.core.ClientAuthenticationMethod;import org.springframework.security.oauth2.jwt.JwtDecoder;import org.springframework.security.oauth2.server.authorization.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.ProviderSettings;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.util.matcher.RequestMatcher;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.NoSuchAlgorithmException;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.time.Duration;import java.util.UUID;/** * 认证服务器配置 * * @author huan.fu 2021/7/12 - 下午2:08 */@Configurationpublic class AuthorizationConfig {    @Autowired    private PasswordEncoder passwordEncoder;    /**     * 定义 Spring Security 的拦截器链     */    @Bean    @Order(Ordered.HIGHEST_PRECEDENCE)    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {        // 受权服务器配置        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =                new OAuth2AuthorizationServerConfigurer<>();        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();        return http                .requestMatcher(endpointsMatcher)                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))                .apply(authorizationServerConfigurer)                .and()                .formLogin()                .and()                .build();    }    /**     * 创立客户端信息,能够保留在内存和数据库,此处保留在数据库中     */    @Bean    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())                // 客户端id 须要惟一                .clientId("csdn")                // 客户端明码                .clientSecret(passwordEncoder.encode("csdn123"))                // 能够基于 basic 的形式和受权服务器进行认证                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)                // 受权码                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)                // 刷新token                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)                // 客户端模式                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)                // 明码模式                .authorizationGrantType(AuthorizationGrantType.PASSWORD)                // 简化模式,已过期,不举荐                .authorizationGrantType(AuthorizationGrantType.IMPLICIT)                // 重定向url                .redirectUri("https://www.baidu.com")                // 客户端申请的作用域,也能够了解这个客户端申请拜访用户的哪些信息,比方:获取用户信息,获取用户照片等                .scope("user.userInfo")                .scope("user.photos")                .clientSettings(clientSettings -> {                    // 是否须要用户确认一下客户端须要获取用户的哪些权限                    // 比方:客户端须要获取用户的 用户信息、用户照片 然而此处用户能够管制只给客户端受权获取 用户信息。                    clientSettings.requireUserConsent(true);                })                .tokenSettings(tokenSettings -> {                    // accessToken 的有效期                    tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));                    // refreshToken 的有效期                    tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));                    // 是否可重用刷新令牌                    tokenSettings.reuseRefreshTokens(true);                })                .build();        JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);        if (null == jdbcRegisteredClientRepository.findByClientId("messaging-client")) {            jdbcRegisteredClientRepository.save(registeredClient);        }        return jdbcRegisteredClientRepository;    }    /**     * 保留受权信息,受权服务器给咱们颁发来token,那咱们必定须要保留吧,由这个服务来保留     */    @Bean    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {        JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);        class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {            public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {                super(registeredClientRepository);                getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);                this.setLobHandler(new DefaultLobHandler());            }        }        CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper =                new CustomOAuth2AuthorizationRowMapper(registeredClientRepository);        authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper);        return authorizationService;    }    /**     * 如果是受权码的流程,可能客户端申请了多个权限,比方:获取用户信息,批改用户信息,此Service解决的是用户给这个客户端哪些权限,比方只给获取用户信息的权限     */    @Bean    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);    }    /**     * 对JWT进行签名的 加解密密钥     */    @Bean    public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");        keyPairGenerator.initialize(2048);        KeyPair keyPair = keyPairGenerator.generateKeyPair();        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 (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);    }    /**     * jwt 解码     */    @Bean    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);    }    /**     * 配置一些断点的门路,比方:获取token、受权端点 等     */    @Bean    public ProviderSettings providerSettings() {        return new ProviderSettings()                // 配置获取token的端点门路                .tokenEndpoint("/oauth2/token")                // 发布者的url地址,个别是本零碎拜访的根门路                // 此处的 qq.com 须要批改咱们零碎的 host 文件                .issuer("http://qq.com:8080");    }}

留神⚠️:
1、须要将 qq.com 在零碎的 host 文件中与 127.0.0.1 映射起来。
2、因为客户端信息、受权信息(token信息等)保留到数据库,因而须要将表建好。

3、详细信息看上方代码的正文

五、测试

从上方的代码中可知:

资源所有者:张三 用户名和明码为:zhangsan/zhangsan123
客户端信息:CSDN clientId和clientSecret:csdn/csdn123
受权服务器地址: qq.com
clientSecret 的值不可透露给客户端,必须保留在服务器端。

1、受权码流程

1、获取受权码

http://qq.com:8080/oauth2/aut... user.userInfo

client_id=csdn:示意客户端是谁
response_type=code:示意返回受权码
scope=user.userInfo user.userInfo:获取多个权限以空格离开
redirect_uri=https://www.baidu.com:跳转申请,用户批准或回绝后

2、依据受权码获取token

 curl -i -X POST \   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ 'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com'

Authorization: 携带具体的 clientId 和 clientSecret 的base64的值
grant_type=authorization_code 示意采纳的形式是受权码
code=xxx:上一步获取到的受权码

3、流程演示

2、依据刷新令牌获取token

curl -i -X POST \   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ 'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN-jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx'

3、客户端模式

此模式下,没有用户的参加,只有客户端和受权服务器之间的参加。

curl -i -X POST \   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ 'http://qq.com:8080/oauth2/token?grant_type=client_credentials'

4、撤销令牌

curl -i -X POST \ 'http://qq.com:8080/oauth2/revoke?token=令牌'

5、查看token 的信息

curl -i -X POST \   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ 'http://qq.com:8080/oauth2/introspect?token=XXX'

6、查看JWK信息

curl -i -X GET \ 'http://qq.com:8080/oauth2/jwks'

六、残缺代码

https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server

七、参考地址

1、https://github.com/spring-projects-experimental/spring-authorization-server