Oauth2認證模式之授權碼模式實現
- 2019 年 10 月 3 日
- 筆記
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 } ] }