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被覆盖了。
最后一步,你新增一个授权模式,你的数据库表得加对应的。不然会匹配不上的。
测试:
总结:不知道有没有其他更好的方法,如果有欢迎留言。
还是那句话。先了解登录的一些流程。才能得心应手的扩展