SpringBoot中oauth2.0學習之服務端配置快速上手
現在第三方登入的例子數見不鮮。其實在這種示例當中,oauth2.0是使用比較多的一種授權登入的標準。oauth2.0也是從oauth1.0升級過來的。那麼關於oauth2.0相關的概念及其原理,大家可以參考這篇文章,這篇文章中會有更詳細的解釋,下來我們直接進入正題。
1.1、gradle依賴
compile('org.springframework.cloud:spring-cloud-starter-oauth2')
compile('org.springframework.cloud:spring-cloud-starter-security')
在這裡我直接引入的是spring-cloud的依賴項,這種依賴的jar包更全面一些,這裡面的核心基礎還是spring-security。這裡SpringBoot的版本為2.0.6.REALEASE
1.2、@EnableAuthorizationServer
在這裡我著重強調一下這個註解:@EnableAuthorizationServer
,這個註解原始碼如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class}) public @interface EnableAuthorizationServer { }
這個註解主要是匯入兩個配置類,分別是:
-
AuthorizationServerEndpointsConfiguration
,這個配置類主要配置授權端點,獲取token的端點。大家就把對應的端點想象成controller即可,在這個controller下開放了若干個@RequestMapping,比如常見的有:/oauth/authorize(授權路徑)
,/oauth/token(獲取token)
等 -
AuthorizationServerSecurityConfiguration
,主要是做spring-security的安全配置,我們可以看一下相關程式碼:
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private List<AuthorizationServerConfigurer> configurers = Collections.emptyList(); @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerEndpointsConfiguration endpoints; @Autowired public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception { for (AuthorizationServerConfigurer configurer : configurers) { configurer.configure(clientDetails); } } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // Over-riding to make sure this.disableLocalConfigureAuthenticationBldr = false // This will ensure that when this configurer builds the AuthenticationManager it will not attempt // to find another 'Global' AuthenticationManager in the ApplicationContext (if available), // and set that as the parent of this 'Local' AuthenticationManager. // This AuthenticationManager should only be wired up with an AuthenticationProvider // composed of the ClientDetailsService (wired in this configuration) for authenticating 'clients' only. } @Override protected void configure(HttpSecurity http) throws Exception { //....省略部分程式碼 String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token"); String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key"); String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token"); if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) { UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class); endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService); } // @formatter:off //上述節點的請求需要授權驗證 http .authorizeRequests() .antMatchers(tokenEndpointPath).fullyAuthenticated() .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess()) .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess()) .and() .requestMatchers() .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); // @formatter:on http.setSharedObject(ClientDetailsService.class, clientDetailsService); } protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { for (AuthorizationServerConfigurer configurer : configurers) { configurer.configure(oauthServer); } } }
1.2.1、AuthorizationServerConfigurer
這個介面是認證授權配置的核心介面,不過既然是SpringBoot我們就先來看看它怎麼幫我們裝配的,我們可以在org.springframework.boot.autoconfigure.security.oauth2.authserver
這個包下面找到對應配置的Bean:
@Configuration
@ConditionalOnClass(EnableAuthorizationServer.class)
@ConditionalOnMissingBean(AuthorizationServerConfigurer.class)
@ConditionalOnBean(AuthorizationServerEndpointsConfiguration.class)
@EnableConfigurationProperties(AuthorizationServerProperties.class)
public class OAuth2AuthorizationServerConfiguration
extends AuthorizationServerConfigurerAdapter {
//....
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//預設基於記憶體建立ClientDetails
ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder>.ClientBuilder builder = clients
.inMemory().withClient(this.details.getClientId());
builder.secret(this.details.getClientSecret())
.resourceIds(this.details.getResourceIds().toArray(new String[0]))
.authorizedGrantTypes(
this.details.getAuthorizedGrantTypes().toArray(new String[0]))
.authorities(
AuthorityUtils.authorityListToSet(this.details.getAuthorities())
.toArray(new String[0]))
.scopes(this.details.getScope().toArray(new String[0]));
if (this.details.getAutoApproveScopes() != null) {
builder.autoApprove(
this.details.getAutoApproveScopes().toArray(new String[0]));
}
if (this.details.getAccessTokenValiditySeconds() != null) {
builder.accessTokenValiditySeconds(
this.details.getAccessTokenValiditySeconds());
}
if (this.details.getRefreshTokenValiditySeconds() != null) {
builder.refreshTokenValiditySeconds(
this.details.getRefreshTokenValiditySeconds());
}
if (this.details.getRegisteredRedirectUri() != null) {
builder.redirectUris(
this.details.getRegisteredRedirectUri().toArray(new String[0]));
}
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
if (this.tokenConverter != null) {
endpoints.accessTokenConverter(this.tokenConverter);
}
if (this.tokenStore != null) {
endpoints.tokenStore(this.tokenStore);
}
if (this.details.getAuthorizedGrantTypes().contains("password")) {
endpoints.authenticationManager(this.authenticationManager);
}
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security)
throws Exception {
security.passwordEncoder(NoOpPasswordEncoder.getInstance());
if (this.properties.getCheckTokenAccess() != null) {
security.checkTokenAccess(this.properties.getCheckTokenAccess());
}
if (this.properties.getTokenKeyAccess() != null) {
security.tokenKeyAccess(this.properties.getTokenKeyAccess());
}
if (this.properties.getRealm() != null) {
security.realm(this.properties.getRealm());
}
}
@Configuration
@ConditionalOnMissingBean(BaseClientDetails.class)
protected static class BaseClientDetailsConfiguration {
private final OAuth2ClientProperties client;
protected BaseClientDetailsConfiguration(OAuth2ClientProperties client) {
this.client = client;
}
/**
由此可知它會尋找security.oauth2.client的配置
*/
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public BaseClientDetails oauth2ClientDetails() {
BaseClientDetails details = new BaseClientDetails();
if (this.client.getClientId() == null) {
this.client.setClientId(UUID.randomUUID().toString());
}
details.setClientId(this.client.getClientId());
details.setClientSecret(this.client.getClientSecret());
details.setAuthorizedGrantTypes(Arrays.asList("authorization_code",
"password", "client_credentials", "implicit", "refresh_token"));
details.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
details.setRegisteredRedirectUri(Collections.<String>emptySet());
return details;
}
}
}
如果沒有用spring-boot的使用者,可以也可以參考上述的配置方法,自行配置
1.3、application.yml的配置
根據上述程式碼我們可以知道,springboot通過外部化配置的security.oauth2.client的字首來配置客戶端。那麼因此我們不妨在外部化配置檔案裡做如下配置:
server:
port: 8080
security:
oauth2:
client:
client-id: root
client-secret: root
scope:
- email
- username
- face
spring:
security:
user:
name: root
password: root
roles: ADMIN
這裡先做最基本的配置,配置client-id
,client-secret
,scope
。特別注意oauth2.0一定要先經過springsecurity的auth認證,因此需要在這裡配置一個記憶體使用者名稱與密碼為root與root
1.4、配置資源伺服器
通過資源伺服器來保護我們指定的資源,必須在獲取授權認證的時候才能訪問。在SpringBoot當中,我們可以通過@EnableResourceServer
註解來開啟此功能。該註解定義如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ResourceServerConfiguration.class)
public @interface EnableResourceServer {
}
我們可以看到這個註解匯入了預設的資源配置資訊:ResourceServerConfiguration
,它的原始碼如下:
@Configuration
public class ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered {
//....
@Override
protected void configure(HttpSecurity http) throws Exception {
ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
ResourceServerTokenServices services = resolveTokenServices();
if (services != null) {
resources.tokenServices(services);
}
else {
if (tokenStore != null) {
resources.tokenStore(tokenStore);
}
else if (endpoints != null) {
resources.tokenStore(endpoints.getEndpointsConfigurer().getTokenStore());
}
}
if (eventPublisher != null) {
resources.eventPublisher(eventPublisher);
}
//配置資源
for (ResourceServerConfigurer configurer : configurers) {
configurer.configure(resources);
}
// @formatter:off
http.authenticationProvider(new AnonymousAuthenticationProvider("default"))
// N.B. exceptionHandling is duplicated in resources.configure() so that
// it works
.exceptionHandling()
.accessDeniedHandler(resources.getAccessDeniedHandler()).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable();
// @formatter:on
http.apply(resources);
if (endpoints != null) {
// Assume we are in an Authorization Server
http.requestMatcher(new NotOAuthRequestMatcher(endpoints.oauth2EndpointHandlerMapping()));
}
for (ResourceServerConfigurer configurer : configurers) {
// Delegates can add authorizeRequests() here
configurer.configure(http);
}
//如果沒有任何配置資源,則所有請求保護
if (configurers.isEmpty()) {
// Add anyRequest() last as a fall back. Spring Security would
// replace an existing anyRequest() matcher with this one, so to
// avoid that we only add it if the user hasn't configured anything.
http.authorizeRequests().anyRequest().authenticated();
}
}
//....
}
在這裡主要是配置資源伺服器的配置,我們可以得到如下幾點資訊:
- 資源配置的核心
ResourceServerConfigurer
,在這裡如果沒有任何配置,則所有請求都要進行token認證 -
TokenStore
主要定義了對token的增刪改查操作,用於持久化token -
ResourceServerTokenServices
資源服務的service(服務層),這裡主要還是根據token來拿到OAuth2Authentication
與OAuth2AccessToken
1.5、完整示例
1.5.1、資源認證配置
@Configuration
@EnableResourceServer
public class ResourceConfigure extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and().authorizeRequests().antMatchers("/free/**").permitAll().and()
.authorizeRequests().anyRequest().authenticated()
.and().formLogin().permitAll();//必須認證過後才可以訪問
}
}
在這裡如果以/free/**
請求路徑的,都允許直接訪問。否則,都必須攜帶access_token
才能訪問。
1.5.2 、授權認證配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().requestMatchers().anyRequest().and().authorizeRequests()
.antMatchers("/oauth/*").authenticated().and().formLogin().permitAll();
}
}
根據上文所述,AuthorizationServerEndpoint
與TokenEndpoint
會開放/oauth/authorize與/oauth/token
端點,因此我們必須保證訪問端點進行授權認證前,通過springsecurity
的使用者認證,因此在這裡配置了/oauth/*
1.5.3、啟動類
@SpringBootApplication
@EnableAuthorizationServer
@Controller
public class AuthorizationServer {
@GetMapping("/order")
public ResponseEntity<String> order() {
ResponseEntity<String> responseEntity = new ResponseEntity("order", HttpStatus.OK);
return responseEntity;
}
@GetMapping("/free/test")
public ResponseEntity<String> test() {
ResponseEntity<String> responseEntity = new ResponseEntity("free", HttpStatus.OK);
return responseEntity;
}
public static void main(String[] args) {
SpringApplication.run(AuthorizationServer.class, args);
}
}
1.5.4、訪問請求
首先我們通過postman 訪問http://localhost:8080/order
會得到如下介面:
此時我們明顯可以看到對應的資源需要攜帶有效的token才可以訪問,那麼我們此時要在postman的Authorization進行oauth2.0配置認證。截圖如下:
在這裡點選Get New Access Token 來從認證伺服器獲取token,點選後配置如下:
`
-
scope
配置對應application.yml中的配置資訊,這裡面可以放置使用者的屬性資訊,比如說暱稱 頭像 電話等等 -
State
代表狀態碼,設定一個State標誌 - 回撥地址這裡必須配置,通過這個地址當同意授權後會返回一個認證的code給我們,我們根據這個code請求token
- 認證地址與獲取token的地址請填寫,相關Endpoint生成的地址
當經過一連串認證後,我們即可拿到token:
當我們獲取到最新的token以後,我們即可訪問到對應的請求資源: