SpringBoot從入門到精通教程(二十二)- Oauth2+Token詳細用法/SpringSecurity

需求背景

本篇文章講解如何通過Springboot2集成驗證服務Token,以及資源服務的用法(更多官方關於 Oauth2

概要

主要使用Spring Boot2和SpringSecurity5

OAuth2術語

  • 資源擁有者 Resource Owner
    • 用戶授權哪些應用程序,能夠去訪問資源信息等,訪問受限於作用域
  • 資源服務 Resource Server
    • 一個在客戶端擁有令牌後,處理驗證它請求的服務
  • 客戶端 Client
    • 一個代表資源擁有者的應用程序,可以訪問受保護的資源
  • 授權服務 Authorization Server
    • 一個在成功驗證了客戶端和資源擁有者,以及驗證請求後,分發令牌的授權服務
  • 令牌 Access Token
    • 一個用於訪問受限資源的唯一token
  • 作用域範圍 Scope
    • 一個許可權限
  • 授權類型 Grant type

Oauth2 密碼授權流程

在oauth2協議中,一個應用會有自己的clientId和clientSecret(從認證方申請),由認證方下發clientId和secret

代碼演示

授權服務 Authorization Server

構建Authorization Server,這裏我使用了SpringSecurity5 和 SpringBoot 2.0.6版本

1. 項目目錄結構

2. pom.xml依賴組件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib-ext-spring</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>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

3. 這裏爲演示,使用了H2數據庫

以下是Spring Security需要用到的OAuth2 SQL

CREATE TABLE IF NOT EXISTS oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256) NOT NULL,
  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(4000),
  autoapprove VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256),
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

CREATE TABLE IF NOT EXISTS oauth_code (
  code VARCHAR(256), authentication BLOB
);

使用下面這句:

INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
  VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);

特別要注意:這裏是下發clientId和secret,值分別爲clientId和secret(這兩個值後面要用到)

這裏的client_secret列值,由bcrypt生成

{bcrypt}前綴寫法,這是Spring Security 5 DelegatingPasswordEncoder的新用法

access_token_validity列:表示token有效時間,單位秒,即300秒有效

同時,以下是User和Authority表(被org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl使用到)

CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(256) NOT NULL,
  password VARCHAR(256) NOT NULL,
  enabled TINYINT(1),
  UNIQUE KEY unique_username(username)
);

CREATE TABLE IF NOT EXISTS authorities (
  username VARCHAR(256) NOT NULL,
  authority VARCHAR(256) NOT NULL,
  PRIMARY KEY(username, authority)
);

同樣,使用下面的sql,初始化user和authority

INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');

注意:用戶名是user,password的值是:pass,後面會用到

Spring Security Configuration 部分

4. WebSecurityConfiguration.java文件

package com.md.demo.oauth.opaque.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.sql.DataSource;

@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

	private final DataSource dataSource;

	private PasswordEncoder passwordEncoder;
	private UserDetailsService userDetailsService;

	public WebSecurityConfiguration(final DataSource dataSource) {
		this.dataSource = dataSource;
	}

	// 排除路徑驗證
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/hello");
	}

	@Override
	protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
	}

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

	@Bean
	public PasswordEncoder passwordEncoder() {
		if (passwordEncoder == null) {
			passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
		}
		return passwordEncoder;
	}

	@Bean
	@Override
	public UserDetailsService userDetailsService() {
		if (userDetailsService == null) {
			userDetailsService = new JdbcDaoImpl();
			((JdbcDaoImpl) userDetailsService).setDataSource(dataSource);
		}
		return userDetailsService;
	}

}

一起使用@EnableWebSecurity註解和WebSecurityConfigurerAdapter,可以提供web基礎安全機制

5. 附加說明

如果你使用了springboot DataSource數據源對象,它會自動裝配,你不需要再自定義,只需要注入即可。它需要注入到由Spring Security提供的UserDetailsService中,結合使用JdbcDaoImpl。如果需要,你也能替換它去自定義實現。

6. 驗證服務配置

驗證服務驗證客戶端和用戶憑證,同時生成token

package com.md.demo.oauth.opaque.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.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.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;

    private TokenStore tokenStore;

    public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
                                            final AuthenticationManager authenticationManager) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
    }

    @Bean
    public TokenStore tokenStore() {
        if (tokenStore == null) {
            tokenStore = new JdbcTokenStore(dataSource);
        }
        return tokenStore;
    }

    @Bean
    public DefaultTokenServices tokenServices(final ClientDetailsService clientDetailsService) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setAuthenticationManager(authenticationManager);
        return tokenServices;
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore());
    }

    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer.passwordEncoder(passwordEncoder)
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

}

7. 測試接口訪問

package com.md.demo.oauth.rest;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("/profile")
public class ProfileController {

    //http://localhost:9001/profile/me?access_token=xxxxxx
    @GetMapping("/me")
    public ResponseEntity<Principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }

    /*
	 * 通過以下接口Post請求獲得access_token:
	 * http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
	 * 
	 * 注意:Authorization中使用BasicAuth,Username和Password分別爲:clientId,secret
	 */
}

資源服務 Resource Server

1. 項目目錄結構

2. pom.xml依賴組件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib-ext-spring</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>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
</dependency>

3. 接口訪問(受保護)

package com.md.demo.oauth.rest;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("/me")
public class UserController {

    //http://localhost:9101/me?access_token=xxxxxx
    @GetMapping
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<Principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }

    /*
	 * 通過以下接口Post請求獲得access_token:
	 * http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
	 * 
	 * 注意:Authorization中使用BasicAuth,Username和Password分別爲:clientId,secret
	 */
}

@PreAuthorize註解驗證用戶的權限去是否執行代碼,同時需要啓用prePost註解

4. Web安全配置

package com.md.demo.oauth.opaque.ds.config;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {

}

prePostEnabled默認是false,使@PreAuthorize生效,則設爲true

5. 資源服務配置

package com.md.demo.oauth.opaque.ds.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 {

	private static final String ROOT_PATTERN = "/**";

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				// 排除路徑驗證
				.antMatchers("/hello")
					.permitAll()
				.antMatchers(ROOT_PATTERN)
					.authenticated();
	}
}

6. application.yml配置

server:
  port: 9101

# 設置url,把token轉換成authentication對象
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:9001/profile/me

spring:
  jackson:
    serialization:
      INDENT_OUTPUT: true

測試訪問

1. 訪問受保護接口:http://localhost:9101/me,默認失敗

2. 獲得token接口:http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass

3. 再訪問受保護接口:http://localhost:9101/me?access_token=8df31c53-e616-47d1-802e-9b00adf64266,即可訪問成功

完整源碼下載

我的Github源碼倉庫地址:

下一章教程

SpringBoot從入門到精通教程(二十三)- Oauth2+JWT集成/SpringSecurity

該系列教程

SpringBoot從入門到精通教程

 

 

 

至此,全部介紹就結束了

 

------------------------------------------------------

------------------------------------------------------

 

關於我(個人域名)

我的開源項目集Github

 

期望和大家一起學習,一起成長,共勉,O(∩_∩)O謝謝

歡迎交流問題,可加個人QQ 469580884,

或者,加我的羣號 751925591,一起探討交流問題

不講虛的,只做實幹家

Talk is cheap,show me the code

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