|
@@ -0,0 +1,251 @@
|
|
|
+> 如果不理解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);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|