@html
開放受權(Open Authorization,OAuth)是一種資源提供商用於受權第三方應用表明資源全部者獲取有限訪問權限的受權機制。因爲在整個受權過程當中,第三方應用都無須觸及用戶的密碼就能夠取得部分資源的使用權限,因此OAuth是安全開放的。前端
例如,用戶想經過 QQ 登陸csdn,這時csdn就是一個第三方應用,csdn要訪問用戶的一些基本信息就須要獲得用戶的受權,若是用戶把本身的 QQ 用戶名和密碼告訴csdn,那麼csdn就能訪問用戶的全部數據,井且只有用戶修改密碼才能收回受權,這種受權方式安全隱患很大,若是使用 OAuth ,就能很好地解決這一問題。java
OAuth第一個版本誕生於2007年12月,並於2010年4月正式被IETF做爲標準發佈(編號RFC 5849)。因爲OAuth1.0複雜的簽名邏輯以及單一的受權流程存在較大缺陷,隨後標準工做組又推出了 OAuth2.0草案,並在2012年10月正式發佈其標準(編號RFC 6749)。OAuth2.0放棄了OAuth1.0中讓開發者感到痛苦的數字簽名和加密方案,使用已經獲得驗證並普遍使用的HTTPS技術做爲安全保障手 段。OAuth2.0與OAuth1.0互不兼容,因爲OAuth1.0已經基本退出歷史舞臺,因此下面提到的OAuth都是指OAuth2.0。git
想要理解OAuth的運行流程,則必需要認識4個重要的角色。github
這是 個大體的流程,由於 OAuth2 中有 種不一樣的受權模式,每種受權模式的受權流程又會有差別,基本流程以下:web
OAuth 協議的受權模式共分爲4種。面試
受權碼(authorization code)方式,指的是第三方應用先申請一個受權碼,而後再用該碼獲取令牌。spring
這種方式是最經常使用的流程,安全性也最高,它適用於那些有後端的 Web 應用。受權碼經過前端傳送,令牌則是儲存在後端,並且全部與資源服務器的通訊都在後端完成。這樣的先後端分離,能夠避免令牌泄漏。數據庫
https://b.com/oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type參數表示要求返回受權碼(code),client_id參數讓 B 知道是誰在請求,redirect_uri參數是 B 接受或拒絕請求後的跳轉網址,scope參數表示要求的受權範圍(這裏是只讀)。json
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code參數就是受權碼。
https://b.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL
上面 URL 中,client_id 參數和 client_secret 參數用來讓 B 確認 A 的身份(client_secret參數是保密的,所以只能在後端發請求),grant_type參數的值是 AUTHORIZATION_CODE,表示採用的受權方式是受權碼,code參數是上一步拿到的受權碼,redirect_uri 參數是令牌頒發後的回調網址。
{ "access_token":"ACCESS_TOKEN", "token_type":"bearer", "expires_in":2592000, "refresh_token":"REFRESH_TOKEN", "scope":"read", "uid":100101, "info":{...} }
上面 JSON 數據中,access_token字段就是令牌,A 網站在後端拿到了。
有些 Web 應用是純前端應用,沒有後端。這時就不能用上面的方式了,必須將令牌儲存在前端。RFC 6749 就規定了第二種方式,容許直接向前端頒發令牌。這種方式沒有受權碼這個中間步驟,因此稱爲(受權碼)"隱藏式"(implicit)。
https://b.com/oauth/authorize? response_type=token& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type參數爲token,表示要求直接返回令牌。
https://a.com/callback#token=ACCESS_TOKEN
上面 URL 中,token參數就是令牌,A 網站所以直接在前端拿到令牌。
注意,令牌的位置是 URL 錨點(fragment),而不是查詢字符串(querystring),這是由於 OAuth 2.0 容許跳轉網址是 HTTP 協議,所以存在"中間人攻擊"的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減小了泄漏令牌的風險。
這種方式把令牌直接傳給前端,是很不安全的。所以,只能用於一些安全要求不高的場景,而且令牌的有效期必須很是短,一般就是會話期間(session)有效,瀏覽器關掉,令牌就失效了。
若是你高度信任某個應用,RFC 6749 也容許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌,這種方式稱爲"密碼式"(password)。
https://oauth.b.com/token? grant_type=password& username=USERNAME& password=PASSWORD& client_id=CLIENT_ID
上面 URL 中,grant_type參數是受權方式,這裏的password表示"密碼式",username和password是 B 的用戶名和密碼。
最後一種方式是憑證式(client credentials),適用於沒有前端的命令行應用,即在命令行下請求令牌。
https://oauth.b.com/token? grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
上面 URL 中,grant_type參數等於client_credentials表示採用憑證式,client_id和client_secret用來讓 B 確認 A 的身份。
這種方式給出的令牌,是針對第三方應用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。
若是是自建單點服務,通常都會使用密碼模式。資源服務器和受權服務器
能夠是同一臺服務器,也能夠分開。這裏咱們學習分佈式的狀況。
受權服務器和資源服務器分開,項目結構以下:
受權服務器的職責:
<!--security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--oauth2--> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.6.RELEASE</version> </dependency>
受權服務器配置經過繼承AuthorizationServerConfigurerAdapter的配置類實現:
/** * @Author 三分惡 * @Date 2020/5/20 * @Description 受權服務器配置 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager;//密碼模式須要注入認證管理器 @Autowired public PasswordEncoder passwordEncoder; //配置客戶端 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client-demo") .secret(passwordEncoder.encode("123")) .authorizedGrantTypes("password") //這裏配置爲密碼模式 .scopes("read_scope"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager);//密碼模式必須添加authenticationManager } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients() .checkTokenAccess("isAuthenticated()"); } }
經過Spring Security來完成用戶及密碼加解密等配置:
/** * @Author 三分惡 * @Date 2020/5/20 * @Description SpringSecurity 配置 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("fighter") .password(passwordEncoder().encode("123")) .authorities(new ArrayList<>(0)); } @Override protected void configure(HttpSecurity http) throws Exception { //全部請求必須認證 http.authorizeRequests().anyRequest().authenticated(); } }
資源服務器的職責:
資源服務器依賴同樣,而配置則經過繼承自ResourceServerConfigurerAdapter的配置類來實現:
/** * @Author 三分惡 * @Date 2020/5/20 * @Description */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Bean public RemoteTokenServices remoteTokenServices() { final RemoteTokenServices tokenServices = new RemoteTokenServices(); tokenServices.setClientId("client-demo"); tokenServices.setClientSecret("123"); tokenServices.setCheckTokenEndpointUrl("http://localhost:8090/oauth/check_token"); return tokenServices; } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.stateless(true); } @Override public void configure(HttpSecurity http) throws Exception { //session建立策略 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); //全部請求須要認證 http.authorizeRequests().anyRequest().authenticated(); } }
主要進行了以下配置:
接口比較簡單:
/** * @Author 三分惡 * @Date 2020/5/20 * @Description */ @RestController public class ResourceController { @GetMapping("/user/{username}") public String user(@PathVariable String username){ return "Hello !"+username; } }
受權服務器使用8090端口啓動,資源服務器使用默認端口。
訪問/oauth/token端點,獲取token:
至關於在Headers中添加 Authorization:Bearer 4a3c351d-770d-42aa-af39-3f54b50152e9。
OK,能夠看到資源正確返回。
這裏僅僅是密碼模式的精簡化配置,在實際項目中,某些部分如:
- 資源服務訪問受權服務去校驗token這部分可能會換成Jwt、Redis等tokenStore實現,
- 受權服務器中的用戶信息與客戶端信息生產環境從數據庫中讀取,對應Spring Security的UserDetailsService實現類或用戶信息的Provider
不少網站登陸時,容許使用第三方網站的身份,這稱爲"第三方登陸"。所謂第三方登陸,實質就是 OAuth 受權。
例如用戶想要登陸 A 網站,A 網站讓用戶提供第三方網站的數據,證實本身的身份。獲取第三方網站的身份數據,就須要 OAuth 受權。
以A網站使用GitHub第三方登陸爲例,流程示意以下:
接下來,簡單地實現GitHub登陸流程。
在使用以前須要先註冊一個應用,讓GitHub能夠識別。
應用的名稱隨便填,主頁 URL 填寫http://localhost:8080,回調地址填寫 http://localhost:8080/oauth/redirect。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
github.client.clientId=29d127aa0753c12263d7 github.client.clientSecret=f3cb9222961efe4c2adccd6d3e0df706972fa5eb github.client.authorizeUrl=https://github.com/login/oauth/authorize github.client.accessTokenUrl=https://github.com/login/oauth/access_token github.client.redirectUrl=http://localhost:8080/oauth/redirect github.client.userInfoUrl=https://api.github.com/user
@Component @ConfigurationProperties(prefix = "github.client") public class GithubProperties { private String clientId; private String clientSecret; private String authorizeUrl; private String redirectUrl; private String accessTokenUrl; private String userInfoUrl; //省略getter、setter }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>網站首頁</title> </head> <body> <div style="text-align: center"> <a href="http://localhost:8080/authorize">Login in with GitHub</a> </div> </body> </html>
@Controller public class GithubLoginController { @Autowired GithubProperties githubProperties; /** * 登陸接口,重定向至github * * @return 跳轉url */ @GetMapping("/authorize") public String authorize() { String url =githubProperties.getAuthorizeUrl() + "?client_id=" + githubProperties.getClientId() + "&redirect_uri=" + githubProperties.getRedirectUrl(); return "redirect:" + url; } /** * 回調接口,用戶贊成受權後,GitHub會將受權碼傳遞給此接口 * @param code GitHub重定向時附加的受權碼,只能用一次 * @return */ @GetMapping("/oauth/redirect") @ResponseBody public String redirect(@RequestParam("code") String code) throws JsonProcessingException { System.out.println("code:"+code); // 使用code獲取token String accessToken = this.getAccessToken(code); // 使用token獲取userInfo String userInfo = this.getUserInfo(accessToken); return userInfo; } /** * 使用受權碼獲取token * @param code * @return */ private String getAccessToken(String code) throws JsonProcessingException { String url = githubProperties.getAccessTokenUrl() + "?client_id=" + githubProperties.getClientId() + "&client_secret=" + githubProperties.getClientSecret() + "&code=" + code + "&grant_type=authorization_code"; // 構建請求頭 HttpHeaders requestHeaders = new HttpHeaders(); // 指定響應返回json格式 requestHeaders.add("accept", "application/json"); // 構建請求實體 HttpEntity<String> requestEntity = new HttpEntity<>(requestHeaders); RestTemplate restTemplate = new RestTemplate(); // post 請求方式 ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class); String responseStr = response.getBody(); // 解析響應json字符串 ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(responseStr); String accessToken = jsonNode.get("access_token").asText(); System.out.println("accessToken:"+accessToken); return accessToken; } /** * * @param accessToken 使用token獲取userInfo * @return */ private String getUserInfo(String accessToken) { String url = githubProperties.getUserInfoUrl(); // 構建請求頭 HttpHeaders requestHeaders = new HttpHeaders(); // 指定響應返回json格式 requestHeaders.add("accept", "application/json"); // AccessToken放在請求頭中 requestHeaders.add("Authorization", "token " + accessToken); // 構建請求實體 HttpEntity<String> requestEntity = new HttpEntity<>(requestHeaders); RestTemplate restTemplate = new RestTemplate(); // get請求方式 ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); String userInfo = response.getBody(); System.out.println("userInfo:"+userInfo); return userInfo; } }
http://localhost:8080/oauth/redirect?code=d45683eded3ac7d4e6ed
OK,用戶信息也一併返回了。
本文爲學習筆記類博客,學習資料見參考!
參考:
【1】:《SpringSecurity 實戰》
【2】:《SpringBoot Vue全棧開發實戰》
【3】:理解OAuth 2.0
【4】:OAuth 2.0 的一個簡單解釋
【5】:OAuth 2.0 的四種方式
【6】:這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登陸流程?
【7】:作微服務繞不過的 OAuth2,鬆哥也來和你們扯一扯
【8】:GitHub OAuth 第三方登陸示例教程
【9】:OAuth 2.0 認證的原理與實踐
【10】:Spring Security OAuth2 Demo —— 密碼模式(Password)
【11】:Spring Security OAuth專題學習-密碼模式及客戶端模式實例
【12】:Spring Boot and OAuth2
【13】:Spring Boot+OAuth2使用GitHub登陸本身的服務