單點(diǎn)登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業(yè)業(yè)務(wù)整合的解決方案之一。SSO的定義是在多個應(yīng)用系統(tǒng)中,用戶只需要登錄一次就可以訪問所有相互信任的應(yīng)用系統(tǒng)。
Spring Security OAuth 是建立在 Spring Security 的基礎(chǔ)之上 OAuth2.0 協(xié)議實(shí)現(xiàn)的一個類庫
Spring Security OAuth2 為 Spring Cloud 搭建認(rèn)證授權(quán)服務(wù)(能夠更好的集成到 Spring Cloud 體系中)
單點(diǎn)登錄主要包括
服務(wù)端:一個第三方授權(quán)中心服務(wù)(Server),用于完成用戶登錄,認(rèn)證和權(quán)限處理
客戶端:當(dāng)用戶訪問客戶端應(yīng)用的安全頁面時,會重定向到授權(quán)中心進(jìn)行身份驗(yàn)證,認(rèn)證完成后方可訪問客戶端應(yīng)用的服務(wù),且多個客戶端應(yīng)用只需要登錄一次即可
相關(guān)版本:
SpringBoot:2.1.5.RELEASE
SpringCloud :Greenwich.SR1
認(rèn)證中心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.創(chuàng)建驗(yàn)證用戶,設(shè)置用戶名和密碼并設(shè)置角色權(quán)限
@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.認(rèn)證服務(wù)器配置
①加入@EnableAuthorizationServer注解來啟動OAuth2.0授權(quán)服務(wù)機(jī)制
②通過繼承AuthorizationServerConfigurerAdapter并且覆寫其中的三個configure方法來進(jìn)行配置
3.1.ClientDetailsServiceConfigurer
用于定義客戶詳細(xì)信息服務(wù)的配置器。客戶端詳情信息進(jìn)行初始化,能夠把客戶端詳情信息寫在內(nèi)存中或者是通過數(shù)據(jù)庫來存儲調(diào)取詳情信息。
多個客戶端來連接Spring OAuth2 Auth Server,需要在配置類里為inMemory生成器定義多個withClients
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應(yīng)用的通行證
clients.inMemory()// 使用in-memory存儲
.withClient("ben1")// client_id
.secret(new BCryptPasswordEncoder().encode("123456"))// client_secret
.authorizedGrantTypes("authorization_code", "refresh_token")// 該client允許的授權(quán)類型
.scopes("all")// 允許的授權(quán)范圍
.autoApprove(false)
//加上驗(yàn)證回調(diào)地址
.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");
}
必須設(shè)置回調(diào)地址redirectUris,并且格式是http://客戶端IP:端口/login的格式,否則會報OAuth Error error=”invalid_request”, error_description=”At least one redirect_uri must be registered with the client.”
原理如下圖:

ClientDetailsServiceConfiguration根據(jù)ClientDetailsServiceConfigurer配置,交給ClientDetailsServiceBuilder的實(shí)現(xiàn)類通過ClientBuilder創(chuàng)建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的一個內(nèi)部類,其中build()會被ClientDetailsServiceConfiguration所調(diào)用
ClientDetailsServiceBuilder部分源碼
public class ClientDetailsServiceBuilder<B extends ClientDetailsServiceBuilder<B>>
extends SecurityConfigurerAdapter<ClientDetailsService, B>
implements SecurityBuilder<ClientDetailsService> {
private List<ClientDetailsServiceBuilder<B>.ClientBuilder> clientBuilders = new ArrayList();
//設(shè)置Client并把其放到list
public ClientDetailsServiceBuilder<B>.ClientBuilder withClient(String clientId) {
ClientDetailsServiceBuilder<B>.ClientBuilder clientBuilder = new ClientDetailsServiceBuilder.ClientBuilder(clientId);
this.clientBuilders.add(clientBuilder);
return clientBuilder;
}
//創(chuàng)建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:第三方應(yīng)用和授權(quán)服務(wù)器之間的安全憑證(可理解為密碼)
scope:指定客戶端申請的權(quán)限范圍,可選值包括read,write,trust;其實(shí)授權(quán)賦予第三方用戶可以在資源服務(wù)器獲取資源,第三方訪問資源的一個權(quán)限,訪問范圍。
resourceIds:客戶端所能訪問的資源id集合
authorizedGrantTypes:此客戶端可以使用的授權(quán)類型,默認(rèn)為空。
可選值包括authorization_code,password,refresh_token,implicit,client_credentials
最常用的grant_type組合有: "authorization_code,refresh_token"(針對通過瀏覽器訪問的客戶端); "password,refresh_token"(針對移動設(shè)備的客戶端)
registeredRedirectUris:客戶端的重定向URI
autoApproveScopes:設(shè)置用戶是否自動Approval操作, 默認(rèn)值為 false,
可選值包括 true,false, read,write.
該字段只適用于grant_type="authorization_code的情況,當(dāng)用戶登錄成功后,
若該值為true或支持的scope值,則會跳過用戶Approve的頁面, 直接授權(quán).
authorities:指定客戶端所擁有的Spring Security的權(quán)限值。
accessTokenValiditySeconds:設(shè)定客戶端的access_token的有效時間值(單位:秒),可選, 若不設(shè)定值則使用默認(rèn)的有效時間值(60 * 60 * 12, 12小時).
refreshTokenValiditySeconds:設(shè)定客戶端的refresh_token的有效時間值(單位:秒),可選, 若不設(shè)定值則使用默認(rèn)的有效時間值(60 * 60 * 24 * 30, 30天).
additionalInformation:這是一個預(yù)留的字段,在Oauth的流程中沒有實(shí)際的使用,可選,但若設(shè)置值,必須是JSON格式的數(shù)據(jù)
具體可參考:http://andaily.com/spring-oauth-server/db_table_description.html
ClientDetailsServiceConfiguration
ClientDetailsServiceConfiguration 依據(jù)配置,由ClientDetailsServiceBuilder創(chuàng)建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(),因?yàn)?code>ClientDetailsServiceBuilder的build()需要調(diào)用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;
}
......
}
同理:創(chuàng)建出的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存儲在數(shù)據(jù)庫中
通過使用jdbcTemplate對數(shù)據(jù)庫進(jìn)行增改查
3.2.AuthorizationServerEndpointsConfigurer
用來配置授權(quán)authorization以及令牌token的訪問端點(diǎn)和令牌服務(wù)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)的默認(rèn)實(shí)現(xiàn),在OAuth2框架中有著很重要的地位。使用隨機(jī)值創(chuàng)建令牌,并處理除永久令牌以外的所有令牌
在認(rèn)證服務(wù)的 Endpoints 中, 使用的正是 DefaultTokenServices, 它為 DefaultTokenServices 提供了默認(rèn)配置
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() {
// 如果未配置, 則創(chuàng)建
if (this.tokenStore == null) {
// 如果配置了 JwtAccessTokenConverter, 則創(chuàng)建 JwtTokenStore
if (this.accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter)this.accessTokenConverter());
} else {
// 否則, 創(chuàng)建 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 的有效時長 (秒), 默認(rèn) 30 天 |
| accessTokenValiditySeconds | access_token 的有效時長 (秒), 默認(rèn) 12 小時 |
| supportRefreshToken | 是否支持 refresh token, 默認(rèn)為 false |
| reuseRefreshToken | 是否復(fù)用 refresh_token, 默認(rèn)為 true (如果為 false, 每次請求刷新都會刪除舊的 refresh_token, 創(chuàng)建新的 refresh_token) |
| tokenStore | token 儲存器 (持久化容器) |
| clientDetailsService | 提供 client 詳情的服務(wù) (clientDetails 可持久化到數(shù)據(jù)庫中或直接放在內(nèi)存里) |
| accessTokenEnhancer | token 增強(qiáng)器, 可以通過實(shí)現(xiàn) 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的對應(yīng)關(guān)系的增刪改查
官方提供的TokenStore實(shí)現(xiàn)類如下:
InMemoryTokenStore:將OAuth2AccessToken保存在內(nèi)存(默認(rèn))
JdbcTokenStore:將OAuth2AccessToken保存在數(shù)據(jù)庫
JwkTokenStore:將OAuth2AccessToken保存到JSON Web Key
JwtTokenStore:將OAuth2AccessToken保存到JSON Web Token
RedisTokenStore將OAuth2AccessToken保存到Redis
有需要也可以實(shí)現(xiàn)TokenStore接口進(jìn)行自定義
JwtTokenStore JWT令牌存儲組件,供給認(rèn)證服務(wù)器取來給授權(quán)服務(wù)器端點(diǎn)配置器
JwtAccessTokenConverter JWT訪問令牌轉(zhuǎn)換器(token生成器),按照設(shè)置的簽名來生成Token
注:JwtAccessTokenConverter實(shí)現(xiàn)了Token增強(qiáng)器TokenEnhancer接口和令牌轉(zhuǎn)換器AccessTokenConverter接口
JwtTokenStore類依賴JwtAccessTokenConverter類,授權(quán)服務(wù)器和資源服務(wù)器都需要接口的實(shí)現(xiàn)類(因此他們可以安全地使用相同的數(shù)據(jù)并進(jìn)行解碼)
需要在AuthorizationServerEndpointsConfigurer 授權(quán)服務(wù)器端點(diǎn)配置中加入
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
jwt具有自解釋的特性,客戶端不需要再去授權(quán)服務(wù)器認(rèn)證這個token的合法性,這里使用對稱密鑰testKey來簽署我們的令牌,意味著需要為資源服務(wù)器使用同樣的確切密鑰。
注:也支持使用非對稱加密的方式,不過有點(diǎn)復(fù)雜
3.3.AuthorizationServerSecurityConfigurer:用來配置令牌(token)端點(diǎn)的安全約束。
@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.認(rèn)證中心yml配置
server:
servlet:
context-path: /pjb
不加server.servlet.context-path會一直處在認(rèn)證頁面
客戶端配置
創(chuàng)建兩個客戶端應(yīng)用:client1和client2
唯一的區(qū)別是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客戶端應(yīng)用配置
配置最核心的部分是 @EnableOAuth2Sso注解來開啟SSO
@EnableWebSecurity注解讓Spring Security生效
@EnableGlobalMethodSecurity注解來判斷用戶對某個控制層的方法是否具有訪問權(quán)限
@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進(jìn)行權(quán)限攔截
@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. 默認(rèn)是一個隨機(jī)的密碼.
security.oauth2.client.user-authorization-uri:用戶跳轉(zhuǎn)去獲取access token的URI(授權(quán)端)
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客戶端跳轉(zhuǎn)URI不生效,需要在認(rèn)證中心中指定
重點(diǎn):
/oauth/authorize:驗(yàn)證
/oauth/token:獲取token
/oauth/confirm_access:用戶授權(quán)
/oauth/error:認(rèn)證失敗
/oauth/check_token:資源服務(wù)器用來校驗(yàn)token
/oauth/token_key:如果jwt模式則可以用此來從認(rèn)證服務(wù)器獲取公鑰
以上這些endpoint都在源碼里的endpoint包里面。
OAuth2獲取token的主要流程:
1.用戶發(fā)起獲取token的請求。
2.過濾器會驗(yàn)證path是否是認(rèn)證的請求/oauth/token,如果為false,則直接返回沒有后續(xù)操作。
3.過濾器通過clientId查詢生成一個Authentication對象。
4.然后會通過username和生成的Authentication對象生成一個UserDetails對象,并檢查用戶是否存在。
5.以上全部通過會進(jìn)入地址/oauth/token,即TokenEndpoint的postAccessToken方法中。
6.postAccessToken方法中會驗(yàn)證Scope,然后驗(yàn)證是否是refreshToken請求等。
7.之后調(diào)用AbstractTokenGranter中的grant方法。
8.grant方法中調(diào)用AbstractUserDetailsAuthenticationProvider的authenticate方法,通過username和Authentication對象來檢索用戶是否存在。
9.然后通過DefaultTokenServices類從tokenStore中獲取OAuth2AccessToken對象。
10.然后將OAuth2AccessToken對象包裝進(jìn)響應(yīng)流返回。
OAuth2刷新token的流程
刷新token(refresh token)的流程與獲取token的流程只有⑨有所區(qū)別:
獲取token調(diào)用的是AbstractTokenGranter中的getAccessToken方法,然后調(diào)用tokenStore中的getAccessToken方法獲取token。
刷新token調(diào)用的是RefreshTokenGranter中的getAccessToken方法,然后使用tokenStore中的refreshAccessToken方法獲取token。
啟動測試
先啟動認(rèn)證中心,再啟動兩個客戶端
訪問客戶端http://localhost:8086/normal會跳轉(zhuǎn)到Spring Security的登錄認(rèn)證頁,也就是認(rèn)證中心登錄頁

在認(rèn)證中心中,我設(shè)置了用戶名是user,密碼是123456,權(quán)限是ROLE_USER
注:在ClientDetailsServiceConfigurer中如果設(shè)置了autoApprove為false
需要手動確認(rèn)授權(quán)

在client1上URL中包含的信息
http://localhost:8080/pjb/oauth/authorize?client_id=ben1&redirect_uri=http://localhost:8086/login&response_type=code&state=4hBAab
點(diǎn)擊approve確定授權(quán)

想跳過這個認(rèn)證確認(rèn)的過程,設(shè)置autoApprove 為true(推薦)
接著訪問http://localhost:8087/normal,點(diǎn)擊approve授權(quán)后也可以訪問到

在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權(quán)限

但是訪問http://localhost:8087/admin 就沒權(quán)限了
