Spring Oauth授权码方式与zuul整合
目前流行分布式架构,通常会把鉴权做成一个独立的服务,给其他服务使用,有时候甚至给第三方使用。此时采用oauth授权再合适不过了。而Spring全家桶自然也考虑到了这一点,提供了oauth包。本身用spring的oauth做成独立的服务没有什么问题,而微服务中,注重高可用,那就意味着我们必定不止部署一个服务,而多个服务之间的负载必然要用到zuul(毕竟这也是spring的亲儿子)。
通常给第三方授权都是用授权码的方式进行,比如QQ或者微信。授权码方式意味着用户输入用户名和密码的过程在我们的系统上,这种方式更加安全可靠。
Spring Oauth的认证过程大概是下面这样
- 先发送一个/oauth/authorize请求,系统先判断请求中的clientId是否正确,正确的话再判断用户是否已经登录,是则继续,不是则跳转到登录页面。
- 跳转到登录页面之后,用户输入用户名和密码,校验正确再重定向到/oauth/authorize。
- /oauth/authorize判断用户已经登录跳转(转发)到授权页面,让用户选择授权或者拒绝,还可以勾选哪些资料授权。
- 最后跳转到一开始请求中包含的授权成功跳转的页面,spring oauth可以直接配置,不需要在请求中带。
有没有发现上面的过程中,完成登录页填写后要做页面重定向,而这个过程是在oauth授权服务里面做的,这会导致一个问题,因为请求是经过zuul转发的,而重定向发生在服务中,会导致跳转之后的地址变成了服务地址,而非zuul的地址了。比如下面这个例子。
http://localhost:9080是zuul,http://localhost:9081是auth,通过zuul访问auth,要http://localhost:9080/oa/auth,我们服务固定前缀/oa/auth。
通过zuul授权过程如下:
1)一开始的授权地址:http://localhost:9080/oa/auth/oauth/authorize?scope=all&response_type=code&client_id=oa
2)没登录的话跳转到登录页面,这边我还可以指定下,http://localhost:9080/oa/auth/login
3)登录完成之后重定向到开始的授权页面,就变成了http://localhost:9080/oauth/authorize?scope=all&response_type=code&client_id=oa,前面的oa/auth没了。
要解决这个问题,需要在zuul里面对地址进行处理,因为spring自带的地址都是基于/oauth开头的,可以在filter里面针对/oauth开头的地址也转发给oauth服务,然后再做些简单的处理。
以下为例子,我们的oauth服务名为provider-oa-auth
Zuul的配置
routes:
oa-oauth-route:
path: /oauth/**
sensitiveHeaders:
service-id: provider-oa-auth
ribbon:
eager-load:
enabled: true
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: false
ConnectTimeout: 10000
ReadTimeout: 10000
oa-auth-route:
path: /oa/auth/**
sensitiveHeaders:
service-id: provider-oa-auth
ribbon:
eager-load:
enabled: true
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: false
ConnectTimeout: 10000
ReadTimeout: 10000
Zuul的filter配置
@Component
public class OauthFilter extends AbstractFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI();
// 此处针对不包含/oa/auth前缀,但包含了/oauth的路径
if(StringUtils.isNotBlank(url) && url.contains("/oauth") && !url.contains("/oa/auth")) {
url = url.substring(url.lastIndexOf("/oauth"));
ctx.put(FilterConstants.REQUEST_URI_KEY, url);
}
return null;
}
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 6;
}
}
provider-oa-auth的oauth配置
@Configuration
public class OaAuthorizationConfig {
@Configuration
@EnableAuthorizationServer
public class OaAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory connectionFactory;
@Autowired
private OaUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public RedisTokenStore tokenStore() {
RedisTokenStore redisTokenStore = new RedisTokenStore(connectionFactory);
return redisTokenStore;
}
/**
* 配置授权服务器的安全,意味着实际上是/oauth/token端点。 /oauth/authorize端点也应该是安全的
* 默认的设置覆盖到了绝大多数需求,所以一般情况下你不需要做任何事情。
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
/**
* 配置ClientDetailsService
* 注意,除非你在下面的configure(AuthorizationServerEndpointsConfigurer)中指定了一个AuthenticationManager,否则密码授权方式不可用。
* 至少配置一个client,否则服务器将不会启动。
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
}
/**
* 该方法是用来配置Authorization Server endpoints的一些非安全特性的,比如token存储、token自定义、授权类型等等的
* 默认情况下,你不需要做任何事情,除非你需要密码授权,那么在这种情况下你需要提供一个AuthenticationManager
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService).tokenStore(tokenStore())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
}
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated();
}
}
provider-oa-auth的security配置
@Configuration
@EnableWebSecurity
@Order(10)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private OaUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/user/**", "/oauth/**", "/actuator/**", "/v2/api-docs").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/oauth/login")
.loginProcessingUrl("/oauth/form")
.failureUrl("/oauth/login?error")
.permitAll()
.and()
.sessionManagement()
.invalidSessionUrl("/oauth/login")
.maximumSessions(1)
.expiredSessionStrategy(new SessionInformationExpiredStrategyImpl());
}
/*忽略静态资源*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/resources/static/**");
}
}
zuul和oauth源代码下载
链接: https://pan.baidu.com/s/15narhBEG3Uk77kyTRf_vJw 提取码: np6m