需求又来了
需求又又又来了
- 需要根据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对象。
存在的问题:
- 不是优雅的解决方法。
- 如果要使用手机号和验证码登录的时候,又得判断拼接。
- 存在一定的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);
}
完全可以根据不同的内容来直接调用不同的方法,而不是根据一个传参来拼接。
\
结合源码看看
- 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的话,坑很多。自己总结一下。