封面图片

编程

Spring Security多登录方式的集成教程

账号密码的单一登录方式已经不能满足系统软件开发要求。目前常见的登录方式还包括通过第三方认证登录,短信验证码登录等,所以一个系统提供多种登录方式是一个很普遍的需求。


前段时间项目上刚好有一个需求,需要对接钉钉开发的政务系统,通过钉钉政务应用进行免登跳转。主要的流程就是应用在钉钉政务上进行注册,然后就可以在钉钉政务上看到相关的应用图标,点击后就会进入该应用,应用通过跳转url携带的code参数从钉钉政务获取用户信息,然后设置用户在应用上为登录状态。

所以需要对目前的账号密码登录体系进行改造。目前的系统使用的spring security session的方式开发的登录认证功能。在保持原有登录功能不变的前提下增加通过授权码的方式登录。2种方式共存,在钉钉政务不可用的情况下,还可以使用系统的账号密码登录。

寻找解决方案

目标确认了,就开始借助google。

第一步是先熟悉一下整个spring security的登录流程。虽然之前也开发过spring security 基于token 的登录认证。但是security的整个认证流程还是比较多的。再熟悉一下整个流程对改造帮助很大,避免出现过多的问题。

通过打断点的方式梳理了一下整个认证的流程:

  1. AbstractAuthenticationProcessingFilter
  2. UsernamePasswordAuthenticationFilter
  3. ProviderManager implements AuthenticationManager
  4. AbstractUserDetailsAuthenticationProvider
  5. MyAuthenticationProvider (自己实现认证逻辑)
  6. MyUserDetailServiceImpl implements UserDetailsService (自己实现查询用户信息)
  7. UsernamePasswordAuthenticationToken

其他就不用管了。最后在配置一下认证成功和认证失败的处理类就OK了。

第二步是搜索包括spring security集成第三方登录,多种登录方式等关键词。通过参考了网上提供最多的短信验证码的登录集成的文章来确定开发内容。

第三步是确定开发步骤及内容。内容如下:

  1. 开发自定义认证过滤器CustomAuthenticationFilter
  2. 开发CodeAuthenticationToken (参考UsernamePasswordAuthenticationToken)
  3. 开发根据code获取用户信息类CodeUserDetailsService (对标MyUserDetailServiceImpl)
  4. 开发认证类CodeAUthenticationProvider (对标MyAuthenticationProvider)
  5. 修改配置类SecurityConfig

主要代码

1public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 2 //验证类型,比如Sms,uernamepassword,code等 3 private String authTypeParameter = "authType"; 4 //对应用户名或手机号等 5 private String principalParameter = "username"; 6 //对应密码或验证码等 7 private String credentialsParameter = "password"; 8 //授权码 9 private String codeParameter = "authCode"; 10 private final boolean postOnly = true; 11 private final String HttpMethod = "POST"; 12 13 //对post请求的login请求进行拦截 14 public ThirdAuthenticationFilter() { 15 super(new AntPathRequestMatcher("/login", "POST")); 16 } 17 18 @Override 19 @Autowired 20 public void setAuthenticationManager(AuthenticationManager authenticationManager) { 21 super.setAuthenticationManager(authenticationManager); 22 } 23 24 @Override 25 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { 26 if (postOnly && !HttpMethod.equals(request.getMethod())) { 27 throw new AuthenticationServiceException( 28 "Authentication method not supported: " + request.getMethod()); 29 } 30 String authType = request.getParameter(authTypeParameter); 31 if(!StringUtils.hasLength(authType)){ 32 authType = AuthTypeEnum.Default.getType(); 33 } 34 String principal = request.getParameter(principalParameter); 35 String credentials = request.getParameter(credentialsParameter); 36 String code = request.getParameter(codeParameter); 37 AbstractAuthenticationToken authRequest = null; 38 switch (authType){ 39 case "code": 40 authRequest = new CodeAuthenticationToken(code); 41 break; 42 //不是授权码模式则走原来的账号密码登录认证 43 case "default": 44 default: 45 authRequest = new UsernamePasswordAuthenticationToken(principal, credentials); 46 47 } 48 authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); 49 50 return this.getAuthenticationManager().authenticate(authRequest); 51 } 52}
1public class CodeAuthenticationToken extends AbstractAuthenticationToken implements Serializable { 2 private static final long serialVersionUID = -4122876712654000772L; 3 4 /** 5 * 认证个体 6 */ 7 private Object principal; 8 public CodeAuthenticationToken(Object principal) { 9 super(null); 10 this.principal = principal; 11 } 12 13 @Override 14 public Object getCredentials() { 15 return null; 16 } 17 18 @Override 19 public Object getPrincipal() { 20 return this.principal; 21 } 22 23 public void setPrincipal(Object principal) { 24 this.principal = principal; 25 } 26 27}
1@Component 2public class CodeUserDetailService { 3 4 public LoginUser queryRemoteUserByCode(String authCode) throws UsernameNotFoundException { 5 //todo 根据authCode查询用户信息,并封装成登录用户信息,获取不到则返回null 6 ... 7 } 8}
1/** 2* 授权码认证类,能获取到用户信息则认证成功,其他情况则认证失败。 3*/ 4public class CodeAuthenticationProvider implements AuthenticationProvider { 5 6 private final CodeUserDetailService userDetailsService; 7 8 public CodeAuthenticationProvider(CodeUserDetailService userDetailsService) { 9 this.userDetailsService = userDetailsService; 10 } 11 12 @Override 13 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 14 CodeAuthenticationToken codeAuthenticationToken = (CodeAuthenticationToken) authentication; 15 LoginUser loginUser = userDetailsService.queryRemoteUserByCode(codeAuthenticationToken.getPrincipal().toString()); 16 if (loginUser == null) { 17 throw new InternalAuthenticationServiceException("无法获取用户信息"); 18 } 19 codeAuthenticationToken.setPrincipal(loginUser); 20 codeAuthenticationToken.setAuthenticated(true); 21 return codeAuthenticationToken; 22 } 23 24 @Override 25 public boolean supports(Class<?> authentication) { 26 return CodeAuthenticationToken.class.isAssignableFrom(authentication); 27 } 28}

修改SecurityConfig

1//在原来的基础上增加CodeAuthenticationProvider() 2//authenticationProvider是原有的账号密码认证器 3//userDetailService是原有的用户信息查询类 4@Override 5protected void configure(AuthenticationManagerBuilder auth) throws Exception { 6 auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); 7 auth.authenticationProvider(authenticationProvider).authenticationProvider(CodeAuthenticationProvider()); 8} 9 10//配置自定义认证过滤器,加载原有的认证成功和认证失败处理类 11@Bean 12public CustomAuthenticationFilter customAuthenticationFilter() { 13 CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(); 14 customAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler); 15 customAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler); 16 return thirdAuthenticationFilter; 17} 18 19@Resource 20CodeUserDetailService codeUserDetailService; 21 22@Bean 23public CodeAuthenticationProvider codeAuthenticationProvider() { 24 return new CodeAuthenticationProvider(codeUserDetailService); 25} 26 27@Override 28protected void configure(HttpSecurity httpSecurity) throws Exception { 29 httpSecurity 30 .formLogin() 31 .loginProcessingUrl("/login") 32 .and() 33 .logout().logoutUrl("/logout") 34 .logoutSuccessHandler(myLogoutSuccessHandler) 35 .and() 36 .sessionManagement().invalidSessionUrl("/login/invalid") 37 .and() 38 .authorizeRequests() 39 .antMatchers("/login", "/logout", "/oauth/token", "/error") 40 .permitAll() 41 .antMatchers("/swagger-ui.html", 42 "/swagger-ui/*", 43 "/swagger-resources/**", 44 "/v2/api-docs", 45 "/v3/api-docs") 46 .permitAll() 47 .anyRequest().authenticated() 48 .and() 49 .authenticationProvider(codeAuthenticationProvider()) 50 .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) 51 //原有账号密码认证类 52 .authenticationProvider(authenticationProvider) 53 .csrf().disable(); 54 55}

以上代码只是增加授权码登录方式新增的或者修改的类。提供改造参考,不要直接使用。

2023年04月26日
在初学者眼中,世界充满了可能;专家眼中,世界大都已经既定。--铃木俊隆