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.嘗試獲取授權碼
結果被主過濾器攔截,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
}
]
}