使用Spring Boot構建獨立的OAuth服務器(二)

在 使用Spring Boot構建獨立的OAuth服務器(一) 中,構建了一個簡單版的OAuth服務器,這裏將進行更多的配置。java

使用Redis存儲Tokengit

在前一篇中Token是存儲在內存中,這樣的話一旦服務器重啓,全部Token都會丟失,這種狀況明顯是不準發生的,根據官方的 OAuth 2 Developers Guide ,Spring提供了多種存儲Token的方式,除了InMemoryTokenStore,JdbcTokenStore和JwtTokenStore,還有文檔中沒有提到的RedisTokenStore,基於性能的考慮,我採用了RedisTokenStore。github

配置使用RedisTokenStore很簡單,只需:redis

  1. 在application.properties中配置Redis相關鏈接信息
    spring.redis.host=localhost
    spring.redis.port=6379

     

  2. 修改OAuth配置類OauthConfig
    @Configuration
    @ImportResource("classpath:/client.xml")
    public class OauthConfig extends AuthorizationServerConfigurerAdapter {
    
    	@Autowired
    	private AuthenticationManager authenticationManager;
    	@Autowired
    	private RedisConnectionFactory redisConnectionFactory;
    
    	@Override
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    		endpoints.tokenServices(tokenServices(endpoints)).authenticationManager(authenticationManager);
    	}
    
    	private DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
    		DefaultTokenServices services = new DefaultTokenServices();
    		services.setTokenStore(tokenStore());
    		services.setSupportRefreshToken(true);
    		services.setReuseRefreshToken(false);
    		services.setClientDetailsService(endpoints.getClientDetailsService());
    		return services;
    	}
    
    	private TokenStore tokenStore() {
    		return new RedisTokenStore(redisConnectionFactory);
    	}
    
    }

     

自定義認證受權錯誤信息spring

在認證受權過程當中,有可能會出現因用戶名密碼錯誤等致使失敗的狀況,這時客戶端可能須要一些額外的信息以便與用戶進行更友好的交互。json

好比認證失敗後,客戶端須要顯示累計失敗的次數,這時就須要OAuth服務器在返回錯誤信息的同時返回累計失敗的次數。安全

  1. 定義錯誤類AuthenticationFailedException
    @SuppressWarnings("serial")
    public class AuthenticationFailedException extends UnauthorizedUserException {
    
    	public AuthenticationFailedException(int attempt) {
    		super("Authentication failed");
    		addAdditionalInformation("attempt", String.valueOf(attempt));
    	}
    
    }

     

  2. 修改CustomUserDetailsService,定義一個固定用戶,用戶名爲failed_user,只要過來認證的用戶名是這個,就拋出AuthenticationFailedException,設置累計失敗次數爲7
    @Component
    public class CustomUserDetailsService implements UserDetailsService {
    
    	@Override
    	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    		if ("failed_user".equals(username)) {
    			throw new AuthenticationFailedException(7);
    		}
    		return new User("user", "pwd", AuthorityUtils.createAuthorityList("ROLE_USER"));
    	}
    
    }

     

若調用http://localhost:8080/oauth/token時username參數爲failed_user,則會獲得以下認證受權失敗結果,其中attempt爲累計失敗次數服務器

{
    "error": "unauthorized_user",
    "error_description": "Authentication failed",
    "attempt": "7"
}

 

自定義受權模式app

若是官方提供的受權模式不能知足需求,就須要自定義一個新的受權模式。ide

好比如今要定義一個受權模式,這個模式只須要檢查用戶名的格式,格式正確就受權成功。

  1. 定義受權類CustomTokenGranter,受權類型名稱爲custom
    public class CustomTokenGranter extends AbstractTokenGranter {
    
    	private static final String GRANT_TYPE = "custom";
    
    	public CustomTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
    			OAuth2RequestFactory requestFactory) {
    		super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    	}
    
    	@Override
    	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
    
    		Map<String, String> param = tokenRequest.getRequestParameters();
    		String username = param.get("username");
    		if (!Pattern.matches("[0-9a-zA-Z]*", username)) {
    			throw new InvalidRequestException("Invalid username");
    		}
    
    		Authentication auth = new AnonymousAuthenticationToken("NA", username,
    				AuthorityUtils.createAuthorityList("ROLE_USER"));
    		OAuth2Authentication oauth2Auth = new OAuth2Authentication(tokenRequest.createOAuth2Request(client), auth);
    		return oauth2Auth;
    
    	}
    
    }

     

  2. 修改OauthConfig,配置CustomTokenGranter,一旦手動配置了受權模式,默認的受權模式就會被覆蓋,因此要用CompositeTokenGranter把官方定義的受權模式也一塊兒配置進去
    @Configuration
    @ImportResource("classpath:/client.xml")
    public class OauthConfig extends AuthorizationServerConfigurerAdapter {
    
    	@Autowired
    	private AuthenticationManager authenticationManager;
    	@Autowired
    	private RedisConnectionFactory redisConnectionFactory;
    
    	@Override
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    		endpoints.tokenServices(tokenServices(endpoints)).authenticationManager(authenticationManager);
    		endpoints.tokenGranter(tokenGranter(endpoints));
    	}
    
    	private DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
    		DefaultTokenServices services = new DefaultTokenServices();
    		services.setTokenStore(tokenStore());
    		services.setSupportRefreshToken(true);
    		services.setReuseRefreshToken(false);
    		services.setClientDetailsService(endpoints.getClientDetailsService());
    		return services;
    	}
    
    	private TokenStore tokenStore() {
    		return new RedisTokenStore(redisConnectionFactory);
    	}
    
    	private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
    		List<TokenGranter> granters = new ArrayList<TokenGranter>(Arrays.asList(endpoints.getTokenGranter()));
    		granters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(),
    				endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
    		granters.add(new RefreshTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
    				endpoints.getOAuth2RequestFactory()));
    		granters.add(new CustomTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
    				endpoints.getOAuth2RequestFactory()));
    		return new CompositeTokenGranter(granters);
    	}
    
    }

     

  3. 修改client.xml,爲client1配置受權類型custom
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
    	xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
    		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    	<oauth2:client-details-service id="clientDetailsService">
    		<oauth2:client client-id="client1" secret="secret1" 
    			authorized-grant-types="password,refresh_token,custom" access-token-validity="1800" 
    			refresh-token-validity="604800" scope="all" />
    	</oauth2:client-details-service>
    
    </beans>

     

調用http://localhost:8080/oauth/token時grant_type參數爲custom,username參數爲test#123,會返回認證受權失敗的結果

{
    "error": "invalid_request",
    "error_description": "Invalid username"
}

若username參數爲test123,則會返回認證受權成功的結果

{
    "access_token": "7210b63c-5b30-4240-b716-5059112a0564",
    "token_type": "bearer",
    "refresh_token": "7d0c7249-7342-4f1b-b219-047a7ad6b24e",
    "expires_in": 1799,
    "scope": "all"
}

端點安全

查看啓動日誌時,發現服務器開放了這四個關於OAuth的端點:/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access和/oauth/error,使用Sprint Boot實現的OAuth服務器默認只保護了/oauth/token,因爲該服務器有可能會被外部訪問,因此須要保護其餘三個端點不被隨意訪問。

  1. 修改OauthConfig,只有角色爲ROLE_CLIENT才能訪問/oauth/check_token
    @Configuration
    @ImportResource("classpath:/client.xml")
    public class OauthConfig extends AuthorizationServerConfigurerAdapter {
    
    	@Autowired
    	private AuthenticationManager authenticationManager;
    	@Autowired
    	private RedisConnectionFactory redisConnectionFactory;
    
    	@Override
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    		endpoints.tokenServices(tokenServices(endpoints)).authenticationManager(authenticationManager);
    		endpoints.tokenGranter(tokenGranter(endpoints));
    	}
    
    	@Override
    	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    		security.checkTokenAccess("hasAuthority('ROLE_CLIENT')");
    	}
    
    	private DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
    		DefaultTokenServices services = new DefaultTokenServices();
    		services.setTokenStore(tokenStore());
    		services.setSupportRefreshToken(true);
    		services.setReuseRefreshToken(false);
    		services.setClientDetailsService(endpoints.getClientDetailsService());
    		return services;
    	}
    
    	private TokenStore tokenStore() {
    		return new RedisTokenStore(redisConnectionFactory);
    	}
    
    	private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
    		List<TokenGranter> granters = new ArrayList<TokenGranter>(Arrays.asList(endpoints.getTokenGranter()));
    		granters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(),
    				endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
    		granters.add(new RefreshTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
    				endpoints.getOAuth2RequestFactory()));
    		granters.add(new CustomTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
    				endpoints.getOAuth2RequestFactory()));
    		return new CompositeTokenGranter(granters);
    	}
    
    }

     

  2. 修改client.xml,爲client1配置角色ROLE_CLIENT
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
    	xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
    		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    	<oauth2:client-details-service id="clientDetailsService">
    		<oauth2:client client-id="client1" secret="secret1" authorities="ROLE_CLIENT"
    			authorized-grant-types="password,refresh_token,custom" access-token-validity="1800" 
    			refresh-token-validity="604800" scope="all" />
    	</oauth2:client-details-service>
    
    </beans>

     

  3. 定義安全配置類SecurityConfig,禁止訪問/oauth下全部的子端點,Spring Boot對/oauth/token和/oauth/check_token的保護會覆蓋掉這個配置類的保護,因此不會影響本來的認證受權功能
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.antMatcher("/oauth/**").authorizeRequests().anyRequest().denyAll();
    	}
    
    }

     

配置完成後訪問http://localhost:8080/oauth/token和http://localhost:8080/oauth/check_token會被要求進行HTTP Basic認證,訪問http://localhost:8080/oauth/authorize和http://localhost:8080/oauth/confirm_access會出現下圖結果

後面在 使用Spring Boot構建獨立的OAuth服務器(三) 中會對Resource進行配置

相關文章
相關標籤/搜索