leftso 6587 0 2018-09-02 16:19:10

引言

在这篇文章中,我们将讨论如何使用Spring Boot Security OAuth2保护REST API。我们将为不同的crud操作实现AuthorizationServerResourceServer和一些REST API,并使用Postman测试这些API。在这里我们将使用mysql数据库读取用户证书而不是内存认证。另外,为了简化我们的ORM解决方案,我们将使用spring-data和BCryptPasswordEncoder进行密码编码。

什么是OAuth

      OAuth只是一个安全的授权协议,它处理第三方应用程序授权访问用户数据而不暴露其密码。例如。(在许多网站上用fb,gPlus,twitter进行登录..)都在这个协议下工作。当你知道涉及的各方时,协议变得更容易。基本上有三方参与:oAuth提供者,oAuth客户端和所有者。这里,oAuth提供者提供了诸如Facebook,Twitter之类的身份验证令牌。同样,oAuth客户端是希望代表所有者访问凭证的应用程序,所有者是拥有oAuth提供程序(例如facebook和twitter)的帐户的用户。

 

什么是OAuth2

OAuth 2是一种授权框架,它使应用程序能够获得有限访问HTTP服务(如Facebook,GitHub和DigitalOcean)上的用户帐户的权限。它的工作方式是将用户身份验证委派给承载用户帐户的服务,并授权第三方应用程序访问该用户帐户。OAuth 2为Web和桌面应用程序以及移动设备提供授权流程。

 

OAuth2角色

OAuth2提供4种不同的角色。

  • Resource Owner: 用户
  • Client: 接入应用
  • Resource Server: API
  • Authorization Server: API

OAuth2授权类型

以下是OAuth2定义的4种不同的授权类型

授权码(Authorization Code):与服务器端应用程序一起使用

隐式(Implicit):用于移动应用程序或Web应用程序(在用户设备上运行的应用程序)

资源所有者密码凭证(Resource Owner Password Credentials):与受信任的应用程序(如服务本身拥有的应用程序)一起使用

客户端证书(Client Credentials):与应用程序API访问一起使用

项目结构

以下是Spring Boot Security OAuth2实现的项目结构。
以下是Spring Boot Security OAuth2实现的项目结构。
 

Maven的依赖

pom.xml

以下是所需的依赖关系。

<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> 
		
    </ dependencies>


 

OAuth2授权服务器配置

这个类扩展AuthorizationServerConfigurerAdapter并负责生成特定于客户端的令牌。假设,如果用户想通过Facebook 登录到devglan.com,那么facebook auth服务器将为Devglan生成令牌。在这种情况下,Devglan成为客户端,它将成为代表用户从脸书 - 授权服务器请求授权代码。以下是Facebook将使用的类似实现。

在这里,我们使用内存凭证,client_id作为devglan-client,CLIENT_SECRET作为devglan-secret。但您也可以自由使用JDBC实现。

@EnableAuthorizationServer:启用授权服务器。AuthorizationServerEndpointsConfigurer定义授权和令牌端点以及令牌服务。

AuthorizationServerConfig.java:
 

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.provider.token.TokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	static final String CLIEN_ID = "devglan-client";
	static final String CLIENT_SECRET = "devglan-secret";
	static final String GRANT_TYPE = "password";
	static final String AUTHORIZATION_CODE = "authorization_code";
	static final String REFRESH_TOKEN = "refresh_token";
	static final String IMPLICIT = "implicit";
	static final String SCOPE_READ = "read";
	static final String SCOPE_WRITE = "write";
	static final String TRUST = "trust";
	static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60;
    static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60;
	
	@Autowired
	private TokenStore tokenStore;

	@Autowired
	private AuthenticationManager authenticationManager;

	@Override
	public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {

		configurer
				.inMemory()
				.withClient(CLIEN_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资源服务器配置

我们的上下文中的资源是我们为粗暴操作公开的REST API。要访问这些资源,必须对客户端进行身份验证。在实时场景中,每当用户尝试访问这些资源时,都会要求用户提供他真实性,一旦用户被授权,他将被允许访问这些受保护的资源。

@EnableResourceServer:启用资源服务器
ResourceServerConfig.java:
 

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;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

	private static final String RESOURCE_ID = "resource_id";
	
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) {
		resources.resourceId(RESOURCE_ID).stateless(false);
	}

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

}

安全配置

这个类扩展了WebSecurityConfigurerAdapter并提供了通常的spring安全配置。这里,我们使用bcrypt编码器来编码我们的密码。您可以尝试使用此在线Bcrypt工具对密码进行编码和匹配。以下配置基本引导了授权服务器和资源服务器。

@EnableWebSecurity:启用弹簧安全Web安全支持。

@EnableGlobalMethodSecurity:支持方法级别访问控制,如@PreAuthorize @PostAuthorize

在这里,我们正在使用inmemorytokenstore,但您可以自由使用JdbcTokenStore或JwtTokenStore.Here

SecurityConfig.java:
 

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

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

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

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(encoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .anonymous().disable()
                .authorizeRequests()
                .antMatchers("/api-docs/**").permitAll();
    }

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}

Rest API

以下是我们为测试目的而暴露的非常基本的REST API。

UserController.java
 

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

    @Autowired
    private UserService userService;

    @RequestMapping(value="/user", method = RequestMethod.GET)
    public List 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";
    }

}

现在让我们定义负责从数据库中获取用户详细信息的用户服务。接下来是Spring将用来验证用户的实现。
UserServiceImpl.java:
 

@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
	
	@Autowired
	private UserDao userDao;

	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		User user = userDao.findByUsername(userId);
		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 getAuthority() {
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
	}

	public List findAll() {
		List list = new ArrayList<>();
		userDao.findAll().iterator().forEachRemaining(list::add);
		return list;
	}
}

默认数据库SQL脚本

以下是在应用程序启动时插入的插入语句。

INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33);
INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23);
INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);


 

测试应用程序

运行Application.java作为Java应用程序。我们将使用邮递员来测试OAuth2实现。

生成AuthToken:在头文件中,我们有用户名和密码分别为Alex123和密码作为Authorization头。按照Oauth2规范,Access token请求应该使用application/x-www-form-urlencoded.以下设置。
设置postmain工具的HTTP请求头部、
一旦你提出请求,你会得到如下结果。它有访问令牌和刷新令牌。
请求测试结果

无令牌 访问资源使用令牌访问资源使用刷新令牌刷新令牌

无令牌访问返回

有令牌返回

通常情况下,oAuth2的令牌到期时间非常少,您可以使用以下API在令牌过期时刷新令牌。
5

 

常见错误

我可以在评论部分看到,大多数读者遇到了2个错误。因此,添加这个部分最好能帮助读者。

访问此资源需要完整身份验证
 

{
"timestamp": 1513747665246,
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/oauth/token"
}


 

如果您错过了在POST的授权部分添加用户名/密码的情况,则会导致此问题。在此部分中,选择类型为基本身份验证,并提供凭证作为devglan-client和devglan-secret,然后向url - http://localhost:8080/oauth/token来获得授权令牌。以下是截图。
常见错误举证1

 

没有客户端身份验证。尝试添加适当的认证过滤器。

{
    "error": "unauthorized",
    "error_description": "There is no client authentication. Try adding an appropriate authentication filter."
}

在这种情况下,检查你的auth url.It应该是 - http://localhost:8080/oauth/token而不是http://localhost:8080/oauth/token/

缺少授权类型
在这种情况下,您错过了在请求中添加grant_type。请尝试将其添加为password

总结

在本教程中,我们了解了如何通过实现资源服务器和授权服务器来保护REST API与OAUTH2的安全。

项目源码下载