Spring Boot + Spring Security OAuth2示例

簡介

       在這篇文章中,我們將討論使用Spring Boot + Spring Security OAuth2保護REST API的示例。我們將實現AuthorizationServer,ResourceServer和一些REST API用於不同的CRUD基本操作並使用Postman測試通過這些API。我們將使用MySQL數據庫來讀取用戶憑據而不是內存中的身份驗證。另外,爲了簡化我們的ORM解決方案,我們將使用spring-data-jpa和BCryptPasswordEncoder進行密碼編碼,以最快的速度上手。

瞭解OAuth

       OAuth只是一種安全授權協議,用於處理第三方應用程序的授權,使第三方應用程序能夠獲得對Web服務的有限訪問權限,以便在不泄露密碼的情況下訪問用戶數據。(例如在許多網站上使用fackbook,twitter登錄)所有工作都在此協議下。對OAuth有相關的知識瞭解,掌握及上手更容易。基本上涉及三方:OAuth提供商,OAuth客戶和所有者。在此,OAuth提供商提供諸如Facebook,Twitter等身份驗證令牌。同樣,OAuth Client是希望代表所有者訪問憑證的應用程序,所有者是在OAuth提供程序(如facebook和twitter)上擁有帳戶的用戶。

瞭解OAuth2

        OAuth2是一種授權框架,,使應用程序能夠獲得對HTTP服務(如Facebook,GitHub)上的用戶帳戶的訪問權限。它的工作原理是將用戶身份驗證委派給託管用戶帳戶的服務,並授權第三方應用程序訪問用戶帳戶。OAuth 2爲Web和桌面應用程序以及移動設備提供授權流程。

OAuth2角色

OAuth2提供4種不同的角色:

資源所有者(Resource Owner)
資源服務器(Resource Server)
授權服務器(Authorization Server)
客戶端(Client)

Maven依賴(Maven Dependencies)

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.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-data-jpa</artifactId>
             </dependency>
	     <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-security</artifactId>
	      </dependency>
		  <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-security-oauth2</artifactId>
             </dependency>
	     <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
	      </dependency>
	      <dependency>
                     <groupId>commons-dbcp</groupId>
                     <artifactId>commons-dbcp</artifactId>
		</dependency>
		<dependency>
				     <groupId>org.projectlombok</groupId>
			         <artifactId>lombok</artifactId>
		</dependency>
    </dependencies>

OAuth2授權服務器配置

     這個類擴展AuthorizationServerConfigurerAdapter並負責生成特定於客戶端的令牌。在這裏,我們使用內存憑證,其中client_id爲test-client,CLIENT_SECRET爲test-secret。但您也可以自由使用JDBC實現。
@EnableAuthorizationServer:啓用授權服務器.AuthorizationServerEndpointsConfigurer定義授權和令牌端點以及令牌服務。

  • AuthorizationConfig.java
package com.battle.oauth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.token.TokenStore;

/**
 * Description: OAuth2授權服務器配置,
 *
 * @EnableAuthorizationServer; 啓用授權服務器.AuthorizationServerEndpointsConfigurer定義授權和令牌端點以及令牌服務
 */

@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {


    public static final String CLIEN_ID = "test-client";
    public static final String CLIENT_SECRET = "test-secret";

    public static final String GRANT_TYPE_PASSWORD = "password";
    public static final String AUTHORIZATION_CODE = "authorization_code";
    public static final String REFRESH_TOKEN = "refresh_token";
    public static final String IMPLICIT = "implicit";

    public static final String SCOPE_READ = "read";
    public static final String SCOPE_WRITE = "write";
    public static final String TRUST = "trust";

    public static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1 * 60 * 60;
    public static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6 * 60 * 60;


    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
               .withClient(CLIEN_ID)//客戶端ID
               .secret(CLIENT_SECRET)
               .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT )
               .scopes(SCOPE_READ, SCOPE_WRITE, TRUST)
               .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
               .refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);
    }

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

OAuth2資源服務器配置

       我們上下文中的資源是我們爲CRUD操作公開的REST API。要訪問這些資源,必須對客戶端進行身份驗證。在實時方案中,每當用戶嘗試訪問這些資源時,將要求用戶提供他的真實性,一旦用戶被授權,他將被允許訪問這些受保護的資源。

  • ResourceServerConfig.java
package com.battle.oauth;

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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;

/**
 * Description: Oauth2資源服務器,
 * @EnableResourceServer: Enables a resource server
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "resource_id";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID)
                 .stateless(false);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.anonymous().disable()
            .authorizeRequests().antMatchers("/users/**").access("hasRole('ADMIN')")
            .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
    }
}

安全配置

       這個類擴展了WebSecurityConfigurerAdapter並提供了常用的spring安全配置。這裏,我們使用bcrypt編碼器來編碼我們的密碼。您可以嘗試使用此在線Bcrypt工具來編碼和匹配bcrypt密碼。以下配置基本上是引導授權服務器和資源服務器。

  • @EnableWebSecurity:啓用Spring安全Web安全支持。

在這裏,我們使用in-memory-tokenstore但你可以自由使用JdbcTokenStore或JwtTokenStore.Here

  • SecurityConfig.java
package com.battle.oauth;

import org.springframework.beans.factory.annotation.Autowired;
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.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.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * Description:
 * @EnableWebSecurity: 啓用Spring安全Web安全支持。
 * @EnableGlobalMethodSecurity: 支持具有方法級訪問控制,例如@PreAuthorize @PostAuthorize
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public UserDetailsService userDetailsService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(encoder());
    }
    
    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.anonymous().disable();
    }
    
    @Bean
    @Order(0)
    public FilterRegistrationBean filterRegistrationBean(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new FilterRegistrationBean(new CorsFilter(source));
    }
}

Rest APIs

下面是爲我們測試而公開的REST APIs

  • UserController.java
package com.battle.controller;

import com.battle.domain.User;
import com.battle.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 - Description:Rest APIs
 */
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public List<User> listUser() {
        return userService.findAll();
    }

    @RequestMapping(value = "/user", method = RequestMethod.POST)
    public User create(@RequestBody User user) {
        return userService.save(user);
    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable(value = "id") Long id) {
        userService.delete(id);
        return "success";
    }
}

擴展實現UserDetailService接口

讓我們定義負責從數據庫中獲取用戶詳細信息的Userservice。以下是spring將用於驗證用戶的實現。

  • UserServiceImpl.java
package com.battle.service.impl;

import com.battle.dao.UserDao;
import com.battle.domain.User;
import com.battle.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Description: custom user service implement class
 * Created by William.Cheung on 2018/10/22.
 */
@Service
public class UserServiceImpl implements UserDetailsService,UserService {

    @Autowired
    private UserDao userDao;
    
	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("Invalid username or password.");
        }
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority());
    }
    private List<SimpleGrantedAuthority> getAuthority() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
    public List<User> findAll() {
        List<User> list = new ArrayList<>();
        userDao.findAll().iterator().forEachRemaining(list::add);
        return list;
    }
    
    @Override
    public void delete(long id) {
        userDao.delete(id);
    }
    
    @Override
    public User save(User user) {
        return userDao.save(user);
    }
}

SQL腳本執行

啓動程序操作之前先建庫,插入如下數據

INSERT INTO用戶(id,用戶名,密碼,工資,年齡)VALUES1'Adam''$ 2a $ 04 $ I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu'4234,45;

測試應用程序

我們將使用postman來測試OAuth2的實現。
生成AuthToken:在標題中,我們將用戶名和密碼分別作爲admin和密碼作爲授權標頭。根據Oauth2規範,訪問令牌請求應該使用application/x-www-form-urlencoded.以下是設置。

  • TokenEndpoint:用來作爲請求者獲得令牌(Token)的服務,默認的URL是/oauth/token.
  • AuthorizationEndpoint:用來作爲請求者獲得授權的服務,默認的URL是/oauth/authorize.

如下圖操作流程:
在這裏插入圖片描述
在這裏插入圖片描述

  • 如上圖是成功獲取到訪問的token,如下圖我們去訪問開放的REST APIs看給我們返回什麼信息。

在這裏插入圖片描述

  • 以上訪問user接口返回信息是:未授權,未在上下文找到授權對象信息。
  • 以下是成功訪問REST API返回的接口集

在這裏插入圖片描述

  • 由於每一個token的有效期比較短,我們需要通過Refresh_token重新獲取新的token。如下圖:
  • 在這裏插入圖片描述
    Spring boot 2.0x
    更多信息請查閱:https://oauth.net/2/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章