Spring Cloud Security OAuth2.0分布式认证授权
注册中心:eureka
网关:zuul (具体使用哪种技术可自行调整,思路大致不变)
1. 概述
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向 后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服 务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
2. 项目结构
3. 搭建认证服务(Authorization Server)
3.1 引入核心依赖
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2 编写AuthorizationServerConfig
@Configuration
//开启oauth2,auth server模式
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
//配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//client的id和密码
.withClient("client1")
.secret(passwordEncoder.encode("123123"))
//给client一个id,这个在client的配置里要用的
.resourceIds("resource1")
//允许的申请token的方式,测试用例在test项目里都有.
//authorization_code授权码模式,这个是标准模式
//implicit简单模式,这个主要是给无后台的纯前端项目用的
//password密码模式,直接拿用户的账号密码授权,不安全
//client_credentials客户端模式,用clientid和密码授权,和用户无关的授权方式
//refresh_token使用有效的refresh_token去重新生成一个token,之前的会失效
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
//授权的范围,每个resource会设置自己的范围.
.scopes("scope1")
//这个是设置要不要弹出确认授权页面的.
.autoApprove(false)
//这个相当于是client的域名,重定向给code的时候会跳转这个域名
.redirectUris("http://www.baidu.com")
.and()
//在spring cloud的测试中,我们有两个资源服务,这里也给他们配置两个client,并分配不同的scope.
.withClient("client2")
.secret(passwordEncoder.encode("123123"))
.resourceIds("resource2")
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
.scopes("scope2")
.autoApprove(false)
.redirectUris("http://www.sogou.com");
}
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter tokenConverter;
//配置token管理服务
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService);
defaultTokenServices.setSupportRefreshToken(true);
//配置token的存储方法
defaultTokenServices.setTokenStore(tokenStore);
defaultTokenServices.setAccessTokenValiditySeconds(300);
defaultTokenServices.setRefreshTokenValiditySeconds(1500);
//配置token增加,把一般token转换为jwt token
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenConverter));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
return defaultTokenServices;
}
//密码模式才需要配置,认证管理器
@Autowired
private AuthenticationManager authenticationManager;
//把上面的各个组件组合在一起
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//认证管理器
.authorizationCodeServices(new InMemoryAuthorizationCodeServices())//授权码管理
.tokenServices(tokenServices())//token管理
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//配置哪些接口可以被访问
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")///oauth/token_key公开
.checkTokenAccess("permitAll()")///oauth/check_token公开
.allowFormAuthenticationForClients();//允许表单认证
}
}
解释:
- ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService)【客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。】
- AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(tokenservices)
- AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.
管理令牌
AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来 加载身份信息,里面包含了这个令牌的相关权限。
自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类, 里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时 候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了 所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是InMemoryTokenStore ,如其命名,所有的 令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了TokenStore接口:
- InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
- JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。
- JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授 权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。
令牌访问端点配置
AuthorizationServerEndpointsConfifigurer 这个对象的实例可以完成令牌服务以及令牌endpoint配置
配置授权类型(Grant Types)
- AuthorizationServerEndpointsConfifigurer 通过设定以下属性决定支持的授权类型(Grant Types):
- authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置 这个属性注入一个 AuthenticationManager 对象。
- userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfifigurer 这个配置对 象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用 来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。
- authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 “authorization_code” 授权码类型模式。
- implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
- tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并 且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的 需求的时候,才会考虑使用这个。
令牌端点的安全约束
AuthorizationServerSecurityConfifigurer:用来配置令牌端点(Token Endpoint)的安全约束
(1)tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。
(2)checkToken这个endpoint完全公开
(3) 允许表单认证
授权服务配置总结:授权服务配置分成三大块,可以关联记忆。
既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。
既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的 token。
既然暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等。
3.3 定义TokenConfifig
@Configuration
public class TokenConfig {
//配置如何把普通token转换成jwt token
@Bean
public JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//使用对称秘钥加密token,resource那边会用这个秘钥校验token
converter.setSigningKey("uaa123");
return converter;
}
//配置token的存储方法
@Bean
public TokenStore tokenStore() {
//把用户信息都存储在token当中,相当于存储在客户端,性能好很多
return new JwtTokenStore(tokenConverter());
}
}
3.4 编写SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//密码模式才需要配置,认证管理器
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin()
.and()
.logout();
}
@Bean
public UserDetailsService userDetailsService() {
return s -> {
if ("admin".equals(s) || "user".equals(s)) {
return new MyUserDetails(s, passwordEncoder().encode(s), s);
}
return null;
};
}
}
3.4 抽取MyUserDetails
public class MyUserDetails implements UserDetails {
private String username;
private String password;
private String perms;
public MyUserDetails() {
}
public MyUserDetails(String username, String password, String perms) {
this.username = username;
this.password = password;
this.perms = perms;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPerms() {
return perms;
}
public void setPerms(String perms) {
this.perms = perms;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Stream.of(perms.split(",")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3.6 编写yaml
server:
port: 3001
spring:
application:
name: authorization_server
cloud:
client:
ipAddress: 127.0.0.1
eureka:
instance:
prefer-ip-address: false
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
hostname: ${spring.cloud.client.ipAddress}
client:
serviceUrl:
#eurekaServers
defaultZone: http://127.0.0.1:2001/eureka
4. 搭建资源服务(Resource Server)
4.1 引入核心依赖
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4.2 编写application.yml
server:
port: 4001
spring:
application:
name: resource_server1
cloud:
client:
ipAddress: 127.0.0.1
eureka:
instance:
prefer-ip-address: false
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
hostname: ${spring.cloud.client.ipAddress}
client:
serviceUrl:
#eurekaServers
defaultZone: http://127.0.0.1:2001/eureka
4.3 编写ResourceServerConfig
在ResouceServerConfifig中定义资源服务配置,主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要 什么样的权限才能访问某个微服务
@Configuration
//开启oauth2,reousrce server模式
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
//设置我这个resource的id, 这个在auth中配置, 这里必须照抄
.resourceId("resource1")
.tokenStore(tokenStore)
//这个貌似是配置要不要把token信息记录在session中
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
//由于在zuul已经做了scope的校验,这里可以不写了.当然你想写上也是没有问题的
.antMatchers("/**").permitAll()//.access("#oauth2.hasScope('scope1')")
.and()
//这个貌似是配置要不要把token信息记录在session中
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
4.4 编写SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll();
}
}
4.5 编写controller资源
@RestController
public class IndexController {
@RequestMapping("user")
public String user() {
return "user";
}
//测试接口
@RequestMapping("admin")
@PreAuthorize("hasAnyAuthority('admin')")
public String admin() {
return "admin";
}
@RequestMapping("me")
public Principal me(Principal principal) {
return principal;
}
}
5. 搭建eureka注册中心
5.1 引入核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
5.2 编写application.yml
server:
port: 2001
spring:
application:
name: eureka
cloud:
client:
ipAddress: 127.0.0.1
eureka:
instance:
#基于ip配置
prefer-ip-address: false
#自定义id
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
hostname: ${spring.cloud.client.ipAddress}
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
#注册到其他eureka
registerWithEureka: false
#从其他eureka拉取信息
fetchRegistry: false
server:
#自我保护
enable-self-preservation: false
6. 搭建zuul网关
6.1 引入核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
6.2 编写application.yaml
server:
port: 5001
spring:
application:
name: zuul
cloud:
client:
ipAddress: 127.0.0.1
eureka:
instance:
prefer-ip-address: false
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
hostname: ${spring.cloud.client.ipAddress}
client:
serviceUrl:
#eurekaServers
defaultZone: http://127.0.0.1:2001/eureka
6.3 自定义filter
//定义一个自定义的filter
@Component
public class ScopeFilter extends ZuulFilter {
@Override
public String filterType() {
//pre
//routing
//post
//error
return "pre";//定义是前置拦截器
}
@Override
public int filterOrder() {
return 0;//顺序
}
@Override
public boolean shouldFilter() {
return true;//开启
}
@Override
public Object run() {
//获取request对象
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String requestURI = request.getRequestURI();
//如果是对授权中心的访问,则全部放过
if (requestURI.startsWith("/authorization_server")) {
return null;
}
//获取token
String token = request.getHeader("Authorization");
try {
//解析token
//JWTToken是由3部分组成的,以"."分割,第一段是头信息,第二段是授权信息,第三段是校验码
//这里我们只需要第二段,取其中的scope做校验即可.
//zuul只负责scope部分的校验,具体的权限由各个微服务自己做具体校验.
token = token.split("\\.")[1];
byte[] bytes = Base64.getUrlDecoder().decode(token);
token = new String(bytes, "UTF-8");
//解析为json对象
JSONObject tokenJSON = JSONObject.parseObject(token);
System.out.println(tokenJSON);
//取scope
List<String> scope = tokenJSON.getJSONArray("scope").toJavaList(String.class);
//访问资源服务1,需要scope1的授权
if (requestURI.startsWith("/resource_server1") && scope.contains("scope1")) {
return null;
}
//访问资源服务2,需要scope2的授权
if (requestURI.startsWith("/resource_server2") && scope.contains("scope2")) {
return null;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//未通过校验,返回403.
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(403);
return null;
}
}
测试
获取token
http://localhost:5001/authorization_server/oauth/token
验证token
http://localhost:5001/authorization_server/oauth/token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UxIl0sInNjb3BlIjpbInNjb3BlMSJdLCJleHAiOjE2ODA3NjQyMzksImp0aSI6ImNmMDM3NWMzLTM5M2UtNDBiZi1iMDgwLTYzZjU2YWQ2YzRlOCIsImNsaWVudF9pZCI6ImNsaWVudDEifQ.9FYPDo4lN3L47bybmLjNtGTM_KYa4Eh7CfWolERZcEQ
访问资源
http://localhost:5001/resource_server1/me