Oauth2認證模式之授權碼模式實現

雲天發表於2019-08-12

Oauth2認證模式之授權碼模式(authorization code)

本示例實現了Oauth2之授權碼模式,授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺伺服器,與"服務提供商"的認證伺服器進行互動。

閱讀本示例之前,你需要先有以下兩點基礎:

  • 需要對spring security有一定的配置使用經驗,使用者認證這一塊,spring security oauth2建立在spring security的基礎之上
  • oauth2開放授權標準基礎,可以穩步到OAuth2 詳解,瀏覽下授權碼模式,理解下基本概念

概述

實現 oauth2,可以簡易的分為三個步驟

  • 配置資源伺服器
  • 配置認證伺服器
  • 配置spring security

程式碼實現

1.pom.xml新增maven依賴

    <dependencies>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

2.配置資源伺服器

public class ResourceServerConfig {

    private static final String RESOURCE_ID = "account";

    @Configuration
    @EnableResourceServer()
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.resourceId(RESOURCE_ID).stateless(true);
        }

        @Override
        public void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity
                    .requestMatchers()
                    // 保險起見,防止被主過濾器鏈路攔截
                    .antMatchers("/account/**").and()
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                    .authorizeRequests()
                    .antMatchers("/account/info/**").access("#oauth2.hasScope('get_user_info')")
                    .antMatchers("/account/child/**").access("#oauth2.hasScope('get_childlist')");
        }
    }
}

3.配置認證伺服器

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client1")
                .resourceIds(RESOURCE_ID)
                .authorizedGrantTypes("authorization_code", "refresh_token", "implicit")
                .authorities("ROLE_CLIENT")
                .scopes("get_user_info", "get_childlist")
                .secret("secret")
                .redirectUris("http://localhost:8081/client/account/redirect")
                .autoApprove(true)
                .autoApprove("get_user_info");
    }

4.配置spring security

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 建立兩個記憶體使用者
        manager.createUser(User.withUsername("admin").password("123456").authorities("USER").build());
        manager.createUser(User.withUsername("lin").password("123456").authorities("USER").build());
        return manager;
    }

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

    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 密碼生成器(預設為bcrypt模式)
     *
     * @return
     */
//    @Bean
//    PasswordEncoder passwordEncoder() {
//        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
//    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity.
                requestMatchers()
                // 必須登入過的使用者才可以進行 oauth2 的授權碼申請
                .antMatchers("/", "/home", "/login", "/oauth/authorize")
                .and()
                .authorizeRequests()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
                .and()
                .httpBasic()
                .disable()
                .exceptionHandling()
                .accessDeniedPage("/login?authorization_error=true")
                .and()
                .csrf()
                .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/oauth/authorize"))
                .disable();
    }
}

使用介紹

  • 找到AuthResServerApplication.java執行server服務,預設埠:8080
  • 找到ClientApplication.java執行client客戶端,埠:8081

1.嘗試直接訪問使用者資訊

http://localhost:8080/account/info/testAccount1/

返回未授權錯誤

<oauth>
<error_description>
Full authentication is required to access this resource
</error_description>
<error>unauthorized</error>
</oauth>

2.嘗試獲取授權碼

http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/client/account/redirect

結果被主過濾器攔截,302 跳轉到登入頁,因為 /oauth/authorize 端點是受保護的端點,必須登入的使用者才能申請 code。

3.輸入使用者名稱和密碼

輸入使用者名稱和密碼 admin 123456
如上使用者名稱密碼是交給 SpringSecurity 的主過濾器用來認證的

4.登入成功後,真正進行授權碼的申請

oauth/authorize 認證成功,會根據 redirect_uri 執行 302 重定向,並且帶上生成的 code,注意重定向到的是 8001 埠,這個時候已經是另外一個應用了。

localhost:8081/client/account/redirect?code=xxxx
程式碼中封裝了一個 http 請求,使得 client1 使用 restTemplate 向 server 傳送 token 的申請,當然是使用 code 來申請的,並最終成功獲取到 access_token

{
access_token: "59a25558-f714-4ca8-aa87-c36f93c120bf",
token_type: "bearer",
refresh_token: "92436849-7ef7-4923-8270-5a2c9b464556",
expires_in: 43199,
scope: "get_user_info get_childlist"
}

5.攜帶 access_token 訪問account資訊

http://localhost:8080/account/info/testAccount1?access_token=59a25558-f714-4ca8-aa87-c36f93c120bf

6.正常返回資訊

{
name: "testAccount1",
nickName: "測試使用者1",
remark: "備註1",
childAccount: [
{
name: "testChild1_0",
nickName: "測試子使用者1_0",
remark: "0",
childAccount: null
},
{
name: "testChild1_1",
nickName: "測試子使用者1_1",
remark: "1",
childAccount: null
},
{
name: "testChild1_2",
nickName: "測試子使用者1_2",
remark: "2",
childAccount: null
},
{
name: "testChild1_3",
nickName: "測試子使用者1_3",
remark: "3",
childAccount: null
},
{
name: "testChild1_4",
nickName: "測試子使用者1_4",
remark: "4",
childAccount: null
},
{
name: "testChild1_5",
nickName: "測試子使用者1_5",
remark: "5",
childAccount: null
},
{
name: "testChild1_6",
nickName: "測試子使用者1_6",
remark: "6",
childAccount: null
},
{
name: "testChild1_7",
nickName: "測試子使用者1_7",
remark: "7",
childAccount: null
},
{
name: "testChild1_8",
nickName: "測試子使用者1_8",
remark: "8",
childAccount: null
},
{
name: "testChild1_9",
nickName: "測試子使用者1_9",
remark: "9",
childAccount: null
}
]
}

資料

相關文章