springboot之oauth2


  一、OAuth2.0是OAuth協議的延續版本,但不向后兼容OAuth 1.0即完全廢止了OAuth1.0。 OAuth 2.0關注客戶端開發者的簡易性。要么通過組織在資源擁有者和HTTP服務商之間的被批准的交互動作代表用戶,要么允許第三方應用代表用戶獲得訪問的權限。同時為Web應用,桌面應用和手機,和起居室設備提供專門的認證流程。

  二、使用場景:

  1、自己開發應用時,需要獲取其他應用的資源。比如:使用QQ登錄,然后獲取QQ頭像等信息

  2、SSO認證服務器,在自己開發應用時使用統一的認證過程,不需要單獨重寫重寫認證體系

  三、概念 

  (1) Third-party application:第三方應用程序,本文中又稱"客戶端"(client)。

  (2)HTTP service:HTTP服務提供商,本文中簡稱"服務提供商"。

  (3)Resource Owner:資源所有者,本文中又稱"用戶"(user)。

  (4)User Agent:用戶代理,本文中就是指瀏覽器。

  (5)Authorization server:認證服務器,即服務提供商專門用來處理認證的服務器。

  (6)Resource server:資源服務器,即服務提供商存放用戶生成的資源的服務器。它與認證服務器,可以是同一台服務器,也可以是不同的服務器。

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

  "客戶端"登錄授權層以后,"服務提供商"根據令牌的權限范圍和有效期,向"客戶端"開放用戶儲存的資料。

  四、模式運行流程

  

  (A)用戶打開客戶端以后,客戶端要求用戶給予授權。

  (B)用戶同意給予客戶端授權。

  (C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。

  (D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。

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

  (F)資源服務器確認令牌無誤,同意向客戶端開放資源。

  五、授權模式

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

  1)授權碼模式

  

  (A)用戶訪問客戶端,后者將前者導向認證服務器。

  (B)用戶選擇是否給予客戶端授權。

  (C)假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。

  (D)客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的后台的服務器上完成的,對用戶不可見。

  (E)認證服務器核對了授權碼和重定向URI,確認無誤后,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。

  2)簡化模式

   

  (A)客戶端將用戶導向認證服務器。

  (B)用戶決定是否給於客戶端授權。

  (C)假設用戶給予授權,認證服務器將用戶導向客戶端指定的"重定向URI",並在URI的Hash部分包含了訪問令牌。

  (D)瀏覽器向資源服務器發出請求,其中不包括上一步收到的Hash值。

  (E)資源服務器返回一個網頁,其中包含的代碼可以獲取Hash值中的令牌。

  (F)瀏覽器執行上一步獲得的腳本,提取出令牌。

  (G)瀏覽器將令牌發給客戶端。

  3)密碼模式

  

  (A)用戶向客戶端提供用戶名和密碼。

  (B)客戶端將用戶名和密碼發給認證服務器,向后者請求令牌。

  (C)認證服務器確認無誤后,向客戶端提供訪問令牌。

  4)客戶端模式

  

  (A)客戶端向認證服務器進行身份認證,並要求一個訪問令牌。

  (B)認證服務器確認無誤后,向客戶端提供訪問令牌。

  六、授權碼模式例子

  這里說明一下這里主要只通過授權碼模式來講解oauth2的使用過程。

  授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的后台服務器,與"服務提供商"的認證服務器進行互動。

  簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。

  密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要授權。

  客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。

  相對來說授權碼的方式使用上面,是非常嚴謹的。不存在,其他模式的相對弊病。

  7、代碼部分

  1)需要的依賴

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>

    <dependencies>
        <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.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
    </dependencies>

  2)認證服務器

  

  主要配置:SecurityConfiguration、AuthServerConfiguration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
        .and()
            .authorizeRequests()
            .anyRequest().authenticated()
        .and()
            .formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        //內存用戶不多解釋
        builder.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder.encode("admin"))
                .roles("ADMIN");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
   @Bean
   @Override
   protected UserDetailsService userDetailsService() {
  return super.userDetailsService();
   }
}
@Configuration
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //這里client使用存在模式,可以實際過程調整為jdbc的方式
        //這里說明一下,redirectUris的連接可以是多個,這里通過access_token都可以訪問的
        //簡單點,就是授權的過程
        clients.inMemory()
                .withClient("client")
                .secret(passwordEncoder.encode("secret"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("All")
                .autoApprove(true)
                .redirectUris("http://localhost:9001/login", "http://localhost:9002/login", "http://localhost:9003/authorize/login");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //權限控制
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //認證體系使用security的方式
        endpoints.authenticationManager(authenticationManager); 
      // 允許調用方式
     endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
     endpoints.userDetailsService(userDetailsService);
    }

  說明:這里我為了更好的區分,把認證服務器和資源服務器分開的,實際上可以使用認證服務器作為資源服務器

  yaml配置

server:
  port: 9000
  servlet:
    context-path: /auth #這里一定要加上contextPath,這個坑自己體會吧

  3)資源服務器

  

  主要配置:ResourceServerConfiguration、application.yaml

/**
 * 資源服務器的配置也很簡單
 * 主要是EnableResourceServer,以及資源的控制
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
        .and()
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}
server:
  port: 9002
security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://localhost:9000/auth/oauth/token
      user-authorization-uri: http://localhost:9000/auth/oauth/authorize
    resource:
      token-info-uri: http://localhost:9000/auth/oauth/check_token

  說明:資源服務器主要用於資源攔截,需要獲取授權碼才能訪問

  4)sso客戶端

  

  主要配置:SecurityConfiguration、application.yaml

/**
 * 這里使用的是sso的方式,可以用於單點登錄
 * 構造方式也很簡單,主要是sso的配置
 */
@Configuration
@EnableOAuth2Sso
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}
server:
  port: 9001
security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://localhost:9000/auth/oauth/token
      user-authorization-uri: http://localhost:9000/auth/oauth/authorize
    resource:
      token-info-uri: http://localhost:9000/auth/oauth/check_token
      #user-info-uri: http://localhost:9002/user/me
      #這里兩種獲取用戶的方式,都可以。但是只能存在一種

  5)客戶端:當然瀏覽器可以為一種客戶端,自己開發的應用也可以為客戶端

  瀏覽器:

  a、獲取授權碼

oauth/authorize?response_type=code&client_id=&redirect_uri=

  本文中:

http://localhost:9000/auth/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://localhost:9002/login

  這里我們就獲取到了code值

  b、通過code獲取令牌

oauth/token?client_id=&client_secret=&grant_type=authorization_code&redirect_uri=&code=

  本文中:

http://localhost:9000/auth/oauth/token?client_id=client&client_secret=secret&grant_type=authorization_code&redirect_uri=http://localhost:9002/login&code=jrbBZS

 

   獲取的對應值

{
    "access_token": "06c1db9b-aac3-4a9a-acaf-56f5a5d0ea21",
    "token_type": "bearer",
    "refresh_token": "046d3fe7-52c4-43e5-902a-673ab2b0d3d4",
    "expires_in": 42981,
    "scope": "All"
}
  • access_token:表示訪問令牌,必選項。
  • token_type:表示令牌類型,該值大小寫不敏感,必選項,可以是bearer類型或mac類型。
  • expires_in:表示過期時間,單位為秒。如果省略該參數,必須其他方式設置過期時間。
  • refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項。
  • scope:表示權限范圍,如果與客戶端申請的范圍一致,此項可省略。

   c、更新令牌

oauth/token?grant_type=refresh_token&refresh_token=

  本文:

http://localhost:9000/auth/oauth/token?grant_type=refresh_token&refresh_token=046d3fe7-52c4-43e5-902a-673ab2b0d3d4

  注意:在使用refresh_token刷新令牌的時候,需要在認證服務器上面設置

  

  SecurityConfiguration加入UserDetailsService 

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

  AuthServerConfiguration也加入UserDetailsService 

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //認證體系使用security的方式
        endpoints.authenticationManager(authenticationManager);
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        endpoints.userDetailsService(userDetailsService);
    }

  否者報錯:

Handling error: IllegalStateException, UserDetailsService is required.

  

   d、訪問資源

url?access_token=06c1db9b-aac3-4a9a-acaf-56f5a5d0ea21

  應用:

  a、默認方式獲取code

http://localhost:9000/auth/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://localhost:9003/authorize/login

  

  第一步基本上都是通過瀏覽器進行登錄的。

  b、程序獲取令牌

       LinkedMultiValueMap<String, Object> valueMap = new LinkedMultiValueMap<>();
            valueMap.add("client_id", authorizationCodeResourceDetails.getClientId());
            valueMap.add("client_secret", authorizationCodeResourceDetails.getClientSecret());
            valueMap.add("grant_type", authorizationCodeResourceDetails.getGrantType());
            valueMap.add("redirect_uri", authorizationCodeResourceDetails.getPreEstablishedRedirectUri());
            valueMap.add("code", code);
            Map<String, String> map = HttpUtils.doFrom(authorizationCodeResourceDetails.getAccessTokenUri(), valueMap, Map.class);

  c、單點登錄

        //獲取用戶信息,說明這里主要目的就是通過資源服務器去獲取用戶信息
            Map principal = HttpUtils.doGet(resourceServerProperties.getUserInfoUri() + "?access_token=" + map.get("access_token"), Map.class);

            //這里通過本地登錄單點登錄
            String username = principal.get("name").toString();
            //如果用戶存在則不添加,這里如果生產應用中,可以更具規則修改
            if (userRepository.findByUsername(username) == null) {
                Role role = roleRepository.findByRoleType(Role.RoleType.USER);
                User newUser = new User();
                newUser.setUsername(username);
                newUser.setPassword(passwordEncoder.encode(username));
                newUser.getRoles().add(role);
                userRepository.save(newUser);
            }

            //這里通過本地登錄的方式來獲取會話
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            LinkedMultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
            params.add("username", username);
            params.add("password", username);
            HttpEntity<LinkedMultiValueMap<String, ? extends Object>> httpEntity = new HttpEntity(params, httpHeaders);
            String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/login";
            ResponseEntity<Object> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Object.class);
            //將登錄后的header原本的給瀏覽器,這就是當前瀏覽器的會話
            HttpHeaders headers = exchange.getHeaders();
            for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
                entry.getValue().stream().forEach(value -> response.addHeader(entry.getKey(), value));
            }
            //這個狀態是根據security的返回數據設定的
            response.setStatus(exchange.getStatusCode().value());

  d、登錄的實現過程

@RestController
@RequestMapping("/authorize")
public class AuthorizedResource {

    @Autowired
    private AuthorizationCodeResourceDetails authorizationCodeResourceDetails;

    @Autowired
    private ResourceServerProperties resourceServerProperties;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @RequestMapping("/login")
    public void login(String code, HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!StringUtils.isEmpty(code)) {
            LinkedMultiValueMap<String, Object> valueMap = new LinkedMultiValueMap<>();
            valueMap.add("client_id", authorizationCodeResourceDetails.getClientId());
            valueMap.add("client_secret", authorizationCodeResourceDetails.getClientSecret());
            valueMap.add("grant_type", authorizationCodeResourceDetails.getGrantType());
            valueMap.add("redirect_uri", authorizationCodeResourceDetails.getPreEstablishedRedirectUri());
            valueMap.add("code", code);
            Map<String, String> map = HttpUtils.doFrom(authorizationCodeResourceDetails.getAccessTokenUri(), valueMap, Map.class);
            System.out.println(map);

            //獲取用戶信息,說明這里主要目的就是通過資源服務器去獲取用戶信息
            Map principal = HttpUtils.doGet(resourceServerProperties.getUserInfoUri() + "?access_token=" + map.get("access_token"), Map.class);

            //這里通過本地登錄單點登錄
            String username = principal.get("name").toString();
            //如果用戶存在則不添加,這里如果生產應用中,可以更具規則修改
            if (userRepository.findByUsername(username) == null) {
                Role role = roleRepository.findByRoleType(Role.RoleType.USER);
                User newUser = new User();
                newUser.setUsername(username);
                newUser.setPassword(passwordEncoder.encode(username));
                newUser.getRoles().add(role);
                userRepository.save(newUser);
            }

            //這里通過本地登錄的方式來獲取會話
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            LinkedMultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
            params.add("username", username);
            params.add("password", username);
            HttpEntity<LinkedMultiValueMap<String, ? extends Object>> httpEntity = new HttpEntity(params, httpHeaders);
            String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/login";
            ResponseEntity<Object> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Object.class);
            //將登錄后的header原本的給瀏覽器,這就是當前瀏覽器的會話
            HttpHeaders headers = exchange.getHeaders();
            for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
                entry.getValue().stream().forEach(value -> response.addHeader(entry.getKey(), value));
            }
            //這個狀態是根據security的返回數據設定的
            response.setStatus(exchange.getStatusCode().value());
        }
    }
}

  說明:這里這是簡單的應用,實際用戶名等可以和密碼都可以綁定現有賬號,或者深度加密!

  e、其他沒有什么大的配置

  application.yaml

server:
  port: 9003
  servlet:
    session:
      cookie:
        name: ACCESS_SESSION
security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      grant-type: authorization_code
      access-token-uri: http://localhost:9000/auth/oauth/token
      user-authorization-uri: http://localhost:9000/auth/oauth/authorize
      pre-established-redirect-uri: http://localhost:9003/authorize/login
    resource:
      user-info-uri: http://localhost:9002/user/me
    sso:
      login-path: /authorize/login
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/model?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
    username: root
    password: 
  jpa:
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
    show-sql: true
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    database: mysql

  6)sso客戶端

   說明:個人還是很喜歡sso的模式的,簡單方便高效

  八、源碼:https://github.com/lilin409546297/security-oauth2-sso

  九、此博客借鑒阮一峰的理解OAuth 2.0


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM