> 如果不理解oauth協議的推薦閱讀 阮一峰的[理解OAuth 2.0](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
當然,我們也要簡單介紹下oauth的運行流程:
```
+--------+ +---------------+
| |--(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 ---| |
+--------+ +---------------+
```
運行流程如下圖,摘自RFC 6749。
- (A)用戶打開客戶端以后,客戶端要求用戶給予授權。
- (B)用戶同意給予客戶端授權。
- (C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。
- (D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。
- (E)客戶端使用令牌,向資源服務器申請獲取資源。
- (F)資源服務器確認令牌無誤,同意向客戶端開放資源。
我們是對內的系統,并不需要那么復雜的流程,所以我們看下oauth的授權模式當中的密碼模式:
```
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
```
這里的流程相對就比較簡單了:
(A)用戶向客戶端提供用戶名和密碼。
(B)客戶端將用戶名和密碼發給認證服務器,向后者請求令牌。
(C)認證服務器確認無誤后,向客戶端提供訪問令牌。
現在將簡單的轉換下思路:
- `Resource Owner`:資源擁有者,擁有訂單,購物車等數據的人,既用戶
- `Client`:客戶端,瀏覽器
- `Authorization Server`:認證服務器,也就是服務器咯。
在此A、B、C三個流程就變成了:
(A)用戶在瀏覽器輸入用戶名和密碼。
(B)瀏覽器將用戶名和密碼發給服務器,向后者請求令牌(token)。
(C)服務器確認無誤后,返回token給用戶。
但是根據標準的流程,并沒有驗證碼之類的容身之地。而`spring security oauth2` 給我們提供的只能是標準的流程,所以我們對代碼進行一些適配,能夠適應我們自己的需求。
## spring的部分源碼
我們先來看下`spring security oauth2`的部分源碼
首先我們直接進行授權的時候,調用的url大概為:`http://localhost:8080/oauth/token?username=user_1&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=123456`,那么授權肯定是與該鏈接相關聯的。基于這個猜測,我們去尋找源碼吧。
在`idea`中使用全局搜索,搜索 字符串`"/oauth/token"`(帶著引號),發現了一個類,似乎與這個請求有關 `ClientCredentialsTokenEndpointFilter`
```java
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
public ClientCredentialsTokenEndpointFilter() {
this("/oauth/token");
}
}
```
```
ClientCredentialsTokenEndpointFilter
---> AbstractAuthenticationProcessingFilter
---> GenericFilterBean
---> Filter
```
發現,這個類是一個 `Filter` 也就是過濾器,通過這個過濾器,過濾請求,那么,我們去看看`doFilter`方法咯,`doFilter` 在 `ClientCredentialsTokenEndpointFilter` 的父類 `AbstractAuthenticationProcessingFilter` 上。
我們看看`AbstractAuthenticationProcessingFilter`:
```java
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 如果不是認證的請求,直接下一個filter
// 這里是怎么判斷是否是下一個請求呢?
// 答:看看url是不是上面ClientCredentialsTokenEndpointFilter 創建時傳過來的url,也就是 /oauth/token
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
// 調用attemptAuthentication 方法,返回一個 Authentication 的實現類,也就是認證信息,這個實現類非常重要!!!
authResult = attemptAuthentication(request, response);
// 如果找不到,那就沒了
if (authResult == null) {
return;
}
}
// 調用成功的方法
successfulAuthentication(request, response, chain, authResult);
}
```
這里最重要的方法`attemptAuthentication` 生成一個授權信息,能夠返回,則證明登錄已經成功了,所以真正的登錄與這里有關。
我們回到`ClientCredentialsTokenEndpointFilter` 這個實現類里面看看`attemptAuthentication`方法吧
```java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// ======精簡沒啥用的方法========
// 構造一個UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
// 調用認證方法進行認證
return this.getAuthenticationManager().authenticate(authRequest);
}
```
我們通過添加斷點可以發現 `this.getAuthenticationManager()` 是一個`ProviderManager` 對象,我們看下
`this.getAuthenticationManager().authenticate()` 里面的 `authenticate`
```java
public class ProviderManager{
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Authentication result = null;
for (AuthenticationProvider provider : getProviders()) {
// 在一堆的provider中尋找到一個合適的授權提供者
if (!provider.supports(toTest)) {
continue;
}
// 由授權提供者進行授權
result = provider.authenticate(authentication);
}
if (result != null) {
return result;
}
}
}
```
一路追蹤到這里,我們發現,實際上,是通過`provider.supports(toTest)` 尋找一個合適的授權提供者,使用`provider.authenticate(authentication)`就行授權,而`supports` 的依據是通過之前生成的token來判斷是否支持:
```java
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
```
我們整理下這幾個流程
```
ClientCredentialsTokenEndpointFilter.doFilter()
--> AbstractAuthenticationProcessingFilter.attemptAuthentication()
--> ProviderManager.authenticate()
--> AuthenticationProvider.supports()
--> AuthenticationProvider.authenticate()
```
我們可以看到這里主要就是干了幾件事情
- 通過filter 確定登錄要過濾的url
- 通過filter 確定生成的`AbstractAuthenticationToken` 比如 `UsernamePasswordAuthenticationToken`
- 通過生成的`AbstractAuthenticationToken` 確定`AuthenticationProvider`
- 通過`AuthenticationProvider` 最后調用 `authenticate()`方法最后進行授權
最后通過`RequestMapping` 返回
```java
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint{
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
return getResponse(token);
}
}
```
- 開發環境準備
- 基本開發手冊
- 項目目錄結構
- 權限管理
- 通用分頁表格
- Swagger文檔
- undertow容器
- 對xss攻擊的防御
- 分布式鎖
- 統一的系統日志
- 統一驗證
- 統一異常處理
- 文件上傳下載
- 一對多、多對多分頁
- 認證與授權
- 從授權開始看源碼
- 自己寫個授權的方法-開源版
- 商城表設計
- 商品信息
- 商品分組
- 購物車
- 訂單
- 地區管理
- 運費模板
- 接口設計
- 必讀
- 購物車的設計
- 訂單設計-確認訂單
- 訂單設計-提交訂單
- 訂單設計-支付
- 生產環境
- nginx安裝與跨域配置
- 安裝mysql
- 安裝redis
- 傳統方式部署項目
- docker
- 使用docker部署商城
- centos jdk安裝
- docker centos 安裝
- Docker Compose 安裝與卸載
- docker 鏡像的基本操作
- docker 容器的基本操作
- 通過yum安裝maven
- 常見問題