单点登录(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-security
和spring-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.”
原理如下图:
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
。
启动测试
先启动认证中心,再启动两个客户端
访问客户端http://localhost:8086/normal会跳转到Spring Security的登录认证页,也就是认证中心登录页
在认证中心中,我设置了用户名是user
,密码是123456
,权限是ROLE_USER
注:在ClientDetailsServiceConfigurer
中如果设置了autoApprove
为false
需要手动确认授权
在client1上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben1&redirect_uri=http://localhost:8086/login&response_type=code&state=4hBAab
点击approve
确定授权
想跳过这个认证确认的过程,设置autoApprove
为true
(推荐)
接着访问http://localhost:8087/normal,点击approve
授权后也可以访问到
在client2上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben2&redirect_uri=http://localhost:8087/login&response_type=code&state=3EpENW
访问http://localhost:8087/medium也是没问题的,都是ROLE_USER
权限
但是访问http://localhost:8087/admin 就没权限了