SpringCloud OAuth2实现单点登录以及OAuth2源码原理解析

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

Spring Security OAuth 是建立在 Spring Security 的基础之上 OAuth2.0 协议实现的一个类库

Spring Security OAuth2 为 Spring Cloud 搭建认证授权服务(能够更好的集成到 Spring Cloud 体系中)

单点登录主要包括

服务端:一个第三方授权中心服务(Server),用于完成用户登录,认证和权限处理
客户端:当用户访问客户端应用的安全页面时,会重定向到授权中心进行身份验证,认证完成后方可访问客户端应用的服务,且多个客户端应用只需要登录一次即可

相关版本:

SpringBoot:2.1.5.RELEASE
SpringCloud :Greenwich.SR1

认证中心Server

1.引入OAuth2依赖和web依赖(不加启动时会报无法访问javax.servlet.Filter)

OAuth2中包含spring-cloud-starter-securityspring-security-oauth2-autoconfigure

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

2.创建验证用户,设置用户名和密码并设置角色权限

@Component
public class SSOUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        String user="user";
        if( !user.equals(s) ) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return new User( s, passwordEncoder.encode("123456"), 
              AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

3.认证服务器配置

①加入@EnableAuthorizationServer注解来启动OAuth2.0授权服务机制
②通过继承AuthorizationServerConfigurerAdapter并且覆写其中的三个configure方法来进行配置
3.1.ClientDetailsServiceConfigurer

用于定义客户详细信息服务的配置器。客户端详情信息进行初始化,能够把客户端详情信息写在内存中或者是通过数据库来存储调取详情信息。

多个客户端来连接Spring OAuth2 Auth Server,需要在配置类里为inMemory生成器定义多个withClients

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 定义了两个客户端应用的通行证
        clients.inMemory()// 使用in-memory存储
                .withClient("ben1")// client_id
                .secret(new BCryptPasswordEncoder().encode("123456"))// client_secret
                .authorizedGrantTypes("authorization_code", "refresh_token")// 该client允许的授权类型
                .scopes("all")// 允许的授权范围
                .autoApprove(false)
                //加上验证回调地址
                .redirectUris("http://localhost:8086/login")
                .and()
                .withClient("ben2")
                .secret(new BCryptPasswordEncoder().encode("123456"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .autoApprove(false)
                .redirectUris("http://localhost:8087/login");
    }

必须设置回调地址redirectUris,并且格式是http://客户端IP:端口/login的格式,否则会报OAuth Error error=”invalid_request”, error_description=”At least one redirect_uri must be registered with the client.”

原理如下图:

ClientDetailsServiceConfigurer原理图.png

ClientDetailsServiceConfiguration根据ClientDetailsServiceConfigurer配置,交给ClientDetailsServiceBuilder的实现类通过ClientBuilder创建Client

ClientDetailsServiceConfigurer 核心源码

public class ClientDetailsServiceConfigurer extends SecurityConfigurerAdapter<ClientDetailsService, ClientDetailsServiceBuilder<?>> {
    public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
        InMemoryClientDetailsServiceBuilder next = ((ClientDetailsServiceBuilder)this.getBuilder()).inMemory();
        this.setBuilder(next);
        return next;
    }

    public JdbcClientDetailsServiceBuilder jdbc(DataSource dataSource) throws Exception {
        JdbcClientDetailsServiceBuilder next = ((ClientDetailsServiceBuilder)this.getBuilder()).jdbc().dataSource(dataSource);
        this.setBuilder(next);
        return next;
    }
    ......
}

ClientDetailsServiceBuilder

ClientBuilderClientDetailsServiceBuilder的一个内部类,其中build()会被ClientDetailsServiceConfiguration所调用

ClientDetailsServiceBuilder部分源码

public class ClientDetailsServiceBuilder<B extends ClientDetailsServiceBuilder<B>> 
              extends SecurityConfigurerAdapter<ClientDetailsService, B> 
              implements SecurityBuilder<ClientDetailsService> {
    private List<ClientDetailsServiceBuilder<B>.ClientBuilder> clientBuilders = new ArrayList();

   //设置Client并把其放到list
    public ClientDetailsServiceBuilder<B>.ClientBuilder withClient(String clientId) {
        ClientDetailsServiceBuilder<B>.ClientBuilder clientBuilder = new ClientDetailsServiceBuilder.ClientBuilder(clientId);
        this.clientBuilders.add(clientBuilder);
        return clientBuilder;
    }

    //创建ClientDetailsService 
    public ClientDetailsService build() throws Exception {
        Iterator var1 = this.clientBuilders.iterator();

        while(var1.hasNext()) {
            ClientDetailsServiceBuilder<B>.ClientBuilder clientDetailsBldr = (ClientDetailsServiceBuilder.ClientBuilder)var1.next();
            this.addClient(clientDetailsBldr.clientId, clientDetailsBldr.build());
        }

        return this.performBuild();
    }
    
    public final class ClientBuilder {
        private final String clientId;
        private Collection<String> authorizedGrantTypes;
        private Collection<String> authorities;
        private Integer accessTokenValiditySeconds;
        private Integer refreshTokenValiditySeconds;
        private Collection<String> scopes;
        private Collection<String> autoApproveScopes;
        private String secret;
        private Set<String> registeredRedirectUris;
        private Set<String> resourceIds;
        private boolean autoApprove;
        private Map<String, Object> additionalInformation;

        private ClientDetails build() {
            BaseClientDetails result = new BaseClientDetails();
            result.setClientId(this.clientId);
            result.setAuthorizedGrantTypes(this.authorizedGrantTypes);
            result.setAccessTokenValiditySeconds(this.accessTokenValiditySeconds);
            result.setRefreshTokenValiditySeconds(this.refreshTokenValiditySeconds);
            result.setRegisteredRedirectUri(this.registeredRedirectUris);
            result.setClientSecret(this.secret);
            result.setScope(this.scopes);
            result.setAuthorities(AuthorityUtils.createAuthorityList((String[])this.authorities.toArray(new String[this.authorities.size()])));
            result.setResourceIds(this.resourceIds);
            result.setAdditionalInformation(this.additionalInformation);
            if (this.autoApprove) {
                result.setAutoApproveScopes(this.scopes);
            } else {
                result.setAutoApproveScopes(this.autoApproveScopes);
            }

            return result;
        }

        private ClientBuilder(String clientId) {
            this.authorizedGrantTypes = new LinkedHashSet();
            this.authorities = new LinkedHashSet();
            this.scopes = new LinkedHashSet();
            this.autoApproveScopes = new HashSet();
            this.registeredRedirectUris = new HashSet();
            this.resourceIds = new HashSet();
            this.additionalInformation = new LinkedHashMap();
            this.clientId = clientId;
        }
        ......
    }
    ......
}

客户端信息配置属性说明:
clientId:(必须的)第三方用户的id(可理解为账号)。
clientSecret:第三方应用和授权服务器之间的安全凭证(可理解为密码)
scope:指定客户端申请的权限范围,可选值包括read,write,trust;其实授权赋予第三方用户可以在资源服务器获取资源,第三方访问资源的一个权限,访问范围。
resourceIds:客户端所能访问的资源id集合
authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
可选值包括authorization_code,password,refresh_token,implicit,client_credentials
最常用的grant_type组合有: "authorization_code,refresh_token"(针对通过浏览器访问的客户端); "password,refresh_token"(针对移动设备的客户端)
registeredRedirectUris:客户端的重定向URI
autoApproveScopes:设置用户是否自动Approval操作, 默认值为 false,
可选值包括 true,false, read,write.
该字段只适用于grant_type="authorization_code的情况,当用户登录成功后,
若该值为true或支持的scope值,则会跳过用户Approve的页面, 直接授权.
authorities:指定客户端所拥有的Spring Security的权限值。
accessTokenValiditySeconds:设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时).
refreshTokenValiditySeconds:设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天).
additionalInformation:这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据

具体可参考:http://andaily.com/spring-oauth-server/db_table_description.html

ClientDetailsServiceConfiguration

ClientDetailsServiceConfiguration 依据配置,由ClientDetailsServiceBuilder创建ClientDetailsService
ClientDetailsServiceConfiguration核心源码

@Configuration
public class ClientDetailsServiceConfiguration {
    private ClientDetailsServiceConfigurer configurer = 
              new ClientDetailsServiceConfigurer(new ClientDetailsServiceBuilder());

    @Bean
    @Lazy
    @Scope(
        proxyMode = ScopedProxyMode.INTERFACES
    )
    public ClientDetailsService clientDetailsService() throws Exception {
        return ((ClientDetailsServiceBuilder)this.configurer.and()).build();
    }
    ......
}

InMemoryClientDetailsServiceBuilderJdbcClientDetailsServiceBuilder均继承于ClientDetailsServiceBuilder,都会重写performBuild(),因为ClientDetailsServiceBuilderbuild()需要调用performBuild()

InMemoryClientDetailsServiceBuilder核心源码

public class InMemoryClientDetailsServiceBuilder 
          extends ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder> {
    private Map<String, ClientDetails> clientDetails = new HashMap();

    protected ClientDetailsService performBuild() {
        InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
        clientDetailsService.setClientDetailsStore(this.clientDetails);
        return clientDetailsService;
    }
    ......
}

JdbcClientDetailsServiceBuilder核心源码

public class JdbcClientDetailsServiceBuilder 
              extends ClientDetailsServiceBuilder<JdbcClientDetailsServiceBuilder> {
    private Set<ClientDetails> clientDetails = new HashSet();
    private DataSource dataSource;
    private PasswordEncoder passwordEncoder;

    protected ClientDetailsService performBuild() {
        Assert.state(this.dataSource != null, "You need to provide a DataSource");
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(this.dataSource);
        if (this.passwordEncoder != null) {
            clientDetailsService.setPasswordEncoder(this.passwordEncoder);
        }

        Iterator var2 = this.clientDetails.iterator();

        while(var2.hasNext()) {
            ClientDetails client = (ClientDetails)var2.next();
            clientDetailsService.addClientDetails(client);
        }

        return clientDetailsService;
    }
    ......
}

同理:创建出的ClientDetailsService也分为InMemoryClientDetailsServiceJdbcClientDetailsService
InMemoryClientDetailsService核心源码

public class InMemoryClientDetailsService implements ClientDetailsService {
    private Map<String, ClientDetails> clientDetailsStore = new HashMap();

    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        ClientDetails details = (ClientDetails)this.clientDetailsStore.get(clientId);
        if (details == null) {
            throw new NoSuchClientException("No client with requested id: " + clientId);
        } else {
            return details;
        }
    }
    ......
}

InMemoryClientDetailsServiceClientDetails存储到Hashmap

JdbcClientDetailsService核心源码

public class JdbcClientDetailsService 
                    implements ClientDetailsService, ClientRegistrationService {
    private String updateClientDetailsSql;
    private String updateClientSecretSql;
    private String insertClientDetailsSql;
    private String selectClientDetailsSql;
    private PasswordEncoder passwordEncoder;
    private final JdbcTemplate jdbcTemplate;
    private JdbcListFactory listFactory;

    public JdbcClientDetailsService(DataSource dataSource) {
        this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT;
        this.updateClientSecretSql = "update oauth_client_details set client_secret = ? where client_id = ?";
        this.insertClientDetailsSql = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
        this.selectClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?";
        this.passwordEncoder = NoOpPasswordEncoder.getInstance();
        Assert.notNull(dataSource, "DataSource required");
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate));
    }

    public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
        try {
            ClientDetails details = (ClientDetails)this.jdbcTemplate.
                                    queryForObject(this.selectClientDetailsSql, 
                      new JdbcClientDetailsService.ClientDetailsRowMapper(), 
                      new Object[]{clientId});
            return details;
        } catch (EmptyResultDataAccessException var4) {
            throw new NoSuchClientException("No client with requested id: " + clientId);
        }
    }
}

JdbcClientDetailsService则是将ClientDetails存储在数据库中
通过使用jdbcTemplate对数据库进行增改查

3.2.AuthorizationServerEndpointsConfigurer

用来配置授权authorization以及令牌token的访问端点和令牌服务token services

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
        tokenServices.setTokenStore(jwtTokenStore());
        tokenServices.setSupportRefreshToken(true);
        //获取ClientDetailsService信息
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        // 一天有效期
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1));
        endpoints.tokenServices(tokenServices);
    }

DefaultTokenService作为OAuth2中操作token(crud)的默认实现,在OAuth2框架中有着很重要的地位。使用随机值创建令牌,并处理除永久令牌以外的所有令牌
在认证服务的 Endpoints 中, 使用的正是 DefaultTokenServices, 它为 DefaultTokenServices 提供了默认配置

public final class AuthorizationServerEndpointsConfigurer {
   private int refreshTokenValiditySeconds = 2592000;
   private int accessTokenValiditySeconds = 43200;
   private boolean supportRefreshToken = false;
   private boolean reuseRefreshToken = true;
   private TokenStore tokenStore;
   private ClientDetailsService clientDetailsService;
   private TokenEnhancer accessTokenEnhancer;
   private AuthenticationManager authenticationManager;

   private DefaultTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(this.tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(this.reuseRefreshToken);
        // 如果未配置, 则配置为 InMemoryClientDetailsService
        tokenServices.setClientDetailsService(this.clientDetailsService());
        tokenServices.setTokenEnhancer(this.tokenEnhancer());
        this.addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

    private TokenStore tokenStore() {
        // 如果未配置, 则创建
        if (this.tokenStore == null) {
            // 如果配置了 JwtAccessTokenConverter, 则创建 JwtTokenStore
            if (this.accessTokenConverter() instanceof JwtAccessTokenConverter) {
                this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter)this.accessTokenConverter());
            } else {
                // 否则, 创建 InMemoryTokenStore
                this.tokenStore = new InMemoryTokenStore();
            }
        }

        return this.tokenStore;
    }

    private TokenEnhancer tokenEnhancer() {
        // 如果未配置tokenEnhancer, 但配置了JwtAccessTokenConverter, 则将这个 convert 返回
        if (this.tokenEnhancer == null && this.accessTokenConverter() instanceof JwtAccessTokenConverter) {
            this.tokenEnhancer = (TokenEnhancer)this.accessTokenConverter;
        }

        return this.tokenEnhancer;
    }
    ......
}

核心属性字段解析

属性字段 作用
refreshTokenValiditySeconds refresh_token 的有效时长 (秒), 默认 30 天
accessTokenValiditySeconds access_token 的有效时长 (秒), 默认 12 小时
supportRefreshToken 是否支持 refresh token, 默认为 false
reuseRefreshToken 是否复用 refresh_token, 默认为 true (如果为 false, 每次请求刷新都会删除旧的 refresh_token, 创建新的 refresh_token)
tokenStore token 储存器 (持久化容器)
clientDetailsService 提供 client 详情的服务 (clientDetails 可持久化到数据库中或直接放在内存里)
accessTokenEnhancer token 增强器, 可以通过实现 TokenEnhancer 以存放 additional information
authenticationManager Authentication 管理者, 起到填充完整 Authentication的作用

TokenStore令牌存储器

OAuth2的永久令牌token管理主要交给TokenStore接口
TokenStore接口源码如下

public interface TokenStore {
    OAuth2Authentication readAuthentication(OAuth2AccessToken var1);

    OAuth2Authentication readAuthentication(String var1);

    void storeAccessToken(OAuth2AccessToken var1, OAuth2Authentication var2);

    OAuth2AccessToken readAccessToken(String var1);

    void removeAccessToken(OAuth2AccessToken var1);

    void storeRefreshToken(OAuth2RefreshToken var1, OAuth2Authentication var2);

    OAuth2RefreshToken readRefreshToken(String var1);

    OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken var1);

    void removeRefreshToken(OAuth2RefreshToken var1);

    void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken var1);

    OAuth2AccessToken getAccessToken(OAuth2Authentication var1);

    Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String var1, String var2);

    Collection<OAuth2AccessToken> findTokensByClientId(String var1);
}

TokenStore管理OAuth2AccessTokenOAuth2AuthenticationOAuth2RefreshTokenOAuth2Authentication的对应关系的增删改查

官方提供的TokenStore实现类如下:

InMemoryTokenStore:将OAuth2AccessToken保存在内存(默认)
JdbcTokenStore:将OAuth2AccessToken保存在数据库
JwkTokenStore:将OAuth2AccessToken保存到JSON Web Key
JwtTokenStore:将OAuth2AccessToken保存到JSON Web Token
RedisTokenStore将OAuth2AccessToken保存到Redis

有需要也可以实现TokenStore接口进行自定义

JwtTokenStore JWT令牌存储组件,供给认证服务器取来给授权服务器端点配置器
JwtAccessTokenConverter JWT访问令牌转换器(token生成器),按照设置的签名来生成Token

注:JwtAccessTokenConverter实现了Token增强器TokenEnhancer接口和令牌转换器AccessTokenConverter接口
JwtTokenStore类依赖JwtAccessTokenConverter类,授权服务器和资源服务器都需要接口的实现类(因此他们可以安全地使用相同的数据并进行解码)

需要在AuthorizationServerEndpointsConfigurer 授权服务器端点配置中加入

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("testKey");
        return converter;
    }

jwt具有自解释的特性,客户端不需要再去授权服务器认证这个token的合法性,这里使用对称密钥testKey来签署我们的令牌,意味着需要为资源服务器使用同样的确切密钥。
注:也支持使用非对称加密的方式,不过有点复杂

3.3.AuthorizationServerSecurityConfigurer:用来配置令牌(token)端点的安全约束。
@Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.tokenKeyAccess("isAuthenticated()");
    }

4.Spring Security安全配置

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("SSOUserDetailsService")
    private UserDetailsService userDetailsService;

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

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        authenticationProvider.setHideUserNotFoundExceptions(false);
        return authenticationProvider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().antMatchers("/oauth/**", "/login/**", "/logout/**")
             .and()
             .authorizeRequests()
             .antMatchers("/oauth/**").authenticated()
             .and()
             .formLogin().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider());
    }
}

注入UserDetailsService时需要加上@Qualifier("SSOUserDetailsService"),否则会报Could not autowire. There are more than one bean of 'UserDetailsService' type.

5.认证中心yml配置

server:
  servlet:
    context-path: /pjb

不加server.servlet.context-path会一直处在认证页面

客户端配置

创建两个客户端应用:client1和client2
唯一的区别是client1的端口是8086,client2的端口是8087

1.依赖引入

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

2.SSO客户端应用配置

配置最核心的部分是 @EnableOAuth2Sso注解来开启SSO
@EnableWebSecurity注解让Spring Security生效
@EnableGlobalMethodSecurity注解来判断用户对某个控制层的方法是否具有访问权限

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated();
    }
}

3.客户端控制层,@PreAuthorize进行权限拦截

@RestController
public class ClientController {

    @GetMapping("/normal")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public String normal( ) {
        return "用户页面";
    }

    @GetMapping("/medium")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public String medium() {
        return "这也是用户页面";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String admin() {
        return "管理员页面";
    }
}

4.客户端yml配置如下

server:
  port: 8086
security:
  oauth2:
    client:
      client-id: ben1
      client-secret: 123456
      user-authorization-uri: http://localhost:8080/pjb/oauth/authorize
      access-token-uri: http://localhost:8080/pjb/oauth/token
    resource:
      jwt:
        key-uri: http://localhost:8080/pjb/oauth/token_key
配置说明

security.oauth2.client.client-id:指定OAuth2 client ID.
security.oauth2.client.client-secret:指定OAuth2 client secret. 默认是一个随机的密码.
security.oauth2.client.user-authorization-uri:用户跳转去获取access token的URI(授权端)
security.oauth2.client.access-token-uri:指定获取access token的URI(令牌端)
security.oauth2.resource.jwt.key-uri:JWT token的URI

需要确保以上URL都是存在的,不然启动会报错

注:在客户端配置文件中指定security.oauth2.client.registered-redirect-uri客户端跳转URI不生效,需要在认证中心中指定

重点:

/oauth/authorize:验证
/oauth/token:获取token
/oauth/confirm_access:用户授权
/oauth/error:认证失败
/oauth/check_token:资源服务器用来校验token
/oauth/token_key:如果jwt模式则可以用此来从认证服务器获取公钥
以上这些endpoint都在源码里的endpoint包里面。

OAuth2获取token的主要流程:

1.用户发起获取token的请求。
2.过滤器会验证path是否是认证的请求/oauth/token,如果为false,则直接返回没有后续操作。
3.过滤器通过clientId查询生成一个Authentication对象。
4.然后会通过username和生成的Authentication对象生成一个UserDetails对象,并检查用户是否存在。
5.以上全部通过会进入地址/oauth/token,即TokenEndpointpostAccessToken方法中。
6.postAccessToken方法中会验证Scope,然后验证是否是refreshToken请求等。
7.之后调用AbstractTokenGranter中的grant方法。
8.grant方法中调用AbstractUserDetailsAuthenticationProviderauthenticate方法,通过usernameAuthentication对象来检索用户是否存在。
9.然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
10.然后将OAuth2AccessToken对象包装进响应流返回。

OAuth2刷新token的流程

刷新token(refresh token)的流程与获取token的流程只有⑨有所区别:
获取token调用的是AbstractTokenGranter中的getAccessToken方法,然后调用tokenStore中的getAccessToken方法获取token
刷新token调用的是RefreshTokenGranter中的getAccessToken方法,然后使用tokenStore中的refreshAccessToken方法获取token

启动测试

先启动认证中心,再启动两个客户端

访问客户端http://localhost:8086/normal会跳转到Spring Security的登录认证页,也就是认证中心登录页

image.png

在认证中心中,我设置了用户名是user,密码是123456,权限是ROLE_USER

注:在ClientDetailsServiceConfigurer中如果设置了autoApprovefalse
需要手动确认授权

image.png

在client1上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben1&redirect_uri=http://localhost:8086/login&response_type=code&state=4hBAab

点击approve确定授权

image.png

想跳过这个认证确认的过程,设置autoApprovetrue(推荐)

接着访问http://localhost:8087/normal,点击approve授权后也可以访问到

image.png

在client2上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben2&redirect_uri=http://localhost:8087/login&response_type=code&state=3EpENW

image.png

访问http://localhost:8087/medium也是没问题的,都是ROLE_USER权限

image.png

但是访问http://localhost:8087/admin 就没权限了

image.png
Github源码地址:https://github.com/JinBinPeng/SpringBoot-SSO