自定义的SpringSecurityOAuth2(获取token)

512 阅读5分钟

需求又来了

需求又又又来了

  • 需要根据username和appId来确定一个用户的Uid(全局id)和AppUserId(App下的唯一id)
  • 根据手机号和验证码登录

熟悉SpringSecurity的人都知道,其中一个最最最核心的方法。继承UserDetailService实现的loadUserByUserName方法。

但是问题来了,这个方法只支持一个传参,username。

public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException{


}

想法1:拼接参数,然后解析。

类似

String newUserName = username+"&&"+Appid

这样在loadUserByUserName方法里面解析到这两个参数。请求数据库返回User对象。

存在的问题:

  1. 不是优雅的解决方法。
  2. 如果要使用手机号和验证码登录的时候,又得判断拼接。
  1. 存在一定的bug。之前尝试过一版,就是这么传参。我选定的拼接符是:"□#□"。然后正常运行了半个月,在后来在对接浏览器和Android 的App的时候,一方不兼容这个符号。会自动吃掉"□"。。。然后笔者和前端小姐姐排查了很久才发现。最后痛定思痛,决定不能采用这种偏方。

想法2: 自定义

首先UserService

public interface UserService {
  /**
   * 通过账号和密码获取用户数据
   *
   * @param countryCode
   * @param username
   * @param appId
   * @param password
   * @return
   */
  OAuthUserDetail getUserByUserNamePassword(
          String countryCode, String username, String password, String appId);

  /**
   * 通过手机号和验证码获取用户数据
   *
   * @param countryCode
   * @param phone
   * @param appId
   * @param VerificationCode
   * @return
   */
  OAuthUserDetail getUserByPhoneAndVerificationCode(
          String countryCode, String phone, String appId, String VerificationCode);

  /**
   * 通过uid 和appId获取用户数据
   *
   * @param uid
   * @param appId
   * @return
   */
  OAuthUserDetail getUserOAuthByUid(String uid, String appId);
}

完全可以根据不同的内容来直接调用不同的方法,而不是根据一个传参来拼接。

\

结合源码看看

  1. TokenEndpoint

之前的不需要管,OAuth2 验证client_id和client_secret 是否合法。

重点是这个方法。

    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

OAuth2AccessToken:这个是啥呢?点进去

很明显,就是token。

这个是根据grantType 来通过不同的方法来获取token

这个TokenGranter 令牌授予者 就是不同的 获取token的方法。

我们看看OAuth2自己实现的TokenGranter ,通过Password来判断的。

protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) 

OAuth2Authentication: 这个又是啥呢???

熟悉SpringSecurity的就会觉得很眼熟,就是SpringSecurity的认证核心,里面有账户和凭证,角色信息等等。Principal

等于说这个方法的就是,我们获取了账户和密码,颁发一个SpringSecurityOAuth2认可的凭证(OAuth2Authentication)给SpringSecurityOAuth2,其它就可以用它自己的东西了,也就是我们真正要自己实现的东西。

\

正常的逻辑的话,还需要写一个自己的认证Token.这里我偷了下懒,用了系统自带的一个PreAuthenticatedAuthenticationToken, 预认证的认证令牌

先上代码:

1 CustomTokenGranter 自定义的TokenGranter 模板类

这里笔者使用了模板模式,方便以后的扩展。

@Getter
public abstract class CustomTokenGranter extends AbstractTokenGranter {
  public CustomTokenGranter(
      AuthorizationServerTokenServices tokenServices,
      ClientDetailsService clientDetailsService,
      OAuth2RequestFactory requestFactory,
      String grantType) {
    super(tokenServices, clientDetailsService, requestFactory, grantType);
  }

  @Override
  protected OAuth2Authentication getOAuth2Authentication(
      ClientDetails client, TokenRequest tokenRequest) {
    Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
    OAuthUserDetail details = getUserDetails(parameters);
    parameters.remove("password");
    Authentication userAuth =
        new PreAuthenticatedAuthenticationToken(details, null, details.getAuthorities());
    OAuth2Request storedOAuth2Request =
        getRequestFactory().createOAuth2Request(client, tokenRequest);
    return new OAuth2Authentication(storedOAuth2Request, userAuth);
  }
	// 这个方法抽象出去,不同的获取User方法
  protected abstract OAuthUserDetail getUserDetails(Map<String, String> parameters);
}

2 CustomAccountPasswordTokenGranter 自定义的password登录 TokenGranter

public class CustomAccountPasswordTokenGranter extends CustomTokenGranter {
  private static final String GRANT_TYPE = CustomConstant.PASSWORD_GRANT_TYPE;
  // 这个就是自定义的UserService,没有使用它所规定的UserDetailService
  private UserService service;

  public CustomAccountPasswordTokenGranter(
      AuthorizationServerTokenServices tokenServices,
      ClientDetailsService clientDetailsService,
      OAuth2RequestFactory requestFactory,
      UserService service) {
    super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    this.service = service;
  }

  @Override
  protected OAuthUserDetail getUserDetails(Map<String, String> parameters) {
    String countryCode = MapUtil.getStr(parameters, "countryCode");
    String appId = MapUtil.getStr(parameters, "client_id");
    String username = MapUtil.getStr(parameters, "username");
    String password = MapUtil.getStr(parameters, "password");

    // 参数校验
    if (StrUtil.isBlank(username)) {
      throw new ServiceException("缺少请求参数:username");
    }
    if (StrUtil.isBlank(password)) {
      throw new ServiceException("缺少请求参数:password");
    }
    if (StrUtil.isBlank(countryCode)) {
      throw new ServiceException("缺少请求参数:countryCode");
    }
    return service.getUserByUserNamePassword(countryCode, username, password, appId);
  }
}

3 CustomPhoneSmsTokenGranter 自定义的手机号和验证码的登录

public class CustomPhoneSmsTokenGranter extends CustomTokenGranter {
  private UserService service;
  private static final String GRANT_TYPE = CustomConstant.PHONE_GRANT_TYPE;

  public CustomPhoneSmsTokenGranter(
      AuthorizationServerTokenServices tokenServices,
      ClientDetailsService clientDetailsService,
      OAuth2RequestFactory requestFactory,
      UserService service) {
    super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    this.service = service;
  }

  @Override
  protected OAuthUserDetail getUserDetails(Map<String, String> parameters) {
    String countryCode = MapUtil.getStr(parameters, "countryCode");
    String phone = MapUtil.getStr(parameters, "phone");
    String appId = MapUtil.getStr(parameters, "client_id");
    String VerificationCode = MapUtil.getStr(parameters, "VerificationCode");

    // 参数校验
    if (StrUtil.isBlank(phone)) {
      throw new ServiceException("缺少请求参数:phone");
    }
    if (StrUtil.isBlank(VerificationCode)) {
      throw new ServiceException("缺少请求参数:VerificationCode");
    }
    if (StrUtil.isBlank(countryCode)) {
      throw new ServiceException("缺少请求参数:countryCode");
    }

    return service.getUserByPhoneAndVerificationCode(countryCode, phone, appId, VerificationCode);
  }
}

现在我们自定义的登录类就写完了,其实也就是简单的写了一个password登录类和手机号密码登录类,问题来了,我们怎么把他塞进去,让OAuth2进入我们的方法呢?

看configure 的配置: AuthorizationServerEndpointsConfigurer endpoints。

AuthorizationServerEndpointsConfigurer 里面已经有了这个,我们copy过来改改,改成自己的。

修改成自己的用户密码管理器,新增自己自定义验证码管理器。

 private List<TokenGranter> getDefaultTokenGranters(
            AuthorizationServerTokenServices tokenServices,
            ClientDetailsService clientDetails,
            AuthorizationCodeServices codeServices,
            OAuth2RequestFactory requestFactory) {
        List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
        tokenGranters.add(
                new AuthorizationCodeTokenGranter(
                        tokenServices, codeServices, 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));
        // 自定义用户密码登录管理器
        CustomAccountPasswordTokenGranter usernampasswordTokenGranter =
                new CustomAccountPasswordTokenGranter(
                        tokenServices, clientDetails, requestFactory, service);
        tokenGranters.add(usernampasswordTokenGranter);
        // 自定义验证码登录管理器
        CustomPhoneSmsTokenGranter phoneSmsTokenGranter =
                new CustomPhoneSmsTokenGranter(tokenServices, clientDetails, requestFactory, service);
        tokenGranters.add(phoneSmsTokenGranter);
        return tokenGranters;
    }

把他塞进配置文件里面

 //  主配置信息
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .exceptionTranslator(new AuthResponseExceptionTranslator())
                .approvalStore(approvalStore())
                .authenticationManager(authenticationManager)
                //            授权码模式需要
                .authorizationCodeServices(authorizationCodeServices())
                //            token 管理
                .tokenStore(tokenStore())
                .accessTokenConverter(accessTokenConverter());
        List<TokenGranter> defaultTokenGranters =
                getDefaultTokenGranters(
                        endpoints.getTokenServices(),
                        endpoints.getClientDetailsService(),
                        endpoints.getAuthorizationCodeServices(),
                        endpoints.getOAuth2RequestFactory());
        endpoints.tokenGranter(new CompositeTokenGranter(defaultTokenGranters));
    }

大功告成!!!!

还是老规矩,测试阶段难得搞了。这个需要对OAuth有点了解的看看,只是提供一种思路。

OAuth2 使用了很多模板模式,为了方便扩展,但是也同样让人有点找不着头脑,但是认真断点,看还是有很大收获的,网上的博客很多,在不是很了解的情况下去cv的话,坑很多。自己总结一下。