hey-girl东拼西凑原创文章,若有歧义可留言,若需转载需标明出处。
hey有话说: 之前的文章中已经详细的介绍了登录认证流程。建议搞不清的兄弟可以先去瞄瞄。在往下看。项目中集成auth2,给了我们内置的4种授权模式+一个令牌刷新的模式,用grant_type区分。
但是,仔细琢磨。这4种肯定是不能适合各种美好的需求的。不得整个洋气的手机登录或者验证码登录或者第三方微信啥的。
注意:如果只是看实现,会很懵的。还是了解下内置的授权模式流程,在看自定义的。我们的小脑袋瓜才能收获满满。了解原理请看我之前的文章。
还有这是在原有的4种模式上新增,而不是说新增这一个,覆盖原有的。要是是并存
废话说完,开始整活。
大体思路分析。
内置的5种模式TokenGranter 实现类,是不是都在CompositeTokenGranter这个类里面通过tokenGranters 管理在一起了。然后循环遍历对比grant_type,找到对应的实现类来处理授权
而每种授权的方式对应一种AuthenticationProvider 实现类来实现。所有 AuthenticationProvider 实现类都通过 ProviderManager 中的 providers 集合存起来
,TokenGranter的实现类会整一个AuthenticationToken实现类给Manager(认证管理类),认证管理类里面也管理了很多Providers(认证具体实现类),那不又得遍历下,通过supports方法 ,看看AuthenticationToken 和谁匹配 。找到具体的那个AuthenticationProvider 去干活。
大致的形容了下,是不是就知道我们自定义的时候要干啥了。
我们从头来,一步步捋清楚。做一个手机号+密码登录的模式练习下
首先用老演员postman发起一个post请求。client_id和client_secret可以放参数中或者头部,随意就好了。
动动我们小脑瓜想想。我们的授权模式grant_type:phone。这个模式是我们自己定义的,CompositeTokenGranter里面是没有管理的。我们想要程序认识新的模式。是不是得给安排个实现类。交给CompositeTokenGranter管理起来。
那有小可爱就犯糊涂了。我们怎么写实现类了。
来来来,看看下其他模式是怎么搞的,我们不能超越,但是可以先模仿是不是。
那我们就挑个常用的,照着ResourceOwnerPasswordTokenGranter写。
package com.hey.girl.auth.grant;
/**
* @Description: 自定义grant_type颁发令牌
* @author: heihei
* @date: 2021年12月03日 11:39
*/
public class PhoneCustomTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "phone";
private final AuthenticationManager authenticationManager;
public PhoneCustomTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory
) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, "phone");
}
protected PhoneCustomTokenGranter(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 phone = (String)parameters.get("phone");
String password = (String)parameters.get("password");
parameters.remove("password");
Authentication userAuth = new PhoneAuthenticationToken(phone, password);
((AbstractAuthenticationToken)userAuth).setDetails(parameters);
try{
userAuth = authenticationManager.authenticate(userAuth);
}catch (Exception e){
throw new InvalidGrantException("Could not authenticate mobile: " + phone);
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
throw new InvalidGrantException("Could not authenticate user: " + phone);
}
}
}
Q1- 代码解析kankan
A1- GRANT_TYPE:授权模式;authenticationManager 认证管理类
Q2- getOAuth2Authentication方法解析
A2- 这个方法是在生成令牌的时候被调用的。 OAuth2Request(等于TokenRequests+ClientDetails的整合)和Authorization(当前授权用户)合在一起生成一个OAuth2Authentication。
在这方法中,主要就是获取请求参数。手机号和密码登录。然后生成一个PhoneAuthenticationToken,传递给manager管理类,通过provider的supports方法。根据不同类型的token,去找对应的Provider做具体认证。认证完以后拿到Authentication。就可以创建OAuth2Authentication对象了。
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
/**
* 身份
*/
private final Object principal;
/**
* 凭证
*/
private Object credentials;
public PhoneAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public PhoneAuthenticationToken( Object principal,Object credentials,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException{
Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
/**
1. @Description: 认证具体实现类
2. @author: heihei
3. @date: 2021年12月03日 10:34
*/
@Setter
public class PhoneAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
/**
* 认证具体方法
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 断言所提供的对象是所提供类的实例
Assert.isInstanceOf(PhoneAuthenticationToken.class,authentication,()->{
return "Only PhoneAuthenticationToken is supported";
});
PhoneAuthenticationToken authenticationToken = (PhoneAuthenticationToken) authentication;
String mobile = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
UserDetails user =userDetailsService.loadUserByUsername(mobile);
PhoneAuthenticationToken authenticationResult = new PhoneAuthenticationToken(user, password,user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* support方法来表示自己支持那种Token的认证
*/
@Override
public boolean supports(Class<?> aClass) {
// 也就是判断当前的Class对象所表示的类,是不是参数中传递的Class对象所表示的类的父类,超接口,
// 或者是相同的类型。是则返回true,否则返回false。
return PhoneAuthenticationToken.class.isAssignableFrom(aClass);
}
}
写到这里,是不是还有个疑问,UserDetailsService是不是得写个。之前写了个public class GirlUserDetailsServiceImpl implements UserDetailsService。在这个类里loadUserByUsername方法是去调取远程服务查询数据库等操作。但是我这里变了。我的需求可能是通过手机号或者邮箱等字段去查询用户。那么我就写个GirlPhoneUserDetailsService 具体业务逻辑,可根据实际改。主要是明白这里的作用
@Slf4j
@Service()
@RequiredArgsConstructor
public class GirlPhoneUserDetailsService implements UserDetailsService {
/**
* feign调用远程服务
*/
private final RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
UserDetails userDetails = getUserDetails(result);
return userDetails;
}
/**
* 构建user details
* @param result 用户信息
* @return
*/
private UserDetails getUserDetails(R<UserInfo> result) {
if(result == null || result.getData() == null) {
throw new UsernameNotFoundException("用户不存在");
}
UserInfo userInfo = result.getData();
Set<String> dbAuthsSet = new HashSet<>();
/* 获取角色
* ArrayUtil是糊涂的工具
*/
if (ArrayUtil.isNotEmpty(userInfo.getRoles())) {
// 获取角色
Arrays.stream(userInfo.getRoles()).forEach(role -> dbAuthsSet.add(SecurityConstants.ROLE + role));
// 获取资源
dbAuthsSet.addAll(Arrays.asList(userInfo.getPermissions()));
}
// 注意toArray(new Object[0])与toArray()在函数上是相同的
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
SysUser user = userInfo.getSysUser();
// 构造security用户
return new GirlUser(user.getUserId(), user.getDeptId(), user.getUsername(),
SecurityConstants.BCRYPT + user.getPassword(),
StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL),
true,true, true, authorities );
}
}
// 贴部分重要代码
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancer())
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
.reuseRefreshTokens(false)
.pathMapping("/oauth/confirm_access", "/token/confirm_access")
.exceptionTranslator(new GirlWebResponseExceptionTranslator());
List<TokenGranter> tokenGranters = getDefaultTokenGranters(
endpoints.getTokenServices(),
endpoints.getClientDetailsService(),
endpoints.getAuthorizationCodeServices(),
endpoints.getOAuth2RequestFactory()
);
endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters));
}
private List<TokenGranter> getDefaultTokenGranters(
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetails,
AuthorizationCodeServices authorizationCodeServices,
OAuth2RequestFactory requestFactory
) {
List<TokenGranter> tokenGranters = new ArrayList();
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 (this.authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(this.authenticationManager, tokenServices, clientDetails, requestFactory));
}
tokenGranters.add(new PhoneCustomTokenGranter(
authenticationManager,tokenServices,
clientDetails,
requestFactory
));
return tokenGranters;
}
这这过程中遇到的问题。也是很常见的一种问题,就是我自己新增的模式覆盖程序提供的5种模式。可以看看这位作者写的添加自定义授权模式遇到的问题,和这位作者一样,我也想着用CompositeTokenGranter里的add方法,但是感觉不太知道哪里用比较合适。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(phoneAuthenticationProvider());
auth.authenticationProvider(daoAuthenticationProvider());
}
@Bean
public PhoneAuthenticationProvider phoneAuthenticationProvider() {
PhoneAuthenticationProvider phoneAuthenticationProvider= new PhoneAuthenticationProvider();
phoneAuthenticationProvider.setUserDetailsService(userPhoneDetailsService);
phoneAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return phoneAuthenticationProvider;
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
这里也是auth.authenticationProvider必须加你要用的provider,不然程序之前有的就GG被覆盖了。
最后一步,你新增一个授权模式,你的数据库表得加对应的。不然会匹配不上的。
测试:
总结:不知道有没有其他更好的方法,如果有欢迎留言。
还是那句话。先了解登录的一些流程。才能得心应手的扩展