基於上文講解的spring cloud 受權服務的搭建,本文擴展了spring security 的登錄方式,增長手機驗證碼登錄、二維碼登錄。 主要實現方式爲使用自定義filter、 AuthenticationProvider、 AbstractAuthenticationToken 根據不一樣登錄方式分別處理。 本文相應代碼在Github上已更新。
GitHub 地址:https://github.com/fp2952/spring-cloud-base/tree/master/auth-center/auth-center-providergit
二維碼掃碼登錄前提是已在微信端登錄,流程以下:github
自定義過濾器,實現AbstractAuthenticationProcessingFilter,在attemptAuthentication方法中根據不一樣登錄類型獲取對於參數、 並生成自定義的 MyAuthenticationToken。web
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // 登錄類型:user:用戶密碼登錄;phone:手機驗證碼登錄;qr:二維碼掃碼登錄 String type = obtainParameter(request, "type"); String mobile = obtainParameter(request, "mobile"); MyAuthenticationToken authRequest; String principal; String credentials; // 手機驗證碼登錄 if("phone".equals(type)){ principal = obtainParameter(request, "phone"); credentials = obtainParameter(request, "verifyCode"); } // 二維碼掃碼登錄 else if("qr".equals(type)){ principal = obtainParameter(request, "qrCode"); credentials = null; } // 帳號密碼登錄 else { principal = obtainParameter(request, "username"); credentials = obtainParameter(request, "password"); if(type == null) type = "user"; } if (principal == null) { principal = ""; } if (credentials == null) { credentials = ""; } principal = principal.trim(); authRequest = new MyAuthenticationToken( principal, credentials, type, mobile); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } private void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } private String obtainParameter(HttpServletRequest request, String parameter) { return request.getParameter(parameter); }
繼承 AbstractAuthenticationToken,添加屬性 type,用於後續判斷。redis
public class MyAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 110L; private final Object principal; private Object credentials; private String type; private String mobile; /** * This constructor can be safely used by any code that wishes to create a * <code>UsernamePasswordAuthenticationToken</code>, as the {@link * #isAuthenticated()} will return <code>false</code>. * */ public MyAuthenticationToken(Object principal, Object credentials,String type, String mobile) { super(null); this.principal = principal; this.credentials = credentials; this.type = type; this.mobile = mobile; this.setAuthenticated(false); } /** * This constructor should only be used by <code>AuthenticationManager</code> or <code>AuthenticationProvider</code> * implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>) * token token. * * @param principal * @param credentials * @param authorities */ public MyAuthenticationToken(Object principal, Object credentials,String type, String mobile, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; this.type = type; this.mobile = mobile; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } public String getType() { return this.type; } public String getMobile() { return this.mobile; } public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if(isAuthenticated) { throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } else { super.setAuthenticated(false); } } public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
代碼與 AbstractUserDetailsAuthenticationProvider 基本一致,只需修改 authenticate 方法 及 createSuccessAuthentication 方法中的 UsernamePasswordAuthenticationToken 爲咱們的 token, 改成:spring
public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 此處修改斷言自定義的 MyAuthenticationToken Assert.isInstanceOf(MyAuthenticationToken.class, authentication, this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.onlySupports", "Only MyAuthenticationToken is supported")); // ... } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { MyAuthenticationToken result = new MyAuthenticationToken(principal, authentication.getCredentials(),((MyAuthenticationToken) authentication).getType(),((MyAuthenticationToken) authentication).getMobile(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
繼承咱們自定義的AuthenticationProvider,編寫驗證方法additionalAuthenticationChecks及 retrieveUser緩存
/** * 自定義驗證 * @param userDetails * @param authentication * @throws AuthenticationException */ protected void additionalAuthenticationChecks(UserDetails userDetails, MyAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if(this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } if(authentication.getCredentials() == null) { this.logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); // 驗證開始 if("phone".equals(authentication.getType())){ // 手機驗證碼驗證,調用公共服務查詢後臺驗證碼緩存: key 爲authentication.getPrincipal()的value, 並判斷其與驗證碼是否匹配, 此處寫死爲 1000 if(!"1000".equals(presentedPassword)){ this.logger.debug("Authentication failed: verifyCode does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad verifyCode")); } }else if(MyLoginAuthenticationFilter.SPRING_SECURITY_RESTFUL_TYPE_QR.equals(authentication.getType())){ // 二維碼只須要根據 qrCode 查詢到用戶便可,因此此處無需驗證 } else { // 用戶名密碼驗證 if(!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { this.logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } } } protected final UserDetails retrieveUser(String username, MyAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { // 調用loadUserByUsername時加入type前綴 loadedUser = this.getUserDetailsService().loadUserByUsername(authentication.getType() + ":" + username); } catch (UsernameNotFoundException var6) { if(authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null); } throw var6; } catch (Exception var7) { throw new InternalAuthenticationServiceException(var7.getMessage(), var7); } if(loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } }
查詢用戶時根據類型採用不一樣方式查詢: 帳號密碼根據用戶名查詢用戶; 驗證碼根據 phone查詢用戶, 二維碼可調用公共服務微信
@Override public UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException { BaseUser baseUser; String[] parameter = var1.split(":"); // 手機驗證碼調用FeignClient根據電話號碼查詢用戶 if("phone".equals(parameter[0])){ ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByPhone(parameter[1]); if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){ logger.error("找不到該用戶,手機號碼:" + parameter[1]); throw new UsernameNotFoundException("找不到該用戶,手機號碼:" + parameter[1]); } baseUser = baseUserResponseData.getData(); } else if("qr".equals(parameter[0])){ // 掃碼登錄根據key從redis查詢用戶 baseUser = null; } else { // 帳號密碼登錄調用FeignClient根據用戶名查詢用戶 ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByUserName(parameter[1]); if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){ logger.error("找不到該用戶,用戶名:" + parameter[1]); throw new UsernameNotFoundException("找不到該用戶,用戶名:" + parameter[1]); } baseUser = baseUserResponseData.getData(); } // 調用FeignClient查詢角色 ResponseData<List<BaseRole>> baseRoleListResponseData = baseRoleService.getRoleByUserId(baseUser.getId()); List<BaseRole> roles; if(baseRoleListResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseRoleListResponseData.getCode())){ logger.error("查詢角色失敗!"); roles = new ArrayList<>(); }else { roles = baseRoleListResponseData.getData(); } //調用FeignClient查詢菜單 ResponseData<List<BaseModuleResources>> baseModuleResourceListResponseData = baseModuleResourceService.getMenusByUserId(baseUser.getId()); // 獲取用戶權限列表 List<GrantedAuthority> authorities = convertToAuthorities(baseUser, roles); // 存儲菜單到redis if( ResponseCode.SUCCESS.getCode().equals(baseModuleResourceListResponseData.getCode()) && baseModuleResourceListResponseData.getData() != null){ resourcesTemplate.delete(baseUser.getId() + "-menu"); baseModuleResourceListResponseData.getData().forEach(e -> { resourcesTemplate.opsForList().leftPush(baseUser.getId() + "-menu", e); }); } // 返回帶有用戶權限信息的User org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(baseUser.getUserName(), baseUser.getPassword(), isActive(baseUser.getActive()), true, true, true, authorities); return new BaseUserDetail(baseUser, user); }
將咱們自定義的類配置到spring security 登錄流程中websocket
@Configuration @Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 自動注入UserDetailsService @Autowired private BaseUserDetailService baseUserDetailService; @Override public void configure(HttpSecurity http) throws Exception { http // 自定義過濾器 .addFilterAt(getMyLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 配置登錄頁/login並容許訪問 .formLogin().loginPage("/login").permitAll() // 登出頁 .and().logout().logoutUrl("/logout").logoutSuccessUrl("/backReferer") // 其他全部請求所有須要鑑權認證 .and().authorizeRequests().anyRequest().authenticated() // 因爲使用的是JWT,咱們這裏不須要csrf .and().csrf().disable(); } /** * 用戶驗證 * @param auth */ @Override public void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(myAuthenticationProvider()); } /** * 自定義密碼驗證 * @return */ @Bean public MyAuthenticationProvider myAuthenticationProvider(){ MyAuthenticationProvider provider = new MyAuthenticationProvider(); // 設置userDetailsService provider.setUserDetailsService(baseUserDetailService); // 禁止隱藏用戶未找到異常 provider.setHideUserNotFoundExceptions(false); // 使用BCrypt進行密碼的hash provider.setPasswordEncoder(new BCryptPasswordEncoder(6)); return provider; } /** * 自定義登錄過濾器 * @return */ @Bean public MyLoginAuthenticationFilter getMyLoginAuthenticationFilter() { MyLoginAuthenticationFilter filter = new MyLoginAuthenticationFilter(); try { filter.setAuthenticationManager(this.authenticationManagerBean()); } catch (Exception e) { e.printStackTrace(); } filter.setAuthenticationSuccessHandler(new MyLoginAuthSuccessHandler()); filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error")); return filter; } }