Spring Boot 的 oAuth2 認證(附源碼)

 

OAuth2 統一認證

原理

OAuth在"客戶端"與"服務提供商"之間,設置了一個受權層(authorization layer)。"客戶端"不能直接登陸"服務提供商",只能登陸受權層,以此將用戶與客戶端區分開來。"客戶端"登陸受權層所用的令牌(token),與用戶的密碼不一樣。用戶能夠在登陸的時候,指定受權層令牌的權限範圍和有效期。php

"客戶端"登陸受權層之後,"服務提供商"根據令牌的權限範圍和有效期,向"客戶端"開放用戶儲存的資料。html

A、用戶打開客戶端之後,客戶端要求用戶給予受權。java

B、用戶贊成給予客戶端受權。mysql

C、客戶端使用上一步得到的受權,向認證服務器申請令牌。git

D、認證服務器對客戶端進行認證之後,確認無誤,贊成發放令牌。github

E、客戶端使用令牌,向資源服務器申請獲取資源。web

F、資源服務器確認令牌無誤,贊成向客戶端開放資源。ajax

 

客戶端受權模式

上面 B、流程中是用戶給予客戶端受權。oauth2 定義了下面四種受權方式:spring

  • 受權碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)

受權碼模式

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;
    }
}

 

相關問題

問題1、提供oAuth2 接口 及 token 的獲取

以前的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!'); 
  }});

問題2、github 第三方受權接入(原文:https://developer.github.com/v3/oauth/

從 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

問題3、 github 的token的獲取

使用 RestTemplate訪問。

OAuth2RestTemplate template = oAuth2RestTemplate(new AuthorizationCodeResourceDetails());
template.setRetryBadAccessTokens(false);
token = template.getAccessToken();

或者在配置文件中從上下文中直接獲取。

OAuth2AccessToken accessToken = oauth2ClientContext.getAccessToken();

 

完整項目下載

完整項目下載—— 點我

 

其餘

Jwt(json web tokens)

Spring security 框架實現原理

 

參閱文檔

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/
相關文章
相關標籤/搜索