SpringCloudAlibabaマイクロサービスコンバット18th-Oauth2.0カスタム認証モード

概要概要

oauth2認証システムには4つの認証モードがあることは誰もが知っています。

  • 認証コードモード(認証コード)

  • 簡易モード(暗黙的)

  • クライアントの資格情報

  • パスワードモード(パスワード)

では、次のような携帯電話番号やSMS確認コードに基づくログインなどのカスタム認証モードを追加するにはどうすればよいですか?

ライセンスモデルをカスタマイズするには、認証入力org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken方法であるoauth2.0での認証プロセス全体を理解する必要があります。

@RequestMapping(
        value = {"/oauth/token"},
        method = {RequestMethod.POST}
    )
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
   ...
}

ソースコードを読み取ると、コア認証ロジックコード(パスワードモード)の実行順序を分類できます。

コアソースコードの解釈

  • TokenEndpoint#postAccessToken(...) 主入口

OAuth2AccessToken token = 
getTokenGranter().grant(tokenRequest.getGrantType(), 
tokenRequest);
  • CompositeTokenGranter#grant(String grantType,TokenRequest tokenRequest ) すべてのTokenGrantersからの認証タイプに従って特定のTokenGranterを見つける責任があります

public class CompositeTokenGranter implements TokenGranter {
    private final List<TokenGranter> tokenGranters;
 ...
 public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
  for (TokenGranter granter : tokenGranters) {
   OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
   if (grant!=null) {
    return grant;
   }
  }
  return null;
 }
 ...
}

では、tokenGrantersはどこから来たのですか?答えは、oauth2認証サーバーのエンドポイント構成クラスです。AuthorizationServerEndpointsConfigurer

public final class AuthorizationServerEndpointsConfigurer {
 ...
 private TokenGranter tokenGranter;
 public TokenGranter getTokenGranter() {
  return tokenGranter();
 }

 //默认的四种授权模式+Refresh令牌模式
 private List<TokenGranter> getDefaultTokenGranters() {
  ClientDetailsService clientDetails = clientDetailsService();
  AuthorizationServerTokenServices tokenServices = tokenServices();
  AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
  OAuth2RequestFactory requestFactory = requestFactory();

  List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
  tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
    requestFactory));
  tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
  ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
  tokenGranters.add(implicit);
  tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
  if (authenticationManager != null) {
   tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
     clientDetails, requestFactory));
  }
  return tokenGranters;
 }

 private TokenGranter tokenGranter() {
  if (tokenGranter == null) {
   tokenGranter = new TokenGranter() {
    private CompositeTokenGranter delegate;

    @Override
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
     if (delegate == null) {
      delegate = new CompositeTokenGranter(getDefaultTokenGranters());
     }
     return delegate.grant(grantType, tokenRequest);
    }
   };
  }
  return tokenGranter;
 }
 ...
}

Springは、デフォルトの4つの認証モードと更新トークンモードの構成をコードにすでに書き込んでいることがわかります。では、Springはどのようにしてカスタム認証モードを認識できるのでしょうか。

構成クラスを介してTokenGranterをオーバーライドし、カスタム認証モードを挿入することができます。

  • ProviderManager#authenticate(Authentication authentication)このクラスは、認証の実装ロジックとプロセスを提供します。認証のためにすべてのAuthenticationProvidersから特定のプロバイダーを見つける責任があります。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
  InitializingBean {
 ...
 public Authentication authenticate(Authentication authentication)
   throws AuthenticationException {
  Class<? extends Authentication> toTest = authentication.getClass();
  AuthenticationException lastException = null;
  AuthenticationException parentException = null;
  Authentication result = null;
  Authentication parentResult = null;
  boolean debug = logger.isDebugEnabled();
  //遍历所有的providers使用supports方法判断该provider是否支持当前的认证类型
  for (AuthenticationProvider provider : getProviders()) {
   if (!provider.supports(toTest)) {
    continue;
   }

   try {
   //找到具体的provider进行认证
    result = provider.authenticate(authentication);
    if (result != null) {
     copyDetails(authentication, result);
     break;
    }
   }
   catch (AccountStatusException | InternalAuthenticationServiceException e) {
    prepareException(e, authentication);
    throw e;
   } catch (AuthenticationException e) {
    lastException = e;
   }
  }
  throw lastException;
 }
 ...
}

コードの実装(コアコード)

image.png

携帯電話番号を使用してログインする場合は、最初にフォームに正しい携帯電話番号を入力し、バックエンドに確認コードの取得を要求してください。(現時点では、バックグラウンドサービスは通常、携帯電話番号を確認コードに関連付け、有効期間を短く設定します)

携帯電話が確認コードを取得したら、フォームに入力してログインします。バックエンドフレームワークは、認証のために携帯電話番号をユーザーに関連付けます。

SMS検証には、携帯電話番号とSMS検証コードの2つの基本的なフォームデータが必要です。

この記事では、フォームログイン方式は実装していませんが、認証にはpostman方式を使用しています。上の写真を使用するのは、SMS認証プロセスの印象を全員に与えるためだけです。

SmsCodeAuthenticationToken

/**
 * <p>
 * <code>SmsAuthenticationToken</code>
 * </p>
 * Description:
 * 实现手机号登录,参考org.springframework.security.authentication.UsernamePasswordAuthenticationToken
 * @author javadaily
 * @date 2020/7/13 8:44
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;

    /**
     * 账号主体信息,手机号验证码登录体系中代表 手机号码
     */
    private final Object principal;


    /**
     * 构建未授权的 SmsCodeAuthenticationToken
     * @param mobile 手机号码
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }


    /**
     * 构建已经授权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities){
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }


    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }


    @Override
    public void setAuthenticated(boolean isAuthenticated) {
        if(isAuthenticated){
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }else{
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

SmsCodeAuthenticationProvider

/**
 * Description:
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 * @author javadaily
 * @date 2020/7/13 13:07
 */
@Log4j2
public class SmsCodeAuthenticationProvider implements AuthenticationProvider{

    private IUserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
        userService = SpringContextHolder.getBean(IUserService.class);

        String mobile = (String) smsCodeAuthenticationToken.getPrincipal();

        //校验手机号验证码
        checkSmsCode(mobile);

        User user = userService.getUserByMobile(mobile);
        if(null == user){
            throw new BadCredentialsException("Invalid mobile!");
        }

        //授权通过
        UserDetails userDetails = buildUserDetails(user);
        return new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
    }

    /**
     * 构建用户认证信息
     * @param user 用户对象
     * @return UserDetails
     */
    private UserDetails buildUserDetails(User user) {
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                AuthorityUtils.createAuthorityList("ADMIN")) ;
    }

    /**
     * 校验手机号与验证码的绑定关系是否正确
     *  todo 需要根据业务逻辑自行处理
     * @author javadaily
     * @date 2020/7/23 17:31
     * @param mobile 手机号码
     */
    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //获取验证码
        String smsCode = request.getParameter("smsCode");
        if(StringUtils.isEmpty(smsCode) || !"666666".equals(smsCode)){
            throw new BadCredentialsException("Incorrect sms code,please check !");
        }
        //todo  手机号与验证码是否匹配
    }

    /**
     * ProviderManager 选择具体Provider时根据此方法判断
     * 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

検証メッセージ認証コードモードの実装クラスは、AuthenticationProviderを実装する必要があり、supportsメソッドによって、特定の認証ProviderManager実装クラスになるように選択されます。携帯電話番号とショートメッセージの関連付け関係は、ご自身のビジネスシナリオに応じて実現する必要があるため、ここに直接記載します。

構成クラスSmsCodeSecurityConfig

@Component
public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    /**
     * 短信验证码配置器
     *  所有的配置都可以移步到WebSecurityConfig
     *  builder.authenticationProvider() 相当于 auth.authenticationProvider();
     *  使用外部配置必须要在WebSecurityConfig中用http.apply(smsCodeSecurityConfig)将配置注入进去
     * @param builder
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity builder) throws Exception {
        //注入SmsCodeAuthenticationProvider
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        builder.authenticationProvider(smsCodeAuthenticationProvider);
    }
}

このクラスは主にSmsCodeAuthenticationProviderのインジェクションを実装します。それ以外の場合、ProviderManagerはSmsCodeAuthenticationProviderを選択できません。

SmsCodeTokenGranter

/**
 * 扩展认证模式
 * @author javadaily
 * @date 2020/7/14 8:31
 */
public class SmsCodeTokenGranter extends AbstractTokenGranter{

    private static final String GRANT_TYPE = "sms_code";

    private final AuthenticationManager authenticationManager;

    public SmsCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected SmsCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String mobile = parameters.get("mobile");

        Authentication userAuth = new SmsCodeAuthenticationToken(mobile);

        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException ex) {
            throw new InvalidGrantException(ex.getMessage());
        } catch (BadCredentialsException ex) {
            throw new InvalidGrantException(ex.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
        }
    }
}

AbstractTokenGranter拡張認証モードsms_codeを継承し、Springに追加して、grantTypeで選択する必要があります。

構成クラスTokenGranterConfig

ロジックの基礎となるカスタム認証は前の手順で実現されていますorg.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters()メインのリファレンス実装であるSpringをSMS認証モードに追加する必要があります

/**
 *参考实现:org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters()
 * @author javadaily
 * @date 2020/7/14 8:38
 */
@Configuration
public class TokenGranterConfig {
    @Autowired
    private ClientDetailsService clientDetailsService;

    private TokenGranter tokenGranter;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    TokenEnhancer tokenEnhancer;

    @Autowired
    private AuthenticationManager authenticationManager;

    private AuthorizationServerTokenServices tokenServices;

    private boolean reuseRefreshToken = true;

    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public TokenGranter tokenGranter(){
        if(null == tokenGranter){
            tokenGranter = new TokenGranter() {
                private CompositeTokenGranter delegate;

                @Override
                public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
                    if(delegate == null){
                        delegate = new CompositeTokenGranter(getDefaultTokenGranters());
                    }
                    return delegate.grant(grantType,tokenRequest);
                }
            };
        }
        return tokenGranter;
    }

    private List<TokenGranter> getDefaultTokenGranters() {
        AuthorizationServerTokenServices tokenServices = tokenServices();
        AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
        OAuth2RequestFactory requestFactory = requestFactory();

        List<TokenGranter> tokenGranters = new ArrayList();
        //授权码模式
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
        //refresh模式
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
        //简化模式
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory);
        tokenGranters.add(implicit);
        //客户端模式
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));

        if (authenticationManager != null) {
            //密码模式
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
            //短信验证码模式
            tokenGranters.add(new SmsCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
        }

        return tokenGranters;
    }

    private AuthorizationServerTokenServices tokenServices() {
        if (tokenServices != null) {
            return tokenServices;
        }
        this.tokenServices = createDefaultTokenServices();
        return tokenServices;
    }

    private AuthorizationServerTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(reuseRefreshToken);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancer);
        addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

    /**
     * 添加预身份验证
     * @param tokenServices
     * @param userDetailsService
     */
    private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
        if (userDetailsService != null) {
            PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
            provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService));
            tokenServices.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider>asList(provider)));
        }
    }

    /**
     * OAuth2RequestFactory的默认实现,它初始化参数映射中的字段,
     * 验证授权类型(grant_type)和范围(scope),并使用客户端的默认值填充范围(scope)(如果缺少这些值)。
     */
    private OAuth2RequestFactory requestFactory() {
        return new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    /**
     * 授权码API
     * @return
     */
    private AuthorizationCodeServices authorizationCodeServices() {
        if (this.authorizationCodeServices == null) {
            this.authorizationCodeServices = new InMemoryAuthorizationCodeServices();
        }
        return this.authorizationCodeServices;
    }
}

認証サーバーの構成を変更するAuthorizationServerConfig

上記では、TokenGranterConfigがAuthorizationServerTokenServicesとして作成されているため、tokenServices関数AuthorizationServerConfigを削除して、configure(AuthorizationServerEndpointsConfigurer endpoints)埋め込みの過程で次のようにするtokenGranterことができます。

@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 @Autowired
    private  TokenGranter tokenGranter;
 ...
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenGranter(tokenGranter);
    }
 ...
}

テスト

  • 通常のテストのデバッグモードを通じて、SmsCodeTokenGranterがSpringに追加され、jwtトークンが正常に返されることがわかります。

  • 確認のために間違った電話番号を入力してください

  • 認証に間違ったSMS確認コードを入力してください

この記事は、SpringCloud alibabaの実際の戦闘シリーズの20番目の記事です。以前の記事に興味がある場合は 、http: //javadaily.cn/tags/SpringCloudで確認できます 

この記事がお役に立てば、いいね、転送、コメントの3つのリンクを忘れずに教えてください。

またね!

白い売春婦に匹敵するお気に入り 親指を立てるの は真実です!

 

これは皆へのささやかな贈り物です。公式アカウントに従って、次のコードを入力してください。Baiduネットワークディスクアドレスを取得できます。ルーチンはありません。

001:「プログラマーにとって必読の本」
002:「中小規模のインターネット企業向けのバックエンドサービスアーキテクチャと運用および保守アーキテクチャをゼロから構築する」
003:「インターネット企業向けの高同時実行ソリューション」
004:「インターネットアーキテクチャ教育ビデオ "
006:
注文システムのSpringBoot実現" 007: "SpringSecurity実際の戦闘ビデオ"
008: "Hadoop実際の戦闘教育ビデオ"
009: "Tencent 2019 Techo Developer Conference PPT"

010:WeChat交換グループ

 

 

おすすめ

転載: blog.csdn.net/jianzhang11/article/details/107650296