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
}
]
}