一、OAuth(開放授權)是一個開放標準,允許用戶授權第三方移動應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方移動應用或分享他們數據的所有內容,OAuth2.0是OAuth協議的延續版本,但不向後兼容OAuth 1.0即完全廢止了OAuth1.0。
二、認證和授權過程。
1)主要包含3中角色:
(1)服務提供方:Authorization Server
(2)資源擁有者:Resource Server
(3)客戶端:Client
2)授權過程
(1)用戶打開授權頁面,詢問用戶授權
(2)用戶同意授權
(3)客戶端向授權服務器進行授權申請
(4)授權服務器進行認證,認證通過後,返回令牌(包含用戶信息)
(5)客戶端得到令牌後,通過攜帶令牌訪問資源服務器資源
(6)資源服務器向授權/認證服務確認後,方可向客戶端開發資源
三、授權服務器(Authorization Server),爲了更加接近生產環境,我這裏採用的都是數據庫或者自定義配置,請注意註釋部分。
1、目錄結構:
2、依賴部分
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
3、啓動項
package com.cetc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication @EnableEurekaClient public class AuthServerApplication { public static void main(String[] args) { SpringApplication.run(AuthServerApplication.class, args); } }
4、基礎配置application.yaml
server: port: 8694 servlet: context-path: /auth session: cookie: name: SESSION_AUTH_SERVER #因爲我這裏都是本地,所以需要修改會話名稱 spring: application: name: auth-server thymeleaf: encoding: UTF-8 mode: HTML5 cache: false datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1/auth-server?useUnicode=true&characterEncoding=utf-8 username: root password: root type: com.zaxxer.hikari.HikariDataSource jpa: show-sql: true hibernate: ddl-auto: update naming: physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy database: mysql database-platform: org.hibernate.dialect.MySQL5InnoDBDialect eureka: client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式
5、下面我們重點來看配置部分(爲了節約篇幅,我這裏就省略了AuthDetailsService、DataSourceConfiguration、PasswordEncoderConfiguration具體的可以參考:Spring-Boot之Security安全管理-10)
(1)security配置
package com.cetc.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity //開啓註解的使用 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { //認證過程 @Autowired private AuthDetailsService authDetailsService; //加密方式 @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(authDetailsService).passwordEncoder(passwordEncoder); } //基本沒有什麼太大的變化 @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .exceptionHandling() .and() .authorizeRequests() .antMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin() //注意我這裏使用的是自定義的登錄頁面 .loginPage("/login.html") .loginProcessingUrl("/login") .and() .logout() .deleteCookies("SESSION_AUTH_SERVER"); } //注意加入bean主要是在authentication server的配置需要使用 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
(2)Authorization配置
package com.cetc.config; import com.zaxxer.hikari.HikariDataSource; 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.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.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; @Configuration @EnableAuthorizationServer public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter{ @Autowired private AuthDetailsService authDetailsService; @Autowired private AuthenticationManager authenticationManager; @Autowired private ClientDetailsService clientDetailsService; @Autowired private TokenStore tokenStore; @Bean public ClientDetailsService clientDetailsService(HikariDataSource dataSource) { //使用數據庫的配置方式 return new JdbcClientDetailsService(dataSource); } @Bean public TokenStore tokenStore(HikariDataSource dataSource) { //token也使用數據的方式,後面會將JWT的使用方式 return new JdbcTokenStore(dataSource); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security //token獲取方式 .tokenKeyAccess("permitAll()") //檢測加入權限 .checkTokenAccess("isAuthenticated()") //允許表單認證 .allowFormAuthenticationForClients(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //這裏就是具體的授權管理過程了 clients.withClientDetails(clientDetailsService); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints //這裏使用的認證方式爲security配置方式 .authenticationManager(authenticationManager) //提供get和post的認證方式 .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET) //這裏一定要配置userDetailsService,不然刷新token會出錯,refresh_token .userDetailsService(authDetailsService) .tokenStore(tokenStore) //自定義認證頁面 .pathMapping("/oauth/confirm_access", "/oauth/confirm_access"); } }
其一:認證過程因爲使用的是數據庫,那麼需要數據庫的表格支持,這裏也提供了爲schema.sql,官網:https://github.com/spring-projects/spring-security-oauth/blob/2.0.x/spring-security-oauth2/src/test/resources/schema.sql
說明:由於官方的腳本適用於HSQL,所以我MYSQL這裏腳本進行了修改,主要是主鍵改成255的長度,LONGVARBINARY改爲BLOB
schema.sql:
-- used in tests that use HSQL create table oauth_client_details ( client_id VARCHAR(255) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) ); create table oauth_client_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(255) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256) ); create table oauth_access_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(255) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256), authentication BLOB, refresh_token VARCHAR(256) ); create table oauth_refresh_token ( token_id VARCHAR(256), token BLOB, authentication BLOB ); create table oauth_code ( code VARCHAR(256), authentication BLOB ); create table oauth_approvals ( userId VARCHAR(256), clientId VARCHAR(256), scope VARCHAR(256), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); -- customized oauth_client_details table create table ClientDetails ( appId VARCHAR(255) PRIMARY KEY, resourceIds VARCHAR(256), appSecret VARCHAR(256), scope VARCHAR(256), grantTypes VARCHAR(256), redirectUrl VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additionalInformation VARCHAR(4096), autoApproveScopes VARCHAR(256) );
其二:自定義認知頁面編寫AuthorizationController。
package com.cetc.web.authorization; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.SessionAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Map; @Controller //注意一定要加@SessionAttributes("authorizationRequest")代表這是認證請求 @SessionAttributes("authorizationRequest") public class AuthorizationController { @RequestMapping("/oauth/confirm_access") public String authorization(Map<String, ?> map, HttpServletRequest request) { AuthorizationRequest authorizationRequest = (AuthorizationRequest) map.get("authorizationRequest"); request.setAttribute("clientId", authorizationRequest.getClientId()); //這裏的scope是一定要的,主要是認證需要傳遞的數據類型,需要scope request.setAttribute("scope", authorizationRequest.getScope()); return "authorization"; } }
authorization.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>授權</title> </head> <body> <form th:action="${#servletContext.getContextPath()} + '/oauth/authorize'" method="post"> <p th:text="${clientId}"></p> <input type="hidden" name="user_oauth_approval" value="true"> <input type="hidden" name="authorize" value="Authorize"> <div th:each="item : ${scope}"> <input type="hidden" th:name="'scope.' + ${item}" value="true"> </div> <button type="submit">同意授權</button> </form> </body> </html>
說明:這裏的數據傳輸主要格式爲:
注意:這裏的socpe.ALL中的ALL爲配置的範圍。
(3)爲了更好的結合jpa,我這裏封裝了OauthClientDetails,主要目的就是,方便操作。
package com.cetc.domain; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "oauth_client_details") public class OauthClientDetails { @Id private String clientId; private String resourceIds; private String clientSecret; private String scope; private String authorizedGrantTypes; private String webServerRedirectUri; private String authorities; private Integer accessTokenValidity; private Integer refreshTokenValidity; private String additionalInformation; private String autoapprove; public String getClientId() { return clientId; } public void setClientId(String clientId) { this.clientId = clientId; } public String getResourceIds() { return resourceIds; } public void setResourceIds(String resourceIds) { this.resourceIds = resourceIds; } public String getClientSecret() { return clientSecret; } public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getAuthorizedGrantTypes() { return authorizedGrantTypes; } public void setAuthorizedGrantTypes(String authorizedGrantTypes) { this.authorizedGrantTypes = authorizedGrantTypes; } public String getWebServerRedirectUri() { return webServerRedirectUri; } public void setWebServerRedirectUri(String webServerRedirectUri) { this.webServerRedirectUri = webServerRedirectUri; } public String getAuthorities() { return authorities; } public void setAuthorities(String authorities) { this.authorities = authorities; } public Integer getAccessTokenValidity() { return accessTokenValidity; } public void setAccessTokenValidity(Integer accessTokenValidity) { this.accessTokenValidity = accessTokenValidity; } public Integer getRefreshTokenValidity() { return refreshTokenValidity; } public void setRefreshTokenValidity(Integer refreshTokenValidity) { this.refreshTokenValidity = refreshTokenValidity; } public String getAdditionalInformation() { return additionalInformation; } public void setAdditionalInformation(String additionalInformation) { this.additionalInformation = additionalInformation; } public String getAutoapprove() { return autoapprove; } public void setAutoapprove(String autoapprove) { this.autoapprove = autoapprove; } }
四、資源服務器Resource Server
1、目錄結構:從結構上面來看,基本和Spring-Cloud之Feign聲明式調用-4沒有什麼異同,所以我這裏只講核心點(ResourceServerConfiguration、application.yaml)。
2、先看需要的依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
3、ResourceServerConfiguration。資源服務器配置
package com.cetc.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{ @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .anyRequest().authenticated(); } }
說明:這裏的認證在認證中心完成,所以這裏只需要,配置攔截即可
4、application.yaml
server: port: 8695 spring: application: name: auth-resource eureka: client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式 security: oauth2: client: client-id: client client-secret: secret grant-type: authorization_code scope: ALL access-token-uri: http://127.0.0.1:8694/auth/oauth/token user-authorization-uri: http://127.0.0.1:8694/auth/oauth/authorize resource: id: auth-resource token-info-uri: http://127.0.0.1:8694/auth/oauth/check_token
說明:這裏就僅僅加入了oauth2的認證配置。這裏的參數主要是在授權服務器提供的授權,這裏主要在數據裏。我在授權的服務器的init中有加入初始化數據。
5、其他部分不再具體講解了,如果存在不懂的地方可以參考:Spring-Cloud之Feign聲明式調用-4
五、驗證第三方客戶端獲取資源服務器提供的接口。(這裏提供的接口爲:/api/authResource/getPort)
1)啓動項目:Eureka Server、Eureka Client、Auth Server、Auth Resource端口分別爲8670、8673、8694、8695。
2)訪問資源服務器的接口/api/authResource/getPort
說明:可以看出,直接訪問時沒有權限的。
3)前面測試訪問時沒有權限,主要是沒有令牌。那我們接下來就是來獲取令牌。前面也提到認證方式,這裏有四種
(1)授權碼模式(authorization_code)
(2)簡化模式(implicit)
(3)密碼模式(password)
(4)客戶端模式(client_credentials)
爲了方便我在數據庫,這幾種方式都進行了配置
4)獲取令牌
(1)授權碼模式(authorization_code)
a、獲取授權碼
oauth/authorize?response_type=code&client_id=&redirect_uri=
本文:
http://127.0.0.1:8694/auth/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://127.0.0.1:8695
說明:從上面的操作來看,我們最後獲得了一個授權碼code=code=hkJfzA,這個授權碼只能使用一次。
b、獲取令牌
oauth/token?client_id=&client_secret=&grant_type=authorization_code&redirect_uri=&code=
本文:
http://127.0.0.1:8694/auth/oauth/token?client_id=client&client_secret=secret&grant_type=authorization_code&redirect_uri=http://127.0.0.1:8695&code=hkJfzA
說明:這裏我們通過授權碼獲取到了令牌和其他參數
{ "access_token": "a2a8069e-2423-4ae9-9ea0-27efdb688d0f", "token_type": "bearer", "refresh_token": "89cfeab9-5bb1-45d1-8258-68919d2f4da1", "expires_in": 43199, "scope": "ALL" }
- access_token:表示訪問令牌,必選項。
- token_type:表示令牌類型,該值大小寫不敏感,必選項,可以是bearer類型或mac類型。
- expires_in:表示過期時間,單位爲秒。如果省略該參數,必須其他方式設置過期時間。
- refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項。
- scope:表示權限範圍,如果與客戶端申請的範圍一致,此項可省略。
(2)簡化模式(implicit)
/oauth/authorize?response_type=token&client_id=&redirect_uri=
本文:
http://127.0.0.1:8694/auth/oauth/authorize?response_type=token&client_id=client&redirect_uri=http://127.0.0.1:8695
可以看出最後得出的重定向加了一下數據
#access_token=a2a8069e-2423-4ae9-9ea0-27efdb688d0f&token_type=bearer&expires_in=42537&scope=ALL
是不是和授權碼模式的一樣,這裏不做詳情解釋。
(3)密碼模式(password)
/oauth/token?grant_type=password&client_id=&client_secret=&username=&password=
本文:
http://127.0.0.1:8694/auth/oauth/token?grant_type=password&client_id=client&client_secret=secret&username=admin&password=admin
返回數據和授權碼模式一樣
(4)客戶端模式(client_credentials)
/oauth/token?grant_type=client_credentials&client_id=&client_secret=
本文:
http://127.0.0.1:8694/auth/oauth/token?grant_type=client_credentials&client_id=client&client_secret=secret
5)攜帶令牌(access_token)訪問資源服務器,以授權碼模式得出的結果爲準。
6)刷新令牌,通過上面的解釋我們知道,令牌是會過期的,所以我們需要刷新令牌refresh_token爲refresh_token的值
/oauth/token?grant_type=refresh_token&client_id=&client_secret=&refresh_token=
本文:
http://127.0.0.1:8694/auth/oauth/token?grant_type=refresh_token&client_id=client&client_secret=secret&refresh_token=89cfeab9-5bb1-45d1-8258-68919d2f4da1
刷新後老的令牌就沒有用了。
六、客戶端,這裏的客戶端也是通過該授權服務器進行授權然後訪問的資源服務器。
1、目錄結構:基礎結構和Spring-Cloud之Feign聲明式調用-4類似
2、依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
3、application.yaml配置
server:
port: 8696
servlet:
session:
cookie:
name: SESSION_AUTH_SSO #本地避免session衝突
spring:
application:
name: auth-soo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式
security:
oauth2:
client:
client-id: client_sso
client-secret: secret_sso
grant-type: authorization_code
scope: ALL
access-token-uri: http://127.0.0.1:8694/auth/oauth/token
user-authorization-uri: http://127.0.0.1:8694/auth/oauth/authorize
resource:
token-info-uri: http://127.0.0.1:8694/auth/oauth/check_token
爲了方便我這裏重新加入了一套認證數據
4、security配置
package com.cetc.config; import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableOAuth2Sso public class SecurityConfiguration extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .anyRequest().authenticated(); } }
5、因爲我們是通過feign做的代理訪問,那麼直接訪問受保護的資源肯定不行,那麼就需要加入feign的攔截配置AuthSSOConfiguration
package com.cetc.config; import feign.RequestInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; @Configuration public class AuthSSOConfiguration { //獲取認證的配置數據 @Bean @ConfigurationProperties(prefix = "security.oauth2.client") public AuthorizationCodeResourceDetails authorizationCodeResourceDetails() { return new AuthorizationCodeResourceDetails(); } @Autowired private AuthorizationCodeResourceDetails authorizationCodeResourceDetails; //這裏使用他配置好的OAuth2ClientContext,這裏有坑自己體會 @Autowired @Qualifier("oauth2ClientContext") private OAuth2ClientContext oAuth2ClientContext; //配置請求攔截 @Bean public RequestInterceptor requestInterceptor() { return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, authorizationCodeResourceDetails); } //oauth2認證配置 @Bean public OAuth2RestTemplate oAuth2RestTemplate() { return new OAuth2RestTemplate(authorizationCodeResourceDetails); } }
6、提供接口/api/authSSO/getPort測試,啓動auth-sso端口8696
測試:
是不是感覺很簡單,那我們來看一下實際的請求過程
所以這個認證過程是通過上面那張圖來實現的,這是spring提供了很多配置而已。通過程序去實現了這個過程。
八、總結:上面的過程我就不再說了,這裏總結一下過程。簡單一點,就是在分佈式開發過程中,我們提供統一的認證/授權平臺,把需要保護的資源加入認證中,當然也可以通過SSO(@EnableOAuth2Sso)的方式直接登錄訪問。資源服務器的目的在於可以提供第三方的訪問需求比如現在的QQ授權、微信授權等。SSO的目的在於單點登錄,做到登錄一次,可以訪問所有資源的目的。
九、本文源碼地址:https://github.com/lilin409546297/spring-cloud/tree/master/oauth2
十、授權碼模式詳情編寫可以參考:https://www.cnblogs.com/ll409546297/p/10396837.html