OAuth2保护微服务 案例实战
文章目录
前言
不得不吐槽一下,一些经典书籍的案例 真的已经过时了,首先依赖包不适用,案例不正确,代码欠缺,而且很多关键位置的代码并没有贴出来,这对于许多不仅仅是看书,也想动手敲一敲案例的学习者来说很不友好!!一度都想跳过这一章,记录使用使用OAuth2+SpringSecurity+SpringCloud时遇到的一系列坑。
参照《Spring微服务实战》
1. 依赖包问题
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<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>
注意起步依赖 spring-cloud-starter···,原书中的spring-cloud-···真的让我吐了,一直怀疑可能是因为我版本的问题,查了很久之后发现,确实是包的问题。
工程结构如图:
2.服务引导类Application
@SpringBootApplication
@RestController
@EnableResourceServer
@EnableAuthorizationServer //OAuth2服务
public class OAuth2Application {
@GetMapping(value = "/auth/user",produces = "application/json")
public Map<String,Object> user(OAuth2Authentication user){
Map<String,Object> userInfo=new HashMap<>();
userInfo.put("user",user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
return userInfo;
}
public static void main(String[] args){
SpringApplication.run(OAuth2Application.class,args);
}
}
OAuth2服务的引导类,除了通过注解声明该服务是OAuth2服务外,添加了一个 /user 路径映射,当有客户端服务访问由OAuth2保护的服务时会调用这个端点。(说直白点就是,客户端服务拿着授权令牌Authorization时,对这个Authorization进行解析,回返回对应的user和authorities)。
此类在原书中仍然是个坑,在指向验证服务时,原书里的userInfoUri:/auth/user,而原书里的启动类却是RequestMapping("/user"),说是会映射到/auth/user路径上去,然而我在试验时,并不是这样,而是要通过/user才可以,这不是给自己添麻烦吗?! (我猜测本书中可能有某个地方设置一个类似路径prefix这样的东西,因为其实所有的请求路径都是 ip+port/auth/···· 然而 它实际的编码里 并没有添加/auth )
3.配置类OAuth2
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //指定为内存存储 jdbc存储或内存存储
.withClient("orgFeign") //注册的应用程序名称
.secret("thisissecret") //密钥
.authorizedGrantTypes( //授权类型
"password",
"client_credentials"
).scopes("webclient","mobileclient"); //作用域
//定义验证服务注册的客户端应用程序
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// .tokenKeyAccess("permitAll()") // auth/oauth/token_key是公开的
// .checkTokenAccess("permitAll()") // auth/oauth/check_token是公开的
// .allowFormAuthenticationForClients() //表单验证(申请令牌)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
该类定义了哪些应用程序可以访问由OAuth2保护的服务,重点关注覆写的两个configure方法。
第一个configure中,定义了哪些客户端注册到OAuth2中,withClient()和secret()是客户端应用程序获取OAuth2访问令牌的名称和密钥,authorizedGrantTypes()为授权类型列表 这里指定为密码类型,scopes为作用域,为自定义的内容 可以用来做细粒度的授权规则。
第二个configure中,有三行注释掉的代码,其实这是我出现401时参照别人的解决方案,但实际无济于事,我401的时候加了还是401,我解决正常运行后注释掉也不影响它照常工作。
4. 配置类Security
与上一个配置类不同,上一个是配置OAuth2相关的,这个配置类是配置SpringSecurity的用户id 密码和角色。与单独使用SpringSecurity进行权限控制资源保护的基本一样。
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("jam").password("123456").roles("USER")
.and()
.withUser("yang").password("123456").roles("USER","ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll();
}
}
同样重点也是两个configure方法,本文的例子中为了方便选择了一个弃用的密码编码器NoOpPasswordEncoder,官方已不推荐使用,第一个configure方法采用基于内存的方式注册了两个用户jam和yang;第二个configure方法才是解决了我之前出现401的关键步骤之一!!!原书中并没有提到需放通 /oauth/token 这一路径,实属有点坑人。
以上就可以尝试起通过OAuth2服务来获取令牌以及验证授权。
5. Postman测试
首先 是这里的路径问题 原书中是 ip:port/auth/oauth/token 同理,用加上auth之后仍然是不行的,加上前面Security配置类上我们放通的是 /oauth/token(只需要放通,背后映射做的处理由OAtuh2帮我们完成),所以正确的路径如图,需要选择Authorization的Type为Basic Auth,右边有两个参数要填写,这里的Username和Password分别对应OAuth2配置类的withClient()和secret(),
除了Authorization之外,还需要写入对应的参数,这里需要在body里选择表单数据 form-data,然后分别写grant_type授权类型,scope作用域,username和password(注册的用户的用户名密码),前两个参数参见 3.OAtuh2配置类,用户名密码是Security配置类上注册的。这里有两个 注释掉的参数 client_id 和client_secret 这是一些博客上解决访问/auth/token报401的方案,但是我这里行不通,所以我把它注掉了。
结果可以看到 access_token和token_type 此为OAuth2颁发的令牌,而我们可以拿着这个令牌去OAuth2服务器验证权限,还记得2.的引导类中的那个地址映射吗?可以在HTTP头部携带这个令牌去验证。
至此,授权令牌的颁发与验证就已经完成了。
6. 配置被保护的服务
// 本文以一个 OrgnizationApplication-9001为例
6.1 yaml与引导类
server:
port: 9001
security:
oauth2:
resource:
userInfoUri: http://localhost:4396/auth/user
@SpringBootApplication
@EnableResourceServer //注解为受OAuth2保护的资源
@EnableEurekaClient
public class OrganizationApplication9001 {
public static void main(String[] args){
SpringApplication.run(OrganizationApplication9001.class,args);
}
}
@EnableResourceServer 会强制执行一个Filter,会对所有到达改服务的请求执行Filter检查传入的HTTP首部是否存在OAuth2访问令牌,然后通过回调yaml中的uri来查看令牌是否有效。
6.2 测试的Controller
@RestController
public class OrganizationController {
@GetMapping("/organization")
public String getMsg(){
return "This is organization 9001";
}
}
6.2 定义访问规则
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET,"/organization")
.hasRole("ADMIN")
.anyRequest()
.authenticated();
}
}
这里定义了 /organization 资源路径的规则,需要ADMIN角色,(本文中的两个用户jam为user角色,而yang为user和admin角色),所以可以通过分别向OAuth获取这两个用户的access_token 然后访问这个服务 加以验证。
6.3 访问被保护的服务
如图,这个截图例子测试的是jam用户也即user角色的结果,会发现403拒绝访问,达到了预期的效果,yang用户即admin角色可以自行测试,是可以正常放通进行访问的。