最近在学习OAuth2同时也在思考将它整合入微服务架构的最佳方式。基于OAuth2剥离各微服务权限认证功能,将其单独抽象为一个权鉴服务是我比较认可的一种方式,在这个基础上个人认为对于一些权限要求简单的场景,可以使用zuul网关集成权鉴功能验证Token,好处是内网微服务可以完全抛开权限更专注于业务逻辑,而zuul网关本身就是访问入口,由它来负责鉴权工作也较为合适。基于这个思路我搭建了一套demo工程用于验证,并将其整理为博文分享出来希望对同样感兴趣的同学有所帮助。
补充:如果说科研上分为理论研究和应用研究的话,那我一定属于后者更注重于应用研究。因此下文将不会赘述各种理论和概念,而是采用最简单方式来快速验证笔者的设想(本文的默认读者应对OAuth2有一定理论储备)。
一、架构说明
我的最终目的是在项目中搭建如下图的OAuth2和微服务的集成架构。
为了让读者可以清晰的理解和快速上手搭建demo工程,我去了掉了项目中所有不影响工程搭建的非必要配置和模块,只保留可以验证试验目的的核心内容。因此对上面架构做了适当裁剪形成下图,本文是基于它来搭建工程。
上图中请求资源分为两组操作,第一组为身份认证授予权限,第二组为携带认证访问资源。
第一组:
- 客户端(Client)向网关服务器(Zuul)发起登录请求。
- 网关服务器(Zuul)向认证服务器(Authorization)转发登录请求。
- 认证服务器(Authorization)向网关(Zuul)返回Jwt Token。
- 网关服务器(Zuul)向客户端(Client)返回Jwt Token。
第二组:
- A 客户端(Client)携带Jwt Token向网关服务器(Zuul)发起资源请求。
- B 网关服务器(Zuul)验证Jwt Token是否有效。
- C 验证有效网关服务器(Zuul)转发请求到资源服务器(Resource)。
- D 资源服务器(Resource)返回资源到网关服务器(Zuul)。
- E 网关服务器(Zuul)返回资源到客户端(Client)。
二、搭建demo工程
工程基于springboot搭建,为了更简便直观权限配置采用内存模式,且仅实现了登录和获取资源的最简代码。以下贴出搭建工程的核心类代码。
2.1.搭建认证服务器
pom.xml添加Maven依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
OAuth2配置
在最简的实现下认证服务器端需要配置以下三个核心类
AuthorizationServerConfig 此类负责在认证端定义客户端和资源服务器及其权限
AuthorizationWebSecurityConfig 此类负责客户端提交的登录用户验证配置
UserDetailServiceImpl 此类负责根据用户名查询用户信息并封装为具体对象
贴代码
AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
/**
* 配置authenticationManager用于认证的过程
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenServices(tokenServices());
}
/**
* 重写此方法用于声明认证服务器能认证的客户端信息
* 相当于在认证服务器中注册哪些客户端(包括资源服务器)能访问
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 采用内存配置
.withClient("client") //定义访问认证服务器的客户端
.secret(passwordEncoder.encode("123456")) //客户端访问认证服务器需要带上的密码
/**
* 定义token权限范围也可理解为角色,其名称可自定义
* 此权限与资源服务器中定义的访问资源所需权限相对应
* 客户端在申请权限时提交scope须与认证端定义权限一致
* 才能获取token,如认证端不定义权限,则客户端申请任
* 何权限均会被授予token
* */
.scopes("read","write")
.accessTokenValiditySeconds(3600) //token过期时间
.resourceIds("resource") //定义客户端可请求的资源服务器
.authorizedGrantTypes("password"); //密码模式
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()"); //开启/oauth/check_token验证端口认证权限访问
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**--------------------------------------JWT 配置--------------------------------------*/
@Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
/**
* TokenStore
* OAuth2 Token(令牌)持久化接口,用于定义 Token 如何存储,它有几个实现类:
* InMemoryTokenStore:实现了在内存中存储令牌
* JdbcTokenStore:通过 JDBC 方式存储令牌
* JwtTokenStore:通过 JWT 方式存储令牌
*/
tokenServices.setTokenStore(new JwtTokenStore(tokenConverter())); // 配置Token存储方式
tokenServices.setTokenEnhancer(tokenConverter()); // 配置Token转换器
return tokenServices;
}
public JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 配置Token加密Key,注意要与客户端保持一致; 也可以配置成RSA方式;
converter.setSigningKey("7Ub2aV");
return converter;
}
}
AuthorizationWebSecurityConfig
@Configuration
@EnableWebSecurity
public class AuthorizationWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
UserDetailServiceImpl
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 认证的过程,由AuthenticationManager去调
* 当前没有查询数据库,用户写死在代码中,实际应
* 用时需要重写
* @param username 用户名
* @return 用户详情对象
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return User.withUsername(username)
.password(passwordEncoder.encode("123456"))
.authorities("ROLE_ADMIN")
.build();
}
}
2.2.搭建网关服务器
pom.xml添加Maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
OAuth2配置
在最简的实现下网关服务器端需要配置以下内容
ZuulApplication 此类负责启动网关
ResourceServerConfig 此类负责在资源服务器端定义自身OAuth配置和资源权限配置
application.yml 此文件负责定义请求路由
贴代码
ZuulApplication
@EnableZuulProxy
@EnableResourceServer
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
ResourceServerConfig
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//声明该资源服务器的id,当请求过来时会首先判断token是否有访问该资源服务器的权限
resources.resourceId("resource").stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated(); // 全部路径均需登录访问(除以排除在外的)
}
/**
* 配置身份认证管理器
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
oAuth2AuthenticationManager.setTokenServices(tokenServices());
return oAuth2AuthenticationManager;
}
/**
* 获取权限鉴别对象
* @return
*/
@Bean
public ResourceServerTokenServices tokenServices() {
// 与授权服务器保持一致
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(new JwtTokenStore(tokenConverter()));
tokenServices.setTokenEnhancer(tokenConverter());
return tokenServices;
}
@Bean
public JwtAccessTokenConverter tokenConverter() {
// 与服务器端保持一致,注意这里需要将TokenConverter注入到容器中去,
// 否则会报错:InvalidTokenException, Cannot convert access token to JSON
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("7Ub2aV");
return converter;
}
}
application.yml
server:
port: 9000
spring:
application:
name: zuul
zuul:
sensitive-headers: #清空过滤内容,也可配置保留对Cookie,Set-Cookie的过滤
routes:
login: #路由别名,无其他意义,转发获取token请求
path: /login/**
url: http://localhost:8080/oauth/token
order: #路由别名,无其他意义,转发资源请求
path: /order/**
url: http://localhost:8081
2.3.搭建资源服务器
在最简的实现下资源服务器端只需要配置一个提供资源访问的类即可
贴代码
ResourceController
@RestController
@RequestMapping("resource")
public class ResourceController {
@GetMapping
public Object getInfo(HttpServletRequest request) {
return "success "+getRemortIP(request);
}
private String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
}
三、运行测试
工程搭建好后就可以启动测试了,我使用postman工具来进行测试。