Spring Security OAuth2 Provider 最小實現(Spring Boot 2.1.3)

原文鏈接:https://www.iteye.com/blog/rensanning-2384996

在原文鏈接的指引下,遇到以下問題,已解決把問題展示出來,後面正文的代碼都是解決後的代碼:

1.第一步授權請求時,後臺報java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”,這是由於springboot版本升級導致不支持明文密碼,查找資料:在Config.java->SecurityConfig

裏添加代碼:

@Bean
public PasswordEncoder passwordEncoder() {
	return NoOpPasswordEncoder.getInstance();
}


2.第四步刷新token時,後臺報錯Handling error: IllegalStateException, UserDetailsService is required.查找資料:在Config.java->SecurityConfig裏添加代碼:

@Bean
@Override
protected UserDetailsService userDetailsService() {
	return super.userDetailsService();
}

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

Config.java->OAuthAuthorizationConfig添加代碼:

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private AuthenticationManager authenticationManager;
	
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
	endpoints.authenticationManager(authenticationManager)
			.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
			.userDetailsService(userDetailsService);
}


正文開始:Spring Boot 2.1.3、Spring Security OAuth2 2.0.12

OAuth2.0的開源 Server / Client 實現可以參考這裏:https://oauth.net/code/,這裏採用Spring Security OAuth2實現四種授權模式中最常用的:Authorization Code Grant。


具體可以看OAuth2.0標準的定義:https://tools.ietf.org/html/rfc6749#section-4.1

這裏首先只爲演示 OAuth2.0 的整個過程,做最小實現!

Spring Security OAuth2默認提供的四個URL:

  • /oauth/authorize : 授權AuthorizationEndpoint
  • /oauth/token : 令牌TokenEndpoint
  • /oauth/check_token : 令牌校驗CheckTokenEndpoint
  • /oauth/confirm_access : 授權頁面WhitelabelApprovalEndpoint
  • /oauth/error : 錯誤頁面WhitelabelErrorEndpoint


相關文章:
Spring Security OAuth2 Provider 之 最小實現
Spring Security OAuth2 Provider 之 數據庫存儲
Spring Security OAuth2 Provider 之 第三方登錄簡單演示
Spring Security OAuth2 Provider 之 自定義開發
Spring Security OAuth2 Provider 之 整合JWT

代碼如下:

pom.xml

Xml代碼 

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.springframework.security.oauth</groupId>  
    <artifactId>spring-security-oauth2</artifactId>  
</dependency> 

Application.java

Java代碼 

@SpringBootApplication  
public class Application {  
      
    public static void main(String[] args) {  
        SpringApplication.run(Application.class, args);  
    }  
      
}  


Config.java

Java代碼 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;


public class Config {
	public static final String OAUTH_CLIENT_ID = "oauth_client";
	public static final String OAUTH_CLIENT_SECRET = "oauth_client_secret";
	public static final String RESOURCE_ID = "my_resource_id";
	public static final String[] SCOPES = { "read", "write" };

	@Configuration
	@EnableAuthorizationServer
	static class OAuthAuthorizationConfig extends AuthorizationServerConfigurerAdapter {

		@Autowired
		private UserDetailsService userDetailsService;

		@Autowired
		private AuthenticationManager authenticationManager;
		@Override
		public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
			clients.inMemory()
					.withClient(OAUTH_CLIENT_ID)
					.secret(OAUTH_CLIENT_SECRET)
					.resourceIds(RESOURCE_ID)
					.scopes(SCOPES)
					.authorities("ROLE_USER")
					.authorizedGrantTypes("authorization_code", "refresh_token")
					.redirectUris("http://default-oauth-callback.com")
					.accessTokenValiditySeconds(60*30) // 30min
					.refreshTokenValiditySeconds(60*60*24); // 24h
		}

		@Override
		public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
			endpoints.authenticationManager(authenticationManager)
					 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
					 .userDetailsService(userDetailsService);
		}

	}

	@Configuration
	@EnableResourceServer
	static class OAuthResourceConfig extends ResourceServerConfigurerAdapter {
		@Override
		public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
			resources.resourceId(RESOURCE_ID);
		}
		@Override
		public void configure(HttpSecurity http) throws Exception {
			http.authorizeRequests()
					.antMatchers(HttpMethod.GET, "/api/**").access("#oauth2.hasScope('read')")
					.antMatchers(HttpMethod.POST, "/api/**").access("#oauth2.hasScope('write')");
		}
	}


	@Configuration
	@EnableWebSecurity
	static class SecurityConfig extends WebSecurityConfigurerAdapter {
		@Override
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth.inMemoryAuthentication()
					.withUser("user").password("123").roles("USER")
					.and()
					.withUser("admin").password("123").roles("ADMIN");
		}

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.csrf().disable();
			http.authorizeRequests()
					.antMatchers("/oauth/authorize").authenticated()
					.and()
					.httpBasic().realmName("OAuth Server");
		}

		@Bean
		@Override
		protected UserDetailsService userDetailsService() {
			return super.userDetailsService();
		}

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

		@Bean
		public PasswordEncoder passwordEncoder() {
			return NoOpPasswordEncoder.getInstance();
		}
	}
}

Controller.java

Java代碼 

@RestController  
public class Controller {  
  
    @GetMapping("/api/get")  
    public String get() {  
        return "Hello World!";  
    }  
  
    @PostMapping("/api/post")  
    public String post() {  
        return "POST process has finished.";  
    }  
      
    @GetMapping("/api/user")  
    public Object get(HttpServletRequest req) {  
        SecurityContextImpl sci = (SecurityContextImpl) req.getSession().getAttribute("SPRING_SECURITY_CONTEXT");  
        if (sci != null) {  
            Authentication authentication = sci.getAuthentication();  
            if (authentication != null) {  
                return authentication.getPrincipal();  
            }  
        }  
        return "none";  
    }  
      
} 

Test.java

Java代碼 

public class Test {  
  
    public static void main(String[] args) {  
        System.out.println(generate("oauth_client", "oauth_client_secret"));  
    }  
  
    private static String generate(String clientId, String clientSecret) {  
        String creds = String.format("%s:%s", new Object[] { clientId, clientSecret });  
        try {  
            return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));  
        } catch (UnsupportedEncodingException e) {  
            throw new IllegalStateException("Could not convert String");  
        }  
    }  
  
}  

【Run As】【Spring Boot App】啓動服務器後,看到以下Log:

引用

Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)

(1)授權請求(Get)

http://localhost:8080/oauth/authorize?client_id=oauth_client&scope=read&response_type=code&state=rensanning&redirect_uri=http://default-oauth-callback.com

由於對/oauth/authorize開啓了HTTP Basic認證,所以需要輸入密碼:


輸入正確用戶名密碼(user/123)後顯示授權頁:



選擇Approve點擊Authorize按鈕後,自動跳轉到 http://default-oauth-callback.com?code=sdb6vF&state=rensanning



URL中的code參數值即爲授權碼,該值是一個6位英數字的隨機數,具體可以看源碼:

org.springframework.security.oauth2.common.util.RandomValueStringGenerator.generate()

!!授權碼10分鐘過期目前還沒有實現!! https://github.com/spring-projects/spring-security-oauth/issues/725

(2)獲得令牌(Post)
 

http://localhost:8080/oauth/token?grant_type=authorization_code&redirect_uri=http://default-oauth-callback.com&code=sdb6vF


【/oauth/token】默認採用的是HTTP Basic Auth(org.springframework.security.web.authentication.www.BasicAuthenticationFilter),所以需要在HTTP的header裏提供clientId和clientSecret的Base64值。具體可以執行Test.java獲取。

Authorization: Basic b2F1dGhfY2xpZW50Om9hdXRoX2NsaWVudF9zZWNyZXQ=



通過以下設置,可以通過參數的形式傳遞clientId和clientSecret的值。比如:

http://localhost:8080/oauth/token?grant_type=authorization_code&redirect_uri=http://default-oauth-callback.com&code=sdb6vF&client_id=oauth_client&client_secret=oauth_client_secret

Java代碼 

@Configuration  
@EnableAuthorizationServer  
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {  
  @Override  
  public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {  
    oauthServer.allowFormAuthenticationForClients();  
  }  
}  

返回令牌信息

引用

{
    "access_token": "1cc5ffbd-faac-4d20-afd9-b8531acd248e",
    "token_type": "bearer",
    "refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
    "expires_in": 1631,
    "scope": "read"
}

生成的令牌是一個UUID,具體可以看源碼:org.springframework.security.oauth2.provider.token.DefaultTokenServices.createAccessToken()

(3)訪問API(Get)

直接訪問API返回401。URL:http://localhost:8080/api/get


通過access_token參數訪問。URL:http://localhost:8080/api/get?access_token=1cc5ffbd-faac-4d20-afd9-b8531acd248e


通過http header參數訪問。

http://localhost:8080/api/get
Authorization: Bearer 1cc5ffbd-faac-4d20-afd9-b8531acd248e

 


*** @EnableResourceServer 自動增加OAuth2AuthenticationProcessingFilter過濾器
*** !!SpringBoot1.5 @EnableResourceServer和@EnableWebSecurity配置的HttpSecurity有先後順序的問題,需要特殊設置!!參考:
https://github.com/spring-projects/spring-security-oauth/issues/993#issuecomment-284430752
https://stackoverflow.com/questions/29893602/spring-security-form-logging-and-outh2-in-same-app

(4)刷新令牌(Post)

 

http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=5b319fed-5600-4ea2-8c4f-61f6e3ea6e41
Authorization: Basic b2F1dGhfY2xpZW50Om9hdXRoX2NsaWVudF9zZWNyZXQ=


返回新的access_token:

引用

{
    "access_token": "3cbe70fc-753f-44ff-9bb4-0ba6bc3c9aab",
    "token_type": "bearer",
    "refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
    "expires_in": 1800,
    "scope": "read"
}

 


舊的Token就不能再用了:


通過新的Token訪問API:


同理,可以測試scope爲write的權限!

參考:
http://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://segmentfault.com/a/1190000010540911
http://ifeve.com/oauth2-tutorial-all/
http://websystique.com/spring-security/secure-spring-rest-api-using-oauth2/
http://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f
https://murashun.jp/blog/20150920-01.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章