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
ClientBuilder是ClientDetailsServiceBuilder的一个内部类,其中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();
}
......
}
InMemoryClientDetailsServiceBuilder和JdbcClientDetailsServiceBuilder均继承于ClientDetailsServiceBuilder,都会重写performBuild(),因为ClientDetailsServiceBuilder的build()需要调用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也分为InMemoryClientDetailsService和JdbcClientDetailsService
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;
}
}
......
}
InMemoryClientDetailsService将ClientDetails存储到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管理OAuth2AccessToken 与OAuth2Authentication和OAuth2RefreshToken与OAuth2Authentication的对应关系的增删改查
官方提供的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,即TokenEndpoint的postAccessToken方法中。
6.postAccessToken方法中会验证Scope,然后验证是否是refreshToken请求等。
7.之后调用AbstractTokenGranter中的grant方法。
8.grant方法中调用AbstractUserDetailsAuthenticationProvider的authenticate方法,通过username和Authentication对象来检索用户是否存在。
9.然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
10.然后将OAuth2AccessToken对象包装进响应流返回。
OAuth2刷新token的流程
刷新token(refresh token)的流程与获取token的流程只有⑨有所区别:
获取token调用的是AbstractTokenGranter中的getAccessToken方法,然后调用tokenStore中的getAccessToken方法获取token。
刷新token调用的是RefreshTokenGranter中的getAccessToken方法,然后使用tokenStore中的refreshAccessToken方法获取token。
启动测试