Spring Cloud(6.1):搭建OAuth2 Authorization Server


配置web.xml

添加spring-cloud-starter-security和spring-security-oauth2-autoconfigure兩個依賴。

</dependency>
<!-- Spring cloud starter: Security -->
<!-- Include: web, actuator, security, zuul, etc. -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!-- Spring Security OAuth2 Autoconfigure (optional in spring-cloud-security after 2.1) -->
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

此外,它還是一個Eureka Client和Config Client,如何配置Eureka Client和Config Client請看前面章節。

 

配置WebSecurity

package com.mytools.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * Spring Security Configuration.
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * password encodeer
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /* (non-Javadoc)
     * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //@formatter:off
        http.authorizeRequests() // configure authorize request rule
                .antMatchers("/index").permitAll()
                // .antMatchers("/url/**").hasRole("ADMIN") // some urls have access ADMIN
                // .anyRequest().authenticated() // any other request need to authenticate
                .and()
            .formLogin() // login as form
                .loginPage("/login") // login url (default is login page with framework)
                // .defaultSuccessUrl("/index") // login success url (default is index)
                .failureUrl("/login-error") // login fail url
                .and()
            // .logout() // logout config
                // .logoutUrl("/logout") // logout url (default is logout)
                // .logoutSuccessUrl("/index") // logout success url (default is login)
            .rememberMe() // Remember me
                .key("uniqueAndSecret") // generate the contents of the token
                .tokenValiditySeconds(60 * 60 * 24 * 30) // 30 days
                .userDetailsService(userDetailsService) // register UserDetailsService for remember me functionality
               // .and()
            //.httpBasic() // use HTTP Basic authentication(in header) for an application
        ;
        //@formatter:on
    }
}

說明:

(1)UserDetailsService的配置:有兩種方式,一種是實現UserDetails/UserDetailsService接口,從DB中獲取User和Role。另一種是使用InMemoryUserDetailsManagerConfigurer在內存中創建user和Role。這里使用了第一種,這一部分是Spring Security的范疇,是這里不再貼代碼。如果對這部分不熟悉,可以參考:

Spring Security(1):認證和授權的核心組件介紹及源碼分析

Spring Security Reference

(2)PasswordEncoder的配置:使用PasswordEncoderFactories可以通過不同前綴來識別和創建各種不同的PasswordEncoder。在當前的Spring Security版本中,password必須加密,不加密會報錯。

(3)AuthenticationManager的配置:AuthenticationManager雖然在Spring Security自動配置中已經創建,但是並沒有暴露為一個Spring Bean(exposed as a Bean)。我們在這里覆蓋它並聲明它為一個Bean,目的是在配置Authorization Server時配置AuthorizationServerEndpoints時使用(for the password grant)。

(4)HttpSecurity的配置:這一部分是Spring Security的范疇,這里不再贅述。這里主要自定義了User&Role&Path的mapping關系,及login, index, logout,remember-me等邏輯和頁面等。

 

配置Authorization Server

package com.mytools.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;

/**
 * OAuth2 Authorization Server Configuration.
 */
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 用來配置令牌端點(Token Endpoint)的安全約束<br>
     * 
     * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer)
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 如果配置這個的話,且url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter來保護
        // 如果沒有配置這個,或者配置了這個但是url中沒有client_id和client_secret的,走basic認證保護
        // [IMPORTANT] 這里如果不設置,client app里的clientAuthenticationScheme應該設置為header,反之設置為form
        // security.allowFormAuthenticationForClients();
    }

    /**
     * 用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情信息在這里進行初始化<br>
     * 
     * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer)
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    /* 定義授權和令牌端點以及令牌服務<br>
     * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager) // 使用Spring提供的AuthenticationManager開啟密碼授權
                .userDetailsService(userDetailsService) // 注入一個 UserDetailsService,那么刷新令牌授權將包含對用戶詳細信息的檢查,以確保該帳戶仍然是活動的
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); // 默認只支持POST
    }
}

說明:

(1)注入AuthenticationManager及UserDetailsService:在配置AuthorizationServerEndpointsConfigurer時使用。

(2)ClientDetailsService配置:與UserDetailsService的配置類似,同樣有兩種方式,一種是實現ClientDetails/ClientDetailsService接口,從DB中獲取Client。另一種是使用InMemoryClientDetailsServiceBuilder在內存中創建Client。這里使用了第一種,也不再貼代碼。

(3)AuthorizationServerSecurityConfigurer配置:用來配置Authorization Server令牌端點(Token Endpoint)的安全約束,一般不用配置。

(4)ClientDetailsServiceConfigurer配置:配置ClientDetails。

(5)AuthorizationServerEndpointsConfigurer配置:配置authenticationManager和userDetailsService是告訴Authorization Server使用Spring Security提供的驗證管理器及用戶詳細信息服務。

 

配置ResourceServer

package com.mytools.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

/**
 * OAuth2 Resource Server Configuration.
 */
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    /**
     * ResourceId: We can give each of the ResourceServer instance to a resourceId.<br>
     * Here is the resourceId validation on OAuth2AuthenticationManager#authenticate:<br>
     * 
     * <pre>
     * Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
     * if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
     *     throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
     * }
     * </pre>
     * 
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("server-auth-resource");
        super.configure(resources);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // [IMPORTANT] 為什么要提前加antMatcher? 可以看一下antMatcher()的注釋:
        // Allows configuring the HttpSecurity to only be invoked when matching the provided ant pattern.
        http.antMatcher("/user").authorizeRequests()
                // .antMatchers("xxx", "xxx").permitAll()
                .anyRequest().authenticated();
    }
}

主要定義了resource-id及受保護的資源的path及Role&Path的mapping關系

 

配置ServerAuthApplication

package com.mytools;

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
@EnableAuthorizationServer
@EnableResourceServer
public class ServerAuthApplication2 {

    public static void main(String[] args) {
        SpringApplication.run(ServerAuthApplication2.class, args);
    }

    /**
     * 映射到/user, Resource Server會調用該端點<br>
     * Resource Server中的@EnableResourceServer會強制執行一個過濾器,<br>
     * 該攔截器會用傳入的token回調[security.oauth2.resource.userInfoUri]中定義的URI來查看令牌是否有效。<br>
     * 此外,該URI還會從Authorization Server傳回一個Map,包含Principal and GrantedAuthority信息。<br>
     * 這個信息是必須的。詳細請看:UserInfoTokenServices.loadAuthentication<br>
     * 
     * @param user
     * @return
     */
    @RequestMapping(value = "/user", produces = "application/json")
    public Map<String, Object> user(OAuth2Authentication user, @RequestParam(required = false) String client) {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("user", user.getUserAuthentication().getName());
        userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
        return userInfo;
    }
}

說明:

(1)使用@EnableAuthorizationServer聲明為一個OAuth2 Authorization Server。

(2)使用@EnableResourceServer聲明為一個OAuth2 Resource Server。這里聲明其為一個Resource Server只是為了保護/user這一個端點。下面/user端點的定義會具體說明原因。

(3)/user Endpoint:Resource Server中的@EnableResourceServer會強制執行一個攔截器,該攔截器會用傳入的token回調Resource Server配置文件中定義的security.oauth2.resource.userInfoUri來查看令牌是否有效。這個userInfoUri就映射到了Authorization Server中/user Endpoint。在/user Endpoint中返回了一個含有包含Principal和GrantedAuthority信息的Map。最終Resource Server拿到這個Map,進而判斷這個User有沒有這個權限來訪問Resource Server中的某個path。相關源碼可以看UserInfoTokenServices的loadAuthentication方法。

 

驗證

准備數據

-- Create user
INSERT INTO user (username, password, email, enabled, create_user, create_date_time, update_user, update_date_time)
VALUES ('admin', '{bcrypt}$2a$10$bmixgIna/bd5gU5ORrWng.xUs2sGBh3BRqj927ChKkAvJA8CVGZmm', 'admin@email.com', true, 'admin', '2019-01-01 10:00:00.000', 'admin', '2019-01-01 10:00:00.000');

-- Create client
INSERT INTO client (client_id, client_secret, authorized_grant_types_str, registered_redirect_uris_str, enabled, create_user, create_date_time, update_user, update_date_time)
VALUES ('dummy-client', '{bcrypt}$2a$10$nbLJ9DdK/HLlKc.Gm/5S4utfxht9D3mj5M7cm9peFDbBGgTLPEh0u', 'authorization_code,password', 'https://www.google.com', true, 'admin', '2019-01-01 10:00:00.000', 'admin', '2019-01-01 10:00:00.000');

-- Create role
INSERT INTO role (rolename, role_level, enabled, create_user, create_date_time, update_user, update_date_time)
VALUES ('ADMIN', 1, true, 'admin', '2019-01-01 10:00:00.000', 'admin', '2019-01-01 10:00:00.000');

-- Create user_role_map
INSERT INTO user_role_map (username, rolename)
VALUES ('admin', 'ADMIN');

Authorization Code(授權碼模式)

(1)調用 http://localhost:10030/server-auth/oauth/authorize?response_type=code&client_id=dummy-client&state=test-state&redirect_uri=https://www.google.com&scope=all

如果使用了Zuul,則可以調用 http://localhost:10020/server-zuul/s3/server-auth/oauth/authorize?response_type=code&client_id=dummy-client&state=test-state&redirect_uri=https://www.google.com&scope=all

如果需要登錄,則需要填寫登錄賬號admin 密碼admin。

(2)選擇scope: all

(3)Get Authorization Code from redirect url: https://www.google.com/?code=ST77hh&state=test-state

(4)Call url with Authorization Code: http://localhost:10030/server-auth/oauth/token?client_id=dummy-client&grant_type=authorization_code&code=ST77hh&redirect_uri=https://www.google.com&scope=all

如果使用了Zuul,則可以調用 http://localhost:10030/server-auth/oauth/token?client_id=dummy-client&grant_type=authorization_code&code=ST77hh&redirect_uri=https://www.google.com&scope=all

(5)Http Basic Authorization: username is dummy-client, password is dummy-client

(6)Get json with token: {"access_token":"8b867ab3-d900-4f1c-947a-b33dc20a91c1","token_type":"bearer","expires_in":43085,"scope":"all"}

Resource Owner Password(密碼模式)

(1)調用 http://localhost:10030/server-auth/oauth/token?client_id=dummy-client&grant_type=password&username=admin&password=admin&scope=all

如果使用了Zuul,則可以調用 http://localhost:10020/server-zuul/s3/server-auth/oauth/token?client_id=dummy-client&grant_type=password&username=admin&password=admin&scope=all

(2)Http Basic Authorization: username is dummy-client, password is dummy-client

(3)Get json with token: {"access_token":"8b867ab3-d900-4f1c-947a-b33dc20a91c1","token_type":"bearer","expires_in":43085,"scope":"all"}

[注1] 如果使用Zuul調用,則需要配置以下內容:

(1)server-auth中的application.yml

## Eureka info
eureka:
  # 如果設置了true並且也設置了eureka.instance.ip-address那么就將此ip地址注冊到Eureka中,那么調用的時候,發送的請求目的地就是此Ip地址
  instance:
    preferIpAddress: true
    ipAddress: localhost

(2)server-zuul中的application.yml

## Zuul info
zuul:
  # Zuul不會將敏感HTTP首部(如Cookie,Set-Cookie,Authorization)轉發到下游服務。它是一個黑名單。這里排除了Cookie,Set-Cookie,Authorization為后面的OAuth2服務
  sensitiveHeaders:

 

其他參數說明

[scope]

如果請求中含有scope

 - Client沒有配置scopes,可以認為是All Scopes,則可以使用(approve)請求的scope

 - Client配置了一組scopes,如果包含請求的scope,則可以使用(approve)請求的scope;如果不包含,則報錯(scope didn't match)

如果請求中沒有scope

 - Client沒有配置scopes,且請求中也沒有,報錯(empty scope is not allowed)

 - Client配置了一組scopes,則可以使用(approve)Client的所有scopes

[注] 注意上面有一個"approve"關鍵字。如果Client為scopes配置了AutoApprove=true,則會跳過approve這一步。

[client_id]

如果在Authorization Server中配置的一個ClientDetails中沒有配置resourceId,則這個Client有訪問所有resource的權限。

如果在Resource Server沒有配置resourceId,則這個resource可以被所有Client有訪問。

如果兩端都配置了,且Client的resourceIds包含Resource Server的resourceId,這個resource才可以被這個Client訪問。

[注] 代碼來源:OAuth2AuthenticationManager#authenticate

[client_secret]

在OAuth2標准中,client_secret並不是required field。但是,在Spring Security中,client_id/client_secret被當作了UserDetails,同樣會調用AuthenticationProvider.authenticate()方法,最終調用DaoAuthenticationProvider.additionalAuthenticationChecks(),再調用PasswordEncoder的match()方法。PasswordEncoder的實現類(比如DelegatingPasswordEncoder,BCryptPasswordEncoder)在驗證空password時都不會通過。

解決方法:可以重寫PasswordEncoder實現類的match()方法,也可以設置client_secret為required field。這里使用后一種。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM