OAuth在"客戶端"與"服務提供商"之間,設置了一個受權層(authorization layer)。"客戶端"不能直接登陸"服務提供商",只能登陸受權層,以此將用戶與客戶端區分開來。"客戶端"登陸受權層所用的令牌(token),與用戶的密碼不一樣。用戶能夠在登陸的時候,指定受權層令牌的權限範圍和有效期。php
"客戶端"登陸受權層之後,"服務提供商"根據令牌的權限範圍和有效期,向"客戶端"開放用戶儲存的資料。html
A、用戶打開客戶端之後,客戶端要求用戶給予受權。java
B、用戶贊成給予客戶端受權。mysql
C、客戶端使用上一步得到的受權,向認證服務器申請令牌。git
D、認證服務器對客戶端進行認證之後,確認無誤,贊成發放令牌。github
E、客戶端使用令牌,向資源服務器申請獲取資源。web
F、資源服務器確認令牌無誤,贊成向客戶端開放資源。ajax
上面 B、流程中是用戶給予客戶端受權。oauth2 定義了下面四種受權方式:spring
A、用戶訪問客戶端,後者將前者導向認證服務器。sql
B、用戶選擇是否給予客戶端受權。
C、假設用戶給予受權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個受權碼。
D、客戶端收到受權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的後臺的服務器上完成的,對用戶不可見。
E、認證服務器覈對了受權碼和重定向URI,確認無誤後,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
步驟說明:
① A步驟中,客戶端申請認證的URI,包含如下參數:
* response_type:表示受權類型,必選項,此處的值固定爲"code"
* client_id:表示客戶端的ID,必選項
* redirect_uri:表示重定向URI,可選項
* scope:表示申請的權限範圍,可選項
* state:表示客戶端的當前狀態,能夠指定任意值,認證服務器會原封不動地返回這個值。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1Host: server.example.com
② C步驟中,服務器迴應客戶端的URI,包含如下參數:
* code:表示受權碼,必選項。該碼的有效期應該很短,一般設爲10分鐘,客戶端只能使用該碼一次,不然會被受權服務器拒絕。該碼與客戶端ID和重定向URI,是一一對應關係。
* state:若是客戶端的請求中包含這個參數,認證服務器的迴應也必須如出一轍包含這個參數。
HTTP/1.1 302 FoundLocation: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA &state=xyz
③ D步驟中,客戶端向認證服務器申請令牌的HTTP請求,包含如下參數:
* grant_type:表示使用的受權模式,必選項,此處的值固定爲"authorization_code"。
* code:表示上一步得到的受權碼,必選項。
* redirect_uri:表示重定向URI,必選項,且必須與A步驟中的該參數值保持一致。
* client_id:表示客戶端ID,必選項。
POST /token HTTP/1.1Host: server.example.comAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
④ E步驟中,認證服務器發送的HTTP回覆,包含如下參數:
* access_token:表示訪問令牌,必選項。
* token_type:表示令牌類型,該值大小寫不敏感,必選項,能夠是bearer類型或mac類型。
* expires_in:表示過時時間,單位爲秒。若是省略該參數,必須其餘方式設置過時時間。
* refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項。
* scope:表示權限範圍,若是與客戶端申請的範圍一致,此項可省略。
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
密碼模式是將受權碼模式中的受權碼固定爲用戶名和密碼。
A、用戶向客戶端提供用戶名和密碼。
B、客戶端將用戶名和密碼發給認證服務器,向後者請求令牌。
C、認證服務器確認無誤後,向客戶端提供訪問令牌。
步驟說明:
① B步驟中,客戶端發出的HTTP請求,包含如下參數:
* grant_type:表示受權類型,此處的值固定爲"password",必選項。
* username:表示用戶名,必選項。
* password:表示用戶的密碼,必選項。
* scope:表示權限範圍,可選項。
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=password&username=johndoe&password=A3ddj3w
② C步驟中,認證服務器向客戶端發送訪問令牌,下面是一個例子。
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
注意,整個過程當中,客戶端不得保存用戶的密碼。
1 .pom.xml
<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>
2 .SecurityConfig.java(主要配置文件)
package club.lemos.sso.config; import club.lemos.sso.config.security.ClientResources; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter; import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.web.filter.CompositeFilter; import javax.annotation.Resource; import javax.servlet.Filter; import java.util.ArrayList; import java.util.List; @Configuration @EnableOAuth2Client @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private OAuth2ClientContext oauth2ClientContext; private final UserDetailsService userDetailService; @Autowired public SecurityConfig(UserDetailsService userDetailService) { this.userDetailService = userDetailService; } /** * 詳細的路由配置參數 * * @param http 配置 * @throws Exception 相關異常 */ @Override protected void configure(HttpSecurity http) throws Exception { http .cors() // 跨域支持 .and() .antMatcher("/**") // 捕捉全部路由 .authorizeRequests() .antMatchers("/", "/login**", "/webjars/**", "/github").permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) // 認證入口(跳轉) .and() .formLogin().loginProcessingUrl("/doLogin") // 表單請求的路由爲 "POST /login" .defaultSuccessUrl("/").failureUrl("/login?err=1") .permitAll() .and() .logout().logoutUrl("/logout") // 註銷請求的路由爲 "GET /logout" .logoutSuccessUrl("/") .permitAll() .invalidateHttpSession(true) .clearAuthentication(true) .and() .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // csrf 安全處理 .and() .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); // 第三方受權層 } @Bean public FilterRegistrationBean oauth2ClientFilterRegistration( OAuth2ClientContextFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(filter); registration.setOrder(-100); return registration; } private Filter ssoFilter() { CompositeFilter filter = new CompositeFilter(); List<Filter> filters = new ArrayList<>(); filters.add(ssoFilter(github(), "/login/github")); filter.setFilters(filters); return filter; } private Filter ssoFilter(ClientResources client, String path) { OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path); OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext); filter.setRestTemplate(template); UserInfoTokenServices tokenServices = new UserInfoTokenServices( client.getResource().getUserInfoUri(), client.getClient().getClientId()); tokenServices.setRestTemplate(template); filter.setTokenServices(tokenServices); return filter; } /** * github 受權鏈接 * * @return 第三方受權鏈接對象 */ @Bean @ConfigurationProperties("github") public ClientResources github() { return new ClientResources(); } /** * BCrypt 密碼加密 * * @return BCrypt 編碼器 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public DaoAuthenticationProvider authProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailService); // authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } /** * 用戶認證服務(用戶名+密碼) * * @param auth 認證 * @throws Exception 相關異常 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authProvider()); } // TODO 提供一個訪問用戶信息(暱稱,角色信息等等)的 api // TODO 提供 cookie持久化時間 // TODO 註銷功能實現 }
ClientResources.java
package club.lemos.sso.config.security; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; public class ClientResources { @NestedConfigurationProperty private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails(); @NestedConfigurationProperty private ResourceServerProperties resource = new ResourceServerProperties(); public AuthorizationCodeResourceDetails getClient() { return client; } public ResourceServerProperties getResource() { return resource; } }
3 .application.yml(配置文件)
logging: level: org: springframework: security: DEBUG root: INFO server: port: 8080 spring: datasource: dbcp2: initial-size: 10 max-idle: 8 min-idle: 8 driverClassName: com.mysql.jdbc.Driver password: root url: jdbc:mysql://localhost:3306/sso?useSSL=false username: root freemarker: charset: UTF-8 check-template-location: true content-type: text/html expose-request-attributes: true expose-session-attributes: true request-context-attribute: request thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html github: client: clientId: dd2bf79a9e6be256f0e8 clientSecret: 0e555a2ee5d627e3abdee3f5096de6d8278d3413 accessTokenUri: https://github.com/login/oauth/access_token userAuthorizationUri: https://github.com/login/oauth/authorize clientAuthenticationScheme: form resource: userInfoUri: https://api.github.com/user
4 .UserDetailsServiceImpl.java(用戶接口實現)
package club.lemos.sso.config.security; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.Date; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO 從數據庫中查找用戶 return new UserDetailsImpl(1L, "lisi", "password", Arrays.asList(new SimpleGrantedAuthority("USER"), new SimpleGrantedAuthority("ADMIN")), true, new Date()); } }
UserDetailsImpl.java
package club.lemos.sso.config.security; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Date; public class UserDetailsImpl implements UserDetails { private final Long id; private final String username; private final String password; private final Collection<? extends GrantedAuthority> authorities; private final boolean enabled; private final Date lastPasswordResetDate; public UserDetailsImpl(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities, boolean enabled, Date lastPasswordResetDate) { this.id = id; this.username = username; this.password = password; this.authorities = authorities; this.enabled = enabled; this.lastPasswordResetDate = lastPasswordResetDate; } @JsonIgnore public Long getId() { return id; } @Override public String getUsername() { return username; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isEnabled() { return enabled; } @JsonIgnore public Date getLastPasswordResetDate() { return lastPasswordResetDate; } }
以前的Web端(B\S結構),能夠正常通訊。登陸跳轉什麼的。對於受限資源,須要經過 oauth2受權( C\S 結構)。能夠經過Web端發送一個請求進行認證。認證成功,得到 token,能夠使用 token訪問f服務器受限資源。formLogin 即表單登陸,比較好理解。oauth2 登陸,須要先得到 token,再用它訪問 api 資源服務器,獲取信息。
1. 證書 token
curl client:secret@localhost:8090/oauth/token -d grant_type=client_credentials
2. 密碼 token
當應用啓動時,springboot 會建立一個默認的用戶,用戶id爲‘user‘。密碼是隨機的,但能夠從打印的日誌中看到。
curl client:secret@localhost:8090/oauth/token -d grant_type=password -d username=user -d password=...
或者使用定義好的用戶名及密碼進行認證
curl client:secret@localhost:8090/oauth/token -d grant_type=password -d username=admin -d password=admin
認證後,訪問資源服務
curl http://localhost:8090/api/users -H "Authorization: bearer 7e7b7ced-3747-43a2-8134-c7e6b87c6451"
3. 頁面中,能夠經過發送帶 auth 認證頭的請求,訪問oauth服務器
ajax 請求認證:
$.ajax({ type: "GET", url: "index1.php", dataType: 'json', async: false, headers: { "Authorization": "Basic " + btoa(USERNAME + ":" + PASSWORD) }, data: '{ "comment" }', success: function (){ alert('Thanks for your comment!'); }});
從 github上申請 開發權限:
執行流程
1> 點擊頁面的 callback超連接(好比 <a href="http://localhost:8090/login/github">go to github</a>)。
callbackURL = http://localhost:8090/login/github GET
2> 重定向到受權頁面,用戶點擊受權
user-authorization-uri = https://github.com/login/oauth/authorize?client_id=141c0a61de83cf2d9841&redirect_uri=http://localhost:8090/login/github&response_type=code&scope=user&state=4jgVT2
3> 重定向回本身的頁面,並攜帶一個 code (受權碼) 和 前一步中的 state參數,若是 states 匹配,則能夠發送一個 POST https://github.com/login/oauth/access_token
http://localhost:8090/login/github?code=2c6dcdce82ef1473e148&state=4jgVT2
4> 請求 token(受權成功,會自動發送這個請求)
https://github.com/login/oauth/access_token POST
響應 token(包含着受權信息,存儲在 JSESSION 中)
access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer
或者
Accept: application/json {"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"}
5> 訪問 Github API(使用 js訪問)
GET https://api.github.com/user?access_token=...
或者設置頭信息
Authorization: token OAUTH-TOKEN
使用 RestTemplate訪問。
OAuth2RestTemplate template = oAuth2RestTemplate(new AuthorizationCodeResourceDetails()); template.setRetryBadAccessTokens(false); token = template.getAccessToken();
或者在配置文件中從上下文中直接獲取。
OAuth2AccessToken accessToken = oauth2ClientContext.getAccessToken();
完整項目下載—— 點我
springboot 官方文檔 Spring-Boot-Reference-Guide https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/content/ Spring Boot and OAuth2 *****接入github 的詳細配置****** https://spring.io/guides/tutorials/spring-boot-oauth2/ --------------------------------------------------------------------------- 有用的文章 spring security & oauth2 http://www.jianshu.com/p/6b211e845b16/ spring-security-oauth2 server http://www.jianshu.com/p/028043425b09 詳解Spring Security進階身份認證之UserDetailsService(附源碼) http://favccxx.blog.51cto.com/2890523/1609692 --------------------------------------------------------------------------- github 相關文檔 https://developer.github.com/v3/ https://developer.github.com/v3/oauth/ https://developer.github.com/v3/oauth_authorizations/#list-your-authorizations https://help.github.com/articles/connecting-with-third-party-applications/