SpringCloud的OAuth2.0例子 scio-cloud-oauth2

SCIO

https://github.com/rench/scio


scio-cloud-oauth2

https://github.com/rench/scio/tree/master/scio-cloud-oauth2

OAuth 2.0

OAuth 2.0定义

OAuth 2.0的角色定义

  • 资源所有者 Resource Owner
  • 资源服务器 Resource Server
  • 客户端 OAuth 2.0 Client
  • 授权服务器 Authorization Server

OAuth 2.0协议流程

 +--------+                               +---------------+
 |        |--(A)- Authorization Request ->|   Resource    |
 |        |                               |     Owner     |
 |        |<-(B)-- Authorization Grant ---|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(C)-- Authorization Grant -->| Authorization |
 | Client |                               |     Server    |
 |        |<-(D)----- Access Token -------|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(E)----- Access Token ------>|    Resource   |
 |        |                               |     Server    |
 |        |<-(F)--- Protected Resource ---|               |
 +--------+                               +---------------+
  • (A)客户端向从资源所有者请求授权。授权请求可以直接向资源所有者发起(如图所示),或者更可取的是通过作为中介的授权服务器间接发起。
  • (B)客户端收到授权许可,这是一个代表资源所有者的授权的凭据,使用本规范中定义的四种许可类型之一或 者使用扩展许可类型表示。授权许可类型取决于客户端请求授权所使用的方式以及授权服务器支持的类型。
  • (C)客户端与授权服务器进行身份认证并出示授权许可请求访问令牌。
  • (D)授权服务器验证客户端身份并验证授权许可,若有效则颁发访问令牌。
  • (E)客户端从资源服务器请求受保护资源并出示访问令牌进行身份验证。
  • (F)资源服务器验证访问令牌,若有效则满足该请求。

OAuth 2.0授权许可

  • 授权码 authorization_code
  • 隐式授权 implicit
  • 资源所有者密码凭据 password
  • 客户端凭据 client_credentials

OAuth 2.0令牌刷新

  • 令牌刷新 refresh_token
+--------+                                           +---------------+
|        |--(A)------- Authorization Grant --------->|               |
|        |                                           |               |
|        |<-(B)----------- Access Token -------------|               |
|        |               & Refresh Token             |               |
|        |                                           |               |
|        |                            +----------+   |               |
|        |--(C)---- Access Token ---->|          |   |               |
|        |                            |          |   |               |
|        |<-(D)- Protected Resource --| Resource |   | Authorization |
| Client |                            |  Server  |   |     Server    |
|        |--(E)---- Access Token ---->|          |   |               |
|        |                            |          |   |               |
|        |<-(F)- Invalid Token Error -|          |   |               |
|        |                            +----------+   |               |
|        |                                           |               |
|        |--(G)----------- Refresh Token ----------->|               |
|        |                                           |               |
|        |<-(H)----------- Access Token -------------|               |
+--------+           & Optional Refresh Token        +---------------+

OAuth 2.0常见例子

Spring Boot&Cloud OAuth 2.0

本文结合Spring Boot OAuth 2和Spring Cloud OAuth 2进行示例,所有的资源和用户使用的是内存模拟数据,如在使用中,请替换为持久化存储。

解释

资源所有者 Resource Owner

资源所有者即需要用户授权的用户,所有的资源请求,都需要在资源所有者授权后才可以进行资源授权。

资源服务器 Resource Server

资源服务器即资源所有者的资源存储的地方,一个客户端向资源服务器获取资源,必须要提供合法的授权token, 资源服务器根据token向授权服务器进行授权认证,认证合法后,判断该客户端有权读取指定的资源。

客户端 OAuth 2.0 Client

向资源服务器发起资源请求的客户端。

授权服务器 Authorization Server

客户端引导用户认证后,授权服务器会颁发合法的token给客户端,同时授权服务器也会提供token验证的功能等。

原理

AuthorizationServer

  1. Spring中的AuthorizationServer的原理是基于spring-security之上,AuthorizationServer进行Filter授权认证的必要条件是进行过spring-securityAuthentication
  2. Spring中的AuthorizationServer可以支持多种客户端方式配置ClientDetailsServiceConfigurer,同时支持token的增强TokenEnhancer
  3. AuthorizationServer需要进行用户token验证,它也是一个ResourceServer

ResourceServer

  1. ResourceServer的原理同样是基于WebSecurity配置的。在ResourceServerConfigurerAdapter中指定需要被保护的资源路径,WebSecurity会拦截到指定的请求,进行OAuth2.0的授权校验,授权成功后,填充Authentication

使用

AuthorizationServer

https://github.com/rench/scio/tree/master/scio-cloud-oauth2

  1. AuthorizationServer的配置,需要进入依赖org.springframework.cloud:spring-cloud-starter-oauth2,同时开启@EnableAuthorizationServer,并继承AuthorizationServerConfigurerAdapter进行配置.
@Configuration
  @EnableAuthorizationServer
  @Order(Ordered.LOWEST_PRECEDENCE)
  public static class ScioAuthorizationServerConfiguration
      extends AuthorizationServerConfigurerAdapter {

    @Autowired private AuthenticationManager authenticationManager;
    @Autowired private TokenStore tokenStore;

    @Autowired(required = false)
    private JwtAccessTokenConverter converter;

    @Autowired private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
      // for custom endpoints
      endpoints.pathMapping("/oauth/token", "/oauth/token");
      endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager);
      endpoints.userDetailsService(userDetailsService);
      TokenEnhancerChain chain = new TokenEnhancerChain();
      // for custom token
      TokenEnhancer enhancer = new ScioOauth2TokenEnhancer();
      List<TokenEnhancer> list = new ArrayList<>(2);
      list.add(enhancer);
      if (converter != null) {
        list.add(converter);
        // add convert
        endpoints.accessTokenConverter(converter);
      }
      chain.setTokenEnhancers(list);

      endpoints.tokenEnhancer(chain);
      endpoints.authorizationCodeServices(new ScioOauth2CodeServices());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
      security.allowFormAuthenticationForClients();
      security.tokenKeyAccess("permitAll()");
      security.checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      // refresh_token only allowed with GrantType:authorization_code,password
      clients
          .inMemory()
          .withClient("client1")
          .scopes("read", "write")
          .secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
          .authorizedGrantTypes(
              "authorization_code", "refresh_token", "implicit", "password", "client_credentials")
          .redirectUris("https://www.xuankejia.cn")
          .and()
          .withClient("scio-cloud-oauth2-client")
          .scopes("read", "write")
          .secret(
              PasswordEncoderFactories.createDelegatingPasswordEncoder()
                  .encode("48f854f3cee94afba7ae95a6c5ce9116"))
          .authorizedGrantTypes("client_credentials");
    }
  }
  1. Resource Onwer Authentication需要配置WebSecurityConfigurerAdapter,来在授权服务器之前获取授权。
@Configuration
  @EnableWebSecurity
  @EnableGlobalMethodSecurity(prePostEnabled = true)
  @Order(1)
  public static class ScioWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
      DelegatingPasswordEncoder delegate =
          (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
      delegate.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance());
      return delegate;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      // auth.inMemoryAuthentication().withUser("mp").password("{noop}123456").roles("USER");
      auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      // Define which links require user login privileges
      http.requestMatchers()
          .antMatchers("/login", "/oauth/authorize")
          .and()
          .authorizeRequests()
          .anyRequest()
          .authenticated()
          .and()
          .formLogin()
          .permitAll();
      http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
      return super.authenticationManager();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
    }
  }

授权用户数据我们先进行内存模拟。

@Service
  public static class ScioUserDetailsService implements UserDetailsService {
    /** mock users */
    private Map<String, String> users = Maps.newHashMap();

    public ScioUserDetailsService() {
      users.put("mp1", "mp1");
      users.put("mp2", "mp2");
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      if (users.containsKey(username)) {
        String noopPwd = users.get(username);
        User u = new User(username, noopPwd, Arrays.asList(new SimpleGrantedAuthority("USER")));
        return u;
      } else {
        throw new UsernameNotFoundException("user not found");
      }
    }
  }
  1. 扩展授权服务器的code生成
public class ScioOauth2CodeServices implements AuthorizationCodeServices {
  // generator 4 char
  private RandomValueStringGenerator generator = new RandomValueStringGenerator(4);
  protected final ConcurrentHashMap<String, OAuth2Authentication> authorizationCodeStore =
      new ConcurrentHashMap<String, OAuth2Authentication>();

  @Override
  public String createAuthorizationCode(OAuth2Authentication authentication) {
    String code = generator.generate();
    authorizationCodeStore.put(code, authentication);

    return code;
  }

  @Override
  public OAuth2Authentication consumeAuthorizationCode(String code) throws InvalidGrantException {
    OAuth2Authentication auth = authorizationCodeStore.remove(code);
    if (auth == null) {
      throw new InvalidGrantException("Invalid authorization code: " + code);
    }
    return auth;
  }
}
  1. 扩展授权服务器的token生成
public class ScioOauth2TokenEnhancer implements TokenEnhancer {

  @Override
  public OAuth2AccessToken enhance(
      OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    if (accessToken instanceof DefaultOAuth2AccessToken) {
      DefaultOAuth2AccessToken enhancerToken = ((DefaultOAuth2AccessToken) accessToken);
      enhancerToken.setValue(getNewScioToken());
      OAuth2RefreshToken refreshToken = enhancerToken.getRefreshToken();
      if (refreshToken instanceof DefaultOAuth2RefreshToken) {
        enhancerToken.setRefreshToken(new DefaultOAuth2RefreshToken(getNewScioToken()));
      }
      Map<String, Object> additionalInformation = new HashMap<String, Object>();
      Map<String, Object> ext = new HashMap<>();
      Object principal = authentication.getPrincipal();
      if (principal instanceof User) {
        User user = (User) principal;
        ext.put("username", user.getUsername());
      } else {
        ext.put("username", principal);
      }
      ext.put("client_id", authentication.getOAuth2Request().getClientId());
      additionalInformation.put("ext", ext);
      enhancerToken.setAdditionalInformation(additionalInformation);
    }
    return accessToken;
  }

  private String getNewScioToken() {
    return "scio@" + UUID.randomUUID().toString().replace("-", "");
  }
}
  1. 进行授权操作
  • authorization_code

该模式下,需要用户在登录界面进行授权后,选择授权的scope。本例子送用户登录登录服务和授权服务在同一服务中,如果需要分离,可以进行授权服务器和登录服务器进行session共享。

1. 访问 http://localhost:8003/oauth/authorize?response_type=code&client_id=client1&redirect_uri=https://www.xuankejia.cn
2. 在登录页面输入模拟的用户名和密码
3. 选择授权的scope(read,write)
4. curl http://localhost:8003/oauth/token
        -dgrant_type=authorization_code
        -dclient_id=client1
        -dclient_secret=123456
        -dcode=ASp8Zb(替换为跳转的url中的token)
        -dredirect_uri=https://www.xuankejia.cn
5. 获取token信息
  • refesh_token

在上一步拿到的token中包含的refresh_token参数,在token即将过期之前,可以使用refresh_token进行token刷新,获取新的token,有效期重新计算。

1. curl http://localhost:8003/oauth/token
       -dgrant_type=refresh_token
       -dclient_id=client1
       -dclient_secret=123456
       -drefresh_token=16ea4250-884f-4ca2-ac72-1c3d44550de0
2. 获取token信息
  • password

该模式下,不需要用户授权,只需要提供client的用户名和密码和用户的账号和密码,即可获取授权,该模式主要在获取到用户的账号和密码后。

1. curl http://localhost:8003/oauth/token
      -dgrant_type=password
      -dclient_id=client1
      -dclient_secret=123456
      -dusername=mp1
      -dpassword=mp1
2. 获取token信息
  • client_credentials

该模式下,不需要用户授权,只需要提供client的用户名和密码,即可获取授权,该模式主要用户获取针对client提供的授权服务以及身份认证。

1. curl http://localhost:8003/oauth/token
       -dgrant_type=client_credentials
       -dclient_id=client1
       -dclient_secret=123456
2. 获取token信息

ResourceServer

https://github.com/rench/scio/tree/master/scio-cloud-oauth2-resource

  1. application.yml
security:
   oauth2:
      resource:
         userInfoUri: http://localhost:8003/userinfo
         token-info-uri: http://localhost:8003/oauth/check_token
         preferTokenInfo: false
  1. ScioOauth2ResourceServerConfig
@Configuration
@EnableResourceServer
public class ScioOauth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(HttpSecurity http) throws Exception {
    // define which resource will be protected by oauth2
    http.authorizeRequests().antMatchers("/userinfo").authenticated();
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    // UserInfoTokenServices to fetch user info from oauth2 server
  }
}
  1. UserInfoRestController
@RestController
public class UserInfoRestController {
  /**
   * access with oauth2 token to get access token userinfo
   *
   * @param principal
   * @return
   */
  @RequestMapping("/userinfo")
  public String userinfo(Principal principal) {
    String username = null;
    if (principal instanceof OAuth2Authentication) {
      username = ((OAuth2Authentication) principal).getName();
    }
    return username;
  }
}

Oauth2Client With Zuul

https://github.com/rench/scio/tree/master/scio-cloud-oauth2-client

  1. Oauth2ClientConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(-1)
public class Oauth2ClientConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf()
        .disable() //
        .httpBasic()
        .disable() //
        .formLogin()
        .disable()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        .antMatchers("/favicon.ico")
        .permitAll();
  }

  @Bean
  public OAuth2RestTemplate loadBalancedOauth2RestTemplate(
      OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
    ClientCredentialsResourceDetails detail = new ClientCredentialsResourceDetails();
    detail.setClientId(resource.getClientId());
    detail.setClientSecret(resource.getClientSecret());
    detail.setAccessTokenUri(resource.getAccessTokenUri());
    // The default context will generate one for each session. just use a new one
    return new OAuth2RestTemplate(detail, new DefaultOAuth2ClientContext());
  }

}
  1. OAuth2ZuulFilter
@Component
@Configuration
@ConfigurationProperties(prefix = "security.oauth2")
public class OAuth2ZuulFilter extends ZuulFilter {
  private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
  private static final String TOKEN_TYPE = "TOKEN_TYPE";

  private List<String> zuulRoutes = new ArrayList<>();

  private OAuth2RestOperations restTemplate;
  @Autowired RouteLocator locator;

  @Autowired
  public void setRestTemplate(OAuth2RestOperations restTemplate) {
    // List<Route> list = locator.getRoutes();
    this.restTemplate = restTemplate;
  }

  @Override
  public int filterOrder() {
    return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1;
  }

  @Override
  public String filterType() {
    return "pre";
  }

  @Override
  public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    if (ctx.containsKey("proxy")) {
      String id = (String) ctx.get("proxy");
      if (!zuulRoutes.contains(id)) {
        return false;
      } else {
        ctx.set(TOKEN_TYPE, "Bearer");
        return true;
      }
    }
    return false;
  }

  @Override
  public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ctx.addZuulRequestHeader("authorization", ctx.get(TOKEN_TYPE) + " " + getAccessToken(ctx));
    return null;
  }

  private String getAccessToken(RequestContext ctx) {
    String value = (String) ctx.get(ACCESS_TOKEN);
    if (restTemplate != null) {
      try {
        value = restTemplate.getAccessToken().getValue();
      } catch (Exception e) {
        throw new BadCredentialsException("Cannot obtain valid access token");
      }
    }
    return value;
  }

  /** @param zuulRoutes the zuulRoutes to set */
  public void setZuulRoutes(List<String> zuulRoutes) {
    this.zuulRoutes = zuulRoutes;
  }
}
  1. application.yml
security:
   basic:
      enabled: false
   oauth2:
      resource:
         userInfoUri: http://localhost:8003/userinfo
         token-info-uri: http://localhost:8003/oauth/check_token
         preferTokenInfo: false
      client:
         clientId: scio-cloud-oauth2-client
         clientSecret: 48f854f3cee94afba7ae95a6c5ce9116
         accessTokenUri: http://localhost:8003/oauth/token
         userAuthorizationUri: http://localhost:8003/oauth/authorize
         clientAuthenticationScheme: form
         authorized-grant-types: client_credentials
         grant-type: client_credentials
      zuul-routes:
         - scio-cloud-oauth2-resource
         
zuul:
  ignoredServices: '*'
  routes:
    scio-cloud-oauth2-resource:
      path: /res/**
      url: http://localhost:8004
      stripPrefix: true
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容