oauth2简介
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
OAuth 2 定义了四种 Grant Type,授权码模式(authorization code)、简化模式(implicit)、密码模式(Password)、客户端模式(client credentials),每一种都有适用的应用场景。
运行流程如下所示:
至于每种模式如何运行,需要什么参数,本文不再赘述,大家可自行查阅相关资料,做到烂熟于心。
微服务安全策略
在传统单体Web应用架构中,身份认证从来都不是问题,通过Spring Security或者Shiro,配合session,可以很方便的解决身份认证和鉴权的问题。
随着不断扩大的业务需求,在传统单体架构捉襟见肘的时候,分布式架构应运而生,分布式架构强调的是服务化以及服务的分散化,大大地提升了系统的可靠性和响应速度。
可是,技术并没有因此而停滞不前,微服务架构又出现了。微服务架构更强调服务的专业化、精细化,更加强调单一职责、轻量级通信(HTTP)、独立性并且进程隔离。
随着应用架构的改变,身份认证的方式也在发生变化,为了适应架构的变化、需求的变化,身份认证与鉴权方案也需要不断的变革。
抛开鉴权不谈,先说一下身份认证。David Borsos 在伦敦的微服务大会上提出的四种方案,单点登录(SSO)、分布式session、客户端 Token、客户端 Token 与 API 网关结合。其中,后两者颇受开发者欢迎,也很适合微服务架构。然而,无论后两者中的哪一种方案,均需一个独立的授权服务器配合,才能发放token,进而协助资源服务器进行身份认证。
搭建授权服务器
基于Spring Cloud Oauth2框架搭建基本的授权服务。
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.luas.cloud</groupId>
<artifactId>java-boot-parent-2.1</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../java-boot-parent-2.1</relativePath>
</parent>
<groupId>com.luas.xmall</groupId>
<artifactId>xmall-auth</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>xmall-auth</name>
<description>xmall authorization center</description>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- nacos cloud -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
动态配置
同前文所述,bootstrap.yml中配置nacos发现地址、配置中心地址,实现服务注册及配置远程获取、动态刷新。后续此类配置均相同,不再赘述。
spring:
application:
name: xmall-auth
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yml
discovery:
server-addr: 127.0.0.1:8848
AuthorizationServer
AuthorizationServer主要配置有三大类,安全配置、端点配置、客户端配置。
安全配置主要针对授权服务器端点的访问策略、认证策略、加密方式等进行配置。
端点配置主要配置授权服务器的token存储方式、token转换、端点增强、端点自定义、token授权、token生成等进行配置。
客户端配置主要配置接入的客户端相关信息,如授权类型、授权范围、秘钥等内容。
package com.luas.xmall.auth.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;
import org.springframework.security.oauth2.config.annotation.builders.JdbcClientDetailsServiceBuilder;
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.client.InMemoryClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new InMemoryTokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String client_secret = passwordEncoder.encode("123456");
clients
.inMemory()
// admin,授权码认证、密码认证、客户端认证、简单认证、刷新token
.withClient("admin")
.secret(client_secret)
.resourceIds("xmall-auth", "xmall-product")
.scopes("server", "select")
.authorizedGrantTypes("authorization_code", "password", "refresh_token", "client_credentials", "implicit")
.redirectUris("http://www.baidu.com")
.and()
// client_1,密码认证、刷新token
.withClient("client_1")
.secret(client_secret)
.resourceIds("xmall-auth", "xmall-product")
.scopes("server", "select")
.authorizedGrantTypes("password", "refresh_token")
.and()
// client_2,客户端认证、刷新token
.withClient("client_2")
.secret(client_secret)
.resourceIds("xmall-auth", "xmall-product")
.scopes("server", "select")
.authorizedGrantTypes("client_credentials", "refresh_token");
}
}
ResourceServer
主要负责针对资源服务器的安全访问策略进行相关配置。
为什么授权服务器还需要进行资源服务器相关配置?
举一个最简单的例子。比如github,第三方接入之后,发起请求,用户跳转到github进行登录并授权,这时,接入方可以得到授权,但是,并不知道用户的具体信息,需要带着授权去询问github,当前用户的相关信息。
github进行相关身份认证之后,将可授权的用户信息返回给接入方。这个获取当前用户相关信息的接口端点,是为资源。既然是资源,就要受资源服务器管辖,进而就需要进行资源服务器配置。
package com.luas.xmall.auth.configuration;
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;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("xmall-auth").stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/oauth/user")
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
注意,用户相关信息的接口端点必须由ResourceServer安全策略拦截。
SpringSecurity配置
老套路了,毋庸置疑需要配置。可前两个都进行了SpringSecurity相关配置,为何还需要单独针对SpringSecurity再进行配置呢?
答案之一就在于授权模式中的授权码模式,后续会出相关文章,详细说明这个问题。此外,还需配置UserDetailsService、PasswordEncoder等内容。
package com.luas.xmall.auth.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.User;
import org.springframework.security.core.userdetails.UserDetails;
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.provisioning.InMemoryUserDetailsManager;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.anyRequest()
.and()
.formLogin()
.and()
.csrf().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean(name = "userDetailsService")
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return createUserDetailsService();
}
@Override
protected UserDetailsService userDetailsService() {
return createUserDetailsService();
}
private UserDetailsService createUserDetailsService() {
String password = passwordEncoder().encode("123456");
List<UserDetails> users = new ArrayList<>();
UserDetails user_admin = User.withUsername("admin").password(password).authorities("ADMIN", "USER").build();
UserDetails user_1 = User.withUsername("user_1").password(password).authorities("ADMIN", "USER").build();
UserDetails user_2 = User.withUsername("user_2").password(password).authorities("USER").build();
users.add(user_admin);
users.add(user_1);
users.add(user_2);
return new InMemoryUserDetailsManager(users);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
用户信息端点
为接入方提供用户信息查询端点,查询当前认证用户的相关信息。
package com.luas.xmall.auth.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/oauth")
public class UserController {
@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
}
启动
端口配置为7777,启动服务。
授权
本文使用Postman工具,模拟rest请求。
访问http://localhost:7777/oauth/token,输入一众参数,诸如client_id、client_secret、grant_type等,点击Send,即出现access_token、refresh_token等信息。
用户信息
拿着上一步获取的授权,访问http://localhost:7777/oauth/user,查询当前用户的相关信息。
注意,要添加授权token,本文采用header方式(header方式添加授权token,即在HttpHeader中添加header Authentication,内容为Bearer+空格+token),如下所示:
授权服务器搭建成功!