来自一个小伙伴在微信上的提问:
看到这个问题,松哥忽然想到我自己之前写过 Spring Boot+Swagger 的用法:
也写过 OAuth2 + Jwt 的用法:
但是还没有将这两个结合在一起写过,所以小伙伴们对此有了疑问,想一想这还是一个非常常见的问题,因为现在使用令牌登录的场景越来越多,在这种情况下,如果使用 Swagger 来测试接口,要怎么在请求头中携带 Token 呢?今天松哥就来和大家聊一聊。
如果小伙伴们没有看过松哥之前发的 OAuth2 系列文章,建议一定先看下(公众号江南一点雨后台回复 OAuth2 获取),再来看本文内容,否则接下来的内容可能会犯迷糊。
这里松哥搭建一个 OAuth2+JWT 的环境来做演示。一共搭建两个服务:
服务名 | 端口 | 备注 |
---|---|---|
auth-server | 8080 | 授权服务器 |
user-server | 8081 | 资源服务器 |
我稍微解释一下:
OK,这是我们项目的一个大致规划。
接下来我们来搭建 OAuth2 测试环境。
首先我们搭建一个名为 auth-server 的授权服务,搭建的时候,选择如下三个依赖:
项目创建完成后,首先提供一个 Spring Security 的基本配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("sang")
.password(passwordEncoder().encode("123"))
.roles("admin")
.and()
.withUser("javaboy")
.password(passwordEncoder().encode("123"))
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().formLogin();
}
}
在这段代码中,为了代码简洁,我就不把 Spring Security 用户存到数据库中去了,直接存在内存中。
这里我创建了一个名为 sang 的用户,密码是 123,角色是 admin。同时我还配置了一个表单登录。
这段配置的目的,实际上就是配置用户。例如你想用微信登录第三方网站,在这个过程中,你得先登录微信,登录微信就要你的用户名/密码信息,那么我们在这里配置的,其实就是用户的用户名/密码/角色信息。
需要注意的是,在当前案例中,我将采用 OAuth2 中的 password 模式进行登录,因此这里还需要明确的提供一个 AuthenticationManager 的 Bean。
基本的用户信息配置完成后,接下来我们来配置授权服务器。
首先来配置 TokenStore:
@Configuration
public class AccessTokenConfig {
@Bean
TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("javaboy");
return converter;
}
}
接下来对授权服务器进行详细配置:
@EnableAuthorizationServer
@Configuration
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Autowired
ClientDetailsService clientDetailsService;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(60 * 60 * 24 * 2);
services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);
return services;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("javaboy")
.secret(passwordEncoder.encode("123"))
.resourceIds("res1")
.authorizedGrantTypes("password", "refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/index.html");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenServices(tokenServices());
}
}
这段代码有点长,我来给大家挨个解释:
好了,如此之后,我们的授权服务器就算是配置完成了,接下来我们启动授权服务器。
如果小伙伴们对于上面的配置感到迷糊,可以在公众号后台回复 OAuth2,先系统的学习一下松哥的 OAuth2 教程。
接下来我们搭建一个资源服务器。大家网上看到的例子,资源服务器大多都是和授权服务器放在一起的,如果项目比较小的话,这样做是没问题的,但是如果是一个大项目,这种做法就不合适了。
资源服务器就是用来存放用户的资源,例如你在微信上的图像、openid 等信息,用户从授权服务器上拿到 access_token 之后,接下来就可以通过 access_token 来资源服务器请求数据。
我们创建一个新的 Spring Boot 项目,叫做 user-server ,作为我们的资源服务器,创建时,添加如下依赖:
项目创建成功之后,先把前面的 AccessTokenConfig 拷贝到资源服务器上,然后添加如下配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res1").tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
}
}
这段配置代码很简单,我简单的说一下:
接下来我们再来配置两个测试接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
}
如此之后,我们的资源服务器就算配置成功了。
分别启动授权服务器和资源服务器,先访问授权服务器获取 access_token:
再利用拿到的 access_token 去访问资源服务器:
OK,测试没问题。
接下来,我们在 user-server 中加入 swagger 功能,首先我们加入 swagger 依赖:
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
这里加入的依赖有两个,一个用来生成接口数据,另一个 swagger-ui 用来做数据展示。
请求头加参数,这里给大家介绍两种,先来看第一种。
先配置一个 Docket 实例,如下:
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.javaboy.oauth2.res.controller"))
.paths(PathSelectors.any())
.build()
.securityContexts(Arrays.asList(securityContexts()))
.securitySchemes(Arrays.asList(securitySchemes()))
.apiInfo(new ApiInfoBuilder()
.description("接口文档的描述信息")
.title("微人事项目接口文档")
.contact(new Contact("javaboy","http://www.javaboy.org","[email protected]"))
.version("v1.0")
.license("Apache2.0")
.build());
}
private SecurityScheme securitySchemes() {
return new ApiKey("Authorization", "Authorization", "header");
}
private SecurityContext securityContexts() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.any())
.build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("xxx", "描述信息");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Arrays.asList(new SecurityReference("Authorization", authorizationScopes));
}
}
这里的配置稍微有点长,我来给大家解释下:
配置完成后,我们还需要给 swagger-ui 放行,否则 swagger-ui 相关的静态资源会被 Spring Security 拦截下来:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/swagger-ui.html")
.antMatchers("/webjars/**")
.antMatchers("/v2/**")
.antMatchers("/swagger-resources/**");
}
}
配置完成后,重启 user-server,浏览器输入 http://localhost:8081/swagger-ui.html,结果如下:
大家可以看到,页面中多了一个 Authorize 按钮,点击该按钮,输入 Bearer ${token}
,如下:
输入完成后,点击 Authorize 按钮,完成认证,接下来,user-server 中的各种接口就可以直接调用测试了。
上面这种方式比较通用,不仅仅适用于 OAuth2,也适用于其他一些自定义的 token 登录方式。
但是这种方式需要开发者先通过其他途径获取到 access_token,有的人会觉得这样有点麻烦,那么有没有更好的办法呢?请看方式二。
认证方式二就是直接在 Swagger 中填入认证信息,这样就不用从外部去获取 access_token 了,效果如下:
我们来看下这个怎么配置。
由于 swagger 去请求 /oauth/token
接口会跨域,所以我们首先要修改 auth-server ,使之支持跨域:
主要是两方面的修改,首先是配置 CorsFilter,允许跨域,如下:
@Configuration
public class GlobalCorsConfiguration {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
然后在 SecurityConfig 中开启跨域支持:
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers(HttpMethod.OPTIONS, "/oauth/**")
.and()
.csrf().disable().formLogin()
.and()
.cors();
}
}
经过这两步的配置,服务端的跨域支持就开启了。
接下来我们在 user-server 中修改关于 Docket bean 的定义:
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.javaboy.oauth2.res.controller"))
.paths(PathSelectors.any())
.build()
.securityContexts(Arrays.asList(securityContext()))
.securitySchemes(Arrays.asList(securityScheme()))
.apiInfo(new ApiInfoBuilder()
.description("接口文档的描述信息")
.title("微人事项目接口文档")
.contact(new Contact("javaboy","http://www.javaboy.org","[email protected]"))
.version("v1.0")
.license("Apache2.0")
.build());
}
private AuthorizationScope[] scopes() {
return new AuthorizationScope[]{
new AuthorizationScope("all", "all scope")
};
}
private SecurityScheme securityScheme() {
GrantType grant = new ResourceOwnerPasswordCredentialsGrant("http://localhost:8080/oauth/token");
return new OAuthBuilder().name("OAuth2")
.grantTypes(Arrays.asList(grant))
.scopes(Arrays.asList(scopes()))
.build();
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(new SecurityReference("OAuth2", scopes())))
.forPaths(PathSelectors.any())
.build();
}
}
这段配置跟前面的类似,主要是 SecurityScheme 不同。这里采用了 OAuthBuilder 来构建,构建时即得配置 token 的获取地址。
好了,配置完成,重启 auth-server 和 user-server 进行测试。测试效果就是松哥前面给出的图片,不再赘述。
这种方式最大的好处就是不用通过其他途径获取 access_token,直接在 swagger-ui 页面输入 password 模式的认证参数即可。非常方便,仅限于 OAuth2 模式。
好了,今天就和小伙伴们介绍了在 Swagger 请求中,如何修改请求头的问题,感兴趣的小伙伴可以下来试试哦~
本文案例下载地址:https://github.com/lenve/spring-security-samples
好啦,小伙伴们如果觉得有收获,记得点个在看鼓励下松哥哦~