摘要: 使用 OAuth 2.0 認證的的好處是顯然易見的。你只需要用同一個賬號密碼,就能在各個網站進行訪問,而免去了在每個網站都進行注冊的繁瑣過程。 本文將介紹 OAuth 2.0 的原理,並基於 Spring Security 和 GitHub 賬號,來演示 OAuth 2.0 的認證的過程。
原文同步至https://waylau.com/principle-and-practice-of-oauth2/
使用 OAuth 2.0 認證的的好處是顯然易見的。你只需要用同一個賬號密碼,就能在各個網站進行訪問,而免去了在每個網站都進行注冊的繁瑣過程。
本文將介紹 OAuth 2.0 的原理,並基於 Spring Security 和 GitHub 賬號,來演示 OAuth 2.0 的認證的過程。
什么是 OAuth 2.0
OAuth 2.0 的規范可以參考 : RFC 6749
OAuth 是一個開放標准,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用。目前,OAuth 的最新版本為 2.0
OAuth 允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每一個令牌授權一個特定的網站(例如,視頻編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth 允許用戶授權第三方網站訪問他們存儲在另外的服務提供者上的信息,而不需要分享他們的訪問許可或他們數據的所有內容。
OAuth 2.0 的核心概念
OAuth 2.0 主要有4類角色:
- resource owner:資源所有者,指終端的“用戶”(user)
- resource server:資源服務器,即服務提供商存放受保護資源。訪問這些資源,需要獲得訪問令牌(access token)。它與認證服務器,可以是同一台服務器,也可以是不同的服務器。如果,我們訪問新浪博客網站,那么如果使用新浪博客的賬號來登錄新浪博客網站,那么新浪博客的資源和新浪博客的認證都是同一家,可以認為是同一個服務器。如果,我們是新浪博客賬號去登錄了知乎,那么顯然知乎的資源和新浪的認證不是一個服務器。
- client:客戶端,代表向受保護資源進行資源請求的第三方應用程序。
- authorization server: 授權服務器, 在驗證資源所有者並獲得授權成功后,將發放訪問令牌給客戶端。 ## OAuth 2.0 的認證流程
認證流程如下:
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+
- (A)用戶打開客戶端以后,客戶端請求資源所有者(用戶)的授權。
- (B)用戶同意給予客戶端授權。
- (C)客戶端使用上一步獲得的授權,向認證服務器申請訪問令牌。
- (D)認證服務器對客戶端進行認證以后,確認無誤,同意發放訪問令牌。
- (E)客戶端使用訪問令牌,向資源服務器申請獲取資源。
- (F)資源服務器確認令牌無誤,同意向客戶端開放資源。
其中,用戶授權有四種模式:
- 授權碼模式(authorization code)
- 簡化模式(implicit)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
實踐 OAuth 2.0
Talk is cheap!下面將演示代碼。
本例子將通過 Gradle、Spring Boot、Spring Security、 Thymeleaf、等技術來實現一個client 以及 resource server,並 通過 GitHub來給我們的應用授權。
依賴
本項目基於Gralde 來管理依賴,讀者可以自行改成 Maven 的方式:
// 該依賴對於編譯發行是必須的 compile('org.springframework.boot:spring-boot-starter-web') // 添加 Thymeleaf 的依賴 compile('org.springframework.boot:spring-boot-starter-thymeleaf') // 添加 Spring Security 依賴 compile('org.springframework.boot:spring-boot-starter-security') // 添加 Thymeleaf Spring Security 依賴,與 Thymeleaf 版本一致都是 3.x compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.2.RELEASE') // 添加 Spring Security OAuth2 依賴 compile('org.springframework.security.oauth:spring-security-oauth2:2.1.0.RELEASE') // 該依賴對於編譯測試是必須的,默認包含編譯產品依賴和編譯時依 testCompile('org.springframework.boot:spring-boot-starter-test') // 添加 Spring Security Test 依賴 testCompile('org.springframework.security:spring-security-test:4.2.2.RELEASE')
配置
項目的核心配置如下:
github.client.clientId=ad2abbc19b6c5f0ed117 github.client.clientSecret=26db88a4dfc34cebaf196e68761c1294ac4ce265 github.client.accessTokenUri=https://github.com/login/oauth/access_token github.client.userAuthorizationUri=https://github.com/login/oauth/authorize github.client.clientAuthenticationScheme=form github.client.tokenName=oauth_token github.client.authenticationScheme=query github.resource.userInfoUri=https://api.github.com/user
包括了作為一個client 所需要大部分參數。其中 clientId 、 clientSecret 是在 GitHub 注冊一個應用時生成的。如果讀者不想注冊應用,則可以直接用上面的配置即可。
如果要注冊,則文章最后有注冊流程。
項目安全的配置
安全配置上需要加上@EnableWebSecurity
、 @EnableOAuth2Client
注解,來啟用Web 安全認證記憶,表明這是一個OAuth 2.0 客戶端 :
@EnableWebSecurity @EnableOAuth2Client // 啟用 OAuth 2.0 客戶端 public class SecurityConfig extends WebSecurityConfigurerAdapter {
使用 Spring Security,我們需要繼承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
並重寫以下 configure 方法:
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class) .antMatcher("/**") .authorizeRequests() .antMatchers("/", "/index", "/403","/css/**", "/js/**", "/fonts/**").permitAll() // 不設限制,都允許訪問 .anyRequest() .authenticated() .and().logout().logoutSuccessUrl("/").permitAll() .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ; }
上面的配置是設置了一些過過濾策略,除了靜態資源以及不需要授權的頁面,我們允許訪問,其他的資源,都是需要授權訪問。
其中,我們也設置了一個過濾器 ssoFilter,用於在 BasicAuthenticationFilter 之前進行攔截。如果攔截道的是/login
,就是訪問認證服務器。
private Filter ssoFilter() { OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login"); OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext); githubFilter.setRestTemplate(githubTemplate); UserInfoTokenServices tokenServices = new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId()); tokenServices.setRestTemplate(githubTemplate); githubFilter.setTokenServices(tokenServices); return githubFilter; } @Bean public FilterRegistrationBean oauth2ClientFilterRegistration( OAuth2ClientContextFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(filter); registration.setOrder(-100); return registration; } @Bean @ConfigurationProperties("github.client") public AuthorizationCodeResourceDetails github() { return new AuthorizationCodeResourceDetails(); } @Bean @ConfigurationProperties("github.resource") public ResourceServerProperties githubResource() { return new ResourceServerProperties(); }
資源服務器
我們寫了兩個控制器來提供相應的資源。
MainController.java
@Controller public class MainController { @GetMapping("/") public String root() { return "redirect:/index"; } @GetMapping("/index") public String index(Principal principal, Model model) { if(principal == null ){ return "index"; } System.out.println(principal.toString()); model.addAttribute("principal", principal); return "index"; } @GetMapping("/403") public String accesssDenied() { return "403"; } }
在index 頁面,將如認證成功,將會顯示一些認證信息。
UserController.java 是用來模擬用戶管理的相關資源。
@RestController
@RequestMapping("/") public class UserController { /** * 查詢所用用戶 * @return */ @GetMapping("/users") @PreAuthorize("hasAuthority('ROLE_USER')") // 指定角色權限才能操作方法 public ModelAndView list(Model model) { List<User> list = new ArrayList<>(); // 當前所在頁面數據列表 list.add(new User("waylau",29)); list.add(new User("老衛",30)); model.addAttribute("title", "用戶管理"); model.addAttribute("userList", list); return new ModelAndView("users/list", "userModel", model); } }
前端頁面
頁面,我主要是采用 Thymeleaf 以及Bootstrap 來編寫的。
首頁用於現實用戶的基本信息。
<body> <div class="container"> <div class="mt-3"> <h2>Hello Spring Security</h2> </div> <div sec:authorize="isAuthenticated()" th:if="${principal}" th:object="${principal}"> <p>已有用戶登錄</p> <p>登錄的用戶為: <span sec:authentication="name"></span></p> <p>用戶權限為: <span th:text="*{userAuthentication.authorities}"></span></p> <p>用戶頭像為: <img alt="" class="avatar width-full rounded-2" height="230" th:src="*{userAuthentication.details.avatar_url}" width="230"></p> </div> <div sec:authorize="isAnonymous()"> <p>未有用戶登錄</p> </div> </div> </body>
用戶管理界面顯示用戶的列表:
<body> <div class="container"> <div class="mt-3"> <h2 th:text="${userModel.title}">Welcome to waylau.com</h2> </div> <table class="table table-hover"> <thead> <tr> <td>Age</td> <td>Name</td> <td sec:authorize="hasRole('ADMIN')">Operation</td> </tr> </thead> <tbody> <tr th:if="${userModel.userList.size()} eq 0"> <td colspan="3">沒有用戶信息!!</td> </tr> <tr th:each="user : ${userModel.userList}"> <td th:text="${user.age}">11</td> <td th:text="${user.name}">waylau</a></td> <td sec:authorize="hasRole('ADMIN')"> <div > 我是管理員 </div> </td> </tr> </tbody> </table> </body>
運行效果
這個是沒有授權訪問首頁:
當我們點擊登錄,會重定向到 GitHub,登錄界面並進行授權:
這個是授權后的首頁:
授權后就能夠進入用戶管理界面:
注冊GitHub 應用
如果需要注冊,請看下面的流程,來生成 Client ID 和 Client Secret
訪問https://github.com/settings/applications/new
注冊應用,生成 客戶端 id 和 密碼。比如:
Client ID :ad2abbc19b6c5f0ed117 Client Secret :26db88a4dfc34cebaf196e68761c1294ac4ce265
客戶端 id 和 密碼寫入程序配置即可。
源碼
- 《Spring Security 教程》:https://github.com/waylau/spring-security-tutorial
https://yq.aliyun.com/articles/72652?spm=5176.100239.blogcont72216.32.TJsaZ6