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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345