OAuth2 密碼模式


#  OAuth2 密碼模式

1 回顧

前面說了一些OAuth2的概念和一些OAuth2流程,現在根據之前的流程,用代碼簡單的實現下這個過程

不過下面的代碼有些是有關SpringSecurity相關的知識,如果想補充這部分知識,請移步江南一點雨關於SpringSecurity的文章,后續文章會寫到這些SpringSecurity的知識。

簡單回顧一下密碼模式的流程:

客戶端帶着用戶名 密碼 還有client_id client_secret等。授權服務器校驗客戶端信息和用戶信息,校驗通過后返回token,客戶端帶着這個token請求資源服務器,資源服務器校驗通過后返回資源

話不多說,開始上代碼

2 引入相關依賴

首先現在准備一個授權服務器

springcloud 已經集成了oauth2和springsecurity,那就用這個吧

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
    </properties>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <scope>compile</scope>
        </dependency>
        <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

3 SpringSecurity配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //密碼管理器,可以認為是時間戳+鹽 加密的一種方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * 配置authenticationManager->providerManager->authenticationProvider->UserdetailServices->userDetails(存放的是用戶信息)-》最終設置到
     * SpringSecurityContextHolder
     * 所以我們可以通過UserDetailService來得到用戶信息,也可以將用信息存儲在內存中,
     * 像下面這樣:可以在這里配置一些用戶名和密碼,以及用戶所對應的權限
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().
                withUser("hxx").
                password(passwordEncoder().encode("123456")).authorities(Collections.emptyList())
                .and().
                withUser("wm").
                password(passwordEncoder().encode("123456")).
                authorities(new ArrayList<>(0));
    }

    //配置http
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //任何請求都需要驗證
        http.authorizeRequests().anyRequest().authenticated();
    }

    //配置web資源
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
}

用戶名和密碼暫時也是存在了內存中

4 授權服務器的配置

授權服務器需要繼承AuthorizationServerConfigurerAdapter。並且開啟授權服務

@Configuration
@EnableAuthorizationServer //開啟授權服務
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;



    @Autowired
    private PasswordEncoder passwordEncoder;

    //配置客戶端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().
                withClient("client1").
                secret(passwordEncoder.encode("client_secret"))
                .authorizedGrantTypes("password").
                scopes("read_scope");
    }

    //配置安全約束
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients().checkTokenAccess("isAuthenticated()");
//                .tokenKeyAccess("permitAll()");
    }

	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }


}

授權服務器主要有如下的配置:

  1. configure(ClientDetailsServiceConfigurer clients)配置客戶端詳細信息 client_id client_secret grant_type(上面這些信息是放到內存中的)
  2. configure(AuthorizationServerSecurityConfigurer security) 配置端點安全約束
  1. configure(AuthorizationServerEndpointsConfigurer endpoints)配置訪問令牌的端點和令牌服務
  1. oauth2中開放的幾個重要的端點:
    1. 訪問令牌token的端點:/oauth/token
    2. 校驗令牌的端點:/oauth/check_token
    1. 授權端點:/oauth/authorize

 

現在說這些是也不太直觀,待會說

5 配置資源服務器配置

准備一個資源服務

資源服務配置文件

@EnableResourceServer
@Configuration
public class ResourcesServerConfig extends ResourceServerConfigurerAdapter {


    @Bean
    public RemoteTokenServices remoteTokenServices(){
        final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8081/oauth/check_token");
        remoteTokenServices.setClientId("client1");
        remoteTokenServices.setClientSecret("client_secret");
        return remoteTokenServices;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().
//                antMatchers("/getUser").hasRole("admin").
                anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer   resources) throws Exception {
        resources.tokenServices(remoteTokenServices());
    }
}

配置資源服務器:

  1. 這里主要配置了一個remoteTokenServices,主要是用來進行遠程調用/oauth/check_token端點進行校驗前端傳遞的access_token
  2. 然后配置了一下資源攔截,和SpringSecurity的配置一樣

寫個接口進行測試

@RestController
public class HelloController {

    @GetMapping("/getUser")
    public String getUser(){
        return "hello  me";
    }
}

6 開始測試

首先啟動資源服務和授權服務

6.1 訪問/oauth/token端點

通過用戶名和密碼 client_id client_secret grant_type訪問獲取token的端點:/oauth/token

得到如下的結果:

 

再看后台日志:

以下是源碼分析

6.1.1 源碼分析

意思是訪問oauth/token端點的時候,請求到了TokenEndpoint的postAccessToken方法

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

	private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();

	private Set<HttpMethod> allowedRequestMethods = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.POST));

...//省略
	
	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					"There is no client authentication. Try adding an appropriate authentication filter.");
		}

		String clientId = getClientId(principal);
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

		if (clientId != null && !clientId.equals("")) {
			// Only validate the client details if a client authenticated during this
			// request.
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException("Given client ID does not match authenticated client");
			}
		}
		...//省略
      OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}

		return getResponse(token);

	}
    ...//省略
}

下面開始分析端點/oauth/token 是如何通過用戶名密碼client信息等來換取令牌端點的

可以看到:

  1. 首先從clientDetailsService里面獲取客戶端信息clientDetails ,然后進行校驗前端傳遞的clientId client_secret是否匹配,檢查grant_type scope。然后通過getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)授權,這個授權主要是先校驗信息的正確性

  1. 在AuthorizationServerEndPointsConfigurer里面的tokenGranter 里面的授權grant方法
private TokenGranter tokenGranter() {
        if (this.tokenGranter == null) {
            this.tokenGranter = new TokenGranter() {
                private CompositeTokenGranter delegate;

                public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
                    if (this.delegate == null) {
                        this.delegate = new CompositeTokenGranter(AuthorizationServerEndpointsConfigurer.this.getDefaultTokenGranters());
                    }

                    return this.delegate.grant(grantType, tokenRequest);
                }
            };
        }

        return this.tokenGranter;
    }
  1. 走到CompositeTokenGranter的grant方法,通過一個個tokenGranter去授權
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		for (TokenGranter granter : tokenGranters) {
			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
			if (grant!=null) {
				return grant;
			}
		}
		return null;
	}
  1. 再通過AbstractTokenGranter,獲取AccessToken
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

		if (!this.grantType.equals(grantType)) {
			return null;
		}
		
		String clientId = tokenRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);

		if (logger.isDebugEnabled()) {
			logger.debug("Getting access token for: " + clientId);
		}

		return getAccessToken(client, tokenRequest);

	}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
	}
  1. 再看getOAuth2Authentication方法,這個方法是用來校驗用戶名和密碼的正確性,正確就返回authentication:
    找到ResourceOwnerPasswordTokenGranter#getOAuth2Authentication,里面就是通過SpringSecurity的流程去校驗username 和password(用戶名和密碼)了,校驗通過后就會返回一個OAuth2Authentication。
@Override
	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

		Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
		String username = parameters.get("username");
		String password = parameters.get("password");
		// Protect from downstream leaks of password
		parameters.remove("password");

		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
		try {
			userAuth = authenticationManager.authenticate(userAuth);
		}
		catch (AccountStatusException ase) {
			//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
			throw new InvalidGrantException(ase.getMessage());
		}
		catch (BadCredentialsException e) {
			// If the username/password are wrong the spec says we should send 400/invalid grant
			throw new InvalidGrantException(e.getMessage());
		}
		if (userAuth == null || !userAuth.isAuthenticated()) {
			throw new InvalidGrantException("Could not authenticate user: " + username);
		}
		
		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		
		return new OAuth2Authentication(storedOAuth2Request, userAuth);
	}
  1. 校驗通過后,然后通過默認的tokenService 利用上面返回的authentication,生成一個access_token相關信息的類OAuth2AccessToken
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
		DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
		int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
		if (validitySeconds > 0) {
			token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
		}
		token.setRefreshToken(refreshToken);
		token.setScope(authentication.getOAuth2Request().getScope());

		return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
	}

這樣通過訪問/oauth/token端點得到access_token就結束了。

總結:

密碼模式下,就是通過用戶名和密碼還有客戶端信息訪問令牌端點得到access_token,在請求授權服務器令牌端點時候,授權服務器會去校驗用戶名和密碼是否匹配,客戶端id和客戶端secret是否匹配等。校驗通過后,就會通過默認的tokenService生成一個Auth2AccessToken對象,返回access_token相關信息

6.2 通過access_token訪問資源

訪問/getUser ,在header頭部里面添加Authorization ->Bearer "access_token",返回了hello me

資源服務器里面的接口

@RestController
public class HelloController {

    @GetMapping("/getUser")
    public String getUser(){
        return "hello  me";
    }
}

6.2.1 源碼分析

瀏覽器帶着access_token 請求資源服務器,資源服務器的首先會校驗是否已授權,由上面資源服務器的配置可知,它是通過遠程調用授權服務器的/oauth/check_token端點來進行校驗的,來看下這個端點,可知,這個端點是在CheckTokenEndpoint#checkToken(String)下面

找到CheckTokenEndpoint

@FrameworkEndpoint
public class CheckTokenEndpoint {

	private ResourceServerTokenServices resourceServerTokenServices;

	private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();

	...//省略

	@RequestMapping(value = "/oauth/check_token")
	@ResponseBody
	public Map<String, ?> checkToken(@RequestParam("token") String value) {

		OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
		if (token == null) {
			throw new InvalidTokenException("Token was not recognised");
		}

		if (token.isExpired()) {
			throw new InvalidTokenException("Token has expired");
		}

		OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

		Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);

		// gh-1070
		response.put("active", true);	// Always true if token exists and not expired

		return response;
	}
	...//省略
}

打個斷點可以知道,resourceServerTokenServices.readAccessToken(value);實際上是通過默認的tokenServices方法來獲取accessToken的,默認的TokenService又通過InMemoryTokenStore去讀取access_token

 

從內存TokenStore中獲取得到的access_token相關的信息

然后封裝成authentication返回,至此,/oauth/check_token斷點在經過FilterSecurityInterceptor這層過濾器攔截通過之后,此時請求資源服務器/getUser得到了相應的資源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM