SpringSecurity 调用流程:
首先会进入UsernamePasswordAuthenticationFilter并且设置权限为null和是否授权为false,然后进入ProviderManager查找支持UsernamepasswordAuthenticationToken的provider并且调用provider.authenticate(authentication);再然后就是UserDetailsService接口的实现类,然后回调UsernamePasswordAuthenticationFilter并且设置权限(具体业务所查出的权限)和设置授权为true。
下面是实现手机验证码登录的流程:
第一步:新建MyAuthenticationToken 自定义AbstractAuthenticationToken
/** * @Description 自定义AbstractAuthenticationToken * @Author wwz * @Date 2019/08/04 * @Param * @Return */ public class MyAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 110L; protected final Object principal; protected Object credentials; /** * 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) { super(null); this.principal = principal; this.credentials = credentials; 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, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } 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; } }
第二步:新建 (手机验证码登录用)MyPhoneAuthenticationToken 继承 MyAuthenticationToken
/** * @Description 手机验证token * @Author wwz * @Date 2019/08/04 * @Param * @Return */ public class MyPhoneAuthenticationToken extends MyAuthenticationToken { public MyPhoneAuthenticationToken(Object principal, Object credentials) { super(principal, credentials); } public MyPhoneAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(principal, credentials, authorities); } }
第三步:新建MyAbstractUserDetailsAuthenticationProvider 抽象类 自定义AuthenticationProvider 抽象,方便其他扩展
/** * @Description 自定义AuthenticationProvider 抽象,方便其他扩展 * @Author wwz * @Date 2019/08/04 * @Param * @Return */ public abstract class MyAbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { protected final Log logger = LogFactory.getLog(this.getClass()); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private UserCache userCache = new NullUserCache(); private boolean forcePrincipalAsString = false; protected boolean hideUserNotFoundExceptions = true; private UserDetailsChecker preAuthenticationChecks = new MyAbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks(); private UserDetailsChecker postAuthenticationChecks = new MyAbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks(); private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); protected abstract void additionalAuthenticationChecks(UserDetails var1, Authentication var2) throws AuthenticationException; public final void afterPropertiesSet() throws Exception { Assert.notNull(this.userCache, "A user cache must be set"); Assert.notNull(this.messages, "A message source must be set"); this.doAfterPropertiesSet(); } public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getPrincipal() == null?"NONE_PROVIDED":authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if(user == null) { cacheWasUsed = false; try { user = this.retrieveUser(username, authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("User \'" + username + "\' not found"); if(this.hideUserNotFoundExceptions) { throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } throw var6; } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, authentication); } catch (AuthenticationException var7) { if(!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, authentication); } this.postAuthenticationChecks.check(user); if(!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if(this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); } protected abstract Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user); protected void doAfterPropertiesSet() throws Exception { } public UserCache getUserCache() { return this.userCache; } public boolean isForcePrincipalAsString() { return this.forcePrincipalAsString; } public boolean isHideUserNotFoundExceptions() { return this.hideUserNotFoundExceptions; } protected abstract UserDetails retrieveUser(String var1, Authentication var2) throws AuthenticationException; public void setForcePrincipalAsString(boolean forcePrincipalAsString) { this.forcePrincipalAsString = forcePrincipalAsString; } public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) { this.hideUserNotFoundExceptions = hideUserNotFoundExceptions; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } public void setUserCache(UserCache userCache) { this.userCache = userCache; } protected UserDetailsChecker getPreAuthenticationChecks() { return this.preAuthenticationChecks; } public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) { this.preAuthenticationChecks = preAuthenticationChecks; } protected UserDetailsChecker getPostAuthenticationChecks() { return this.postAuthenticationChecks; } public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) { this.postAuthenticationChecks = postAuthenticationChecks; } public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; } private class DefaultPostAuthenticationChecks implements UserDetailsChecker { private DefaultPostAuthenticationChecks() { } public void check(UserDetails user) { if(!user.isCredentialsNonExpired()) { MyAbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired"); throw new CredentialsExpiredException(MyAbstractUserDetailsAuthenticationProvider.this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired")); } } } private class DefaultPreAuthenticationChecks implements UserDetailsChecker { private DefaultPreAuthenticationChecks() { } public void check(UserDetails user) { if(!user.isAccountNonLocked()) { MyAbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked"); throw new LockedException(MyAbstractUserDetailsAuthenticationProvider.this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); } else if(!user.isEnabled()) { MyAbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled"); throw new DisabledException(MyAbstractUserDetailsAuthenticationProvider.this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } else if(!user.isAccountNonExpired()) { MyAbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired"); throw new AccountExpiredException(MyAbstractUserDetailsAuthenticationProvider.this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } } }
第四步:新建 MyPhoneAuthenticationProvider(手机验证码登录 )继承 MyAbstractUserDetailsAuthenticationProvider ,在这里使用MyPhoneAuthenticationToken,注入参数,同时在这里进行手机验证码验证等,为了方便我这里直接写死了。
/** * @Description 手机验证码登录 * @Author wwz * @Date 2019/08/04 * @Param * @Return */ public class MyPhoneAuthenticationProvider extends MyAbstractUserDetailsAuthenticationProvider { private UserDetailsService userDetailsService; @Override protected void additionalAuthenticationChecks(UserDetails var1, Authentication authentication) throws AuthenticationException { if(authentication.getPrincipal() == null){ throw new BadCredentialsException(this.messages.getMessage("MyPhoneAuthenticationProvider.badPrincipal", "Bad badPrincipal")); } if (authentication.getCredentials() == null) { throw new BadCredentialsException(this.messages.getMessage("MyPhoneAuthenticationProvider.badCredentials", "Bad credentials")); } else { String phoneCode = authentication.getCredentials().toString(); // String phoneNumber = authentication.getPrincipal().toString(); // // String old_code = (String) redisTemplate.opsForValue().get(phoneNumber+"_code"); // // if(old_code==null){ // // 验证码未获取或已经失效 // throw new BadCredentialsException(this.messages.getMessage("MyPhoneAuthenticationProvider.badCredentials", "Bad phoneCode")); // } // if(!phoneCode.equals(old_code)){ // // 验证码错误 // throw new BadCredentialsException(this.messages.getMessage("MyPhoneAuthenticationProvider.badCredentials", "Bad phoneCode")); // } if (!"1234".equals(phoneCode)) { throw new BadCredentialsException(this.messages.getMessage("MyPhoneAuthenticationProvider.badCredentials", "Bad phoneCode")); } } } @Override protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { MyPhoneAuthenticationToken result = new MyPhoneAuthenticationToken(principal, authentication.getCredentials(), user.getAuthorities()); result.setDetails(authentication.getDetails()); return result; } @Override protected UserDetails retrieveUser(String phone, Authentication authentication) throws AuthenticationException { UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(phone); } catch (UsernameNotFoundException var6) { 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; } } @Override public boolean supports(Class<?> authentication) { return MyPhoneAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
第五步: 修改MyUserDetailsService,改为抽象类,使用模板方法模式: protected abstract AuthUser getUser(String var1); 来获取不同数据来源
/** * @Description 自定义用户验证数据 * @Author wwz * @Date 2019/07/28 * @Param * @Return */ @Service public abstract class MyUserDetailsService implements UserDetailsService { @Autowired protected AuthUserMapper authUserMapper; @Override public UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException { // 自定义用户权限数据 AuthUser authUser1 = getUser(var1); AuthUser authUser = authUserMapper.selectById(authUser1.getId()); if (authUser == null) { throw new UsernameNotFoundException("用户名不存在"); } if (!authUser.getValid()) { throw new UsernameNotFoundException("用户不可用"); } Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); if (authUser.getAuthRoles() != null) { for (AuthRole role : authUser.getAuthRoles()) { // 当前角色可用 if (role.getValid()) { //角色必须是ROLE_开头 GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName()); grantedAuthorities.add(grantedAuthority); if (role.getAuthPermissions() != null) { for (AuthPermission permission : role.getAuthPermissions()) { // 当前权限可用 if (permission.getValid()) { // 拥有权限设置为 auth/member/GET 可以访问auth服务下面 member的查询方法 GrantedAuthority authority = new SimpleGrantedAuthority(permission.getServicePrefix() + "/" + permission.getUri() + "/" + permission.getMethod()); grantedAuthorities.add(authority); } } } } //获取权限 } } MyUserDetails userDetails = new MyUserDetails(authUser, grantedAuthorities); return userDetails; } protected abstract AuthUser getUser(String var1); }
继续第五步:
新建MyUsernameUserDetailsService继承 MyUsernameUserDetailsService 该方法为原来的登录提供数据
@Service public class MyUsernameUserDetailsService extends MyUserDetailsService { @Override protected AuthUser getUser(String var1) { // 账号密码登录根据用户名查询用户 AuthUser authUser = authUserMapper.selectByUsername(var1); if (authUser == null) { throw new UsernameNotFoundException("找不到该用户,用户名:" + var1); } return authUser; } }
新建MyPhoneUserDetailsService 继承MyUsernameUserDetailsService 该方法为新的手机验证码登录提供数据。
@Service public class MyPhoneUserDetailsService extends MyUserDetailsService { @Override protected AuthUser getUser(String var1) { // 手机验证码登录使用,根据手机号码查询用户信息 AuthUser authUser = authUserMapper.selectByPhone(var1); if (authUser == null) { throw new UsernameNotFoundException("找不到该用户,手机号码有误:" + var1); } return authUser; } }
第六步:新建MyLoginAuthSuccessHandler 登录成功处理器,该方法用于验证client信息 并返回token信息。
/** * @Description 登录成功处理器 * @Author wwz * @Date 2019/08/04 * @Param * @Return */ @Component public class MyLoginAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Autowired private MyClientDetailsService clientDetailsService; @Autowired private AuthorizationServerTokenServices authorizationServerTokenServices; @Bean public MyClientDetailsService clientDetailsService(){ return new MyClientDetailsService(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header == null && !header.startsWith("Basic")) { throw new UnapprovedClientAuthenticationException("请求投中无client信息"); } String tmp = header.substring(6); String defaultClientDetails = new String(Base64.getDecoder().decode(tmp)); String[] clientArrays = defaultClientDetails.split(":"); String clientId = clientArrays[0].trim(); String clientSecret = clientArrays[1].trim(); ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); if (clientDetails == null) { throw new UnapprovedClientAuthenticationException("clientId 不存在" + clientId); //判断 方言 是否一致 } else if (!passwordEncoder().matches(clientSecret, clientDetails.getClientSecret())) { throw new UnapprovedClientAuthenticationException("clientSecret 不匹配" + clientId); } TokenRequest tokenRequest = new TokenRequest(null, clientId, clientDetails.getScope(), "custom"); OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSONObject.toJSONString(token)); } }
第七步:新建MyLoginAuthFailureHandler登录失败处理器,返回失败异常,该异常为第四步抛出异常
/** * @Description 登录失败处理器 * @Author wwz * @Date 2019/08/05 * @Param * @Return */ @Component public class MyLoginAuthFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { ResponseVo responseVo = new ResponseVo(); responseVo.setCode(401); responseVo.setMessage(exception.getMessage()); responseVo.setData("path:"+request.getRequestURI()); response.setStatus(401); HttpUtilsResultVO.writerError(responseVo, response); } }
第八步: 新建MyPhoneLoginAuthenticationFilter 手机验证码登录过滤器,拦截登录的url,进行数据注入到MyPhoneAuthenticationToken。
/** * @Description 手机验证码:post /token/login?type=phoneNumber&phoneNumber=15000000000&phoneCode=1234 * @Author wwz * @Date 2019/08/04 * @Param * @Return */ public class MyPhoneLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final String PHONE_NUMBER_KEY = "phoneNumber"; // 手机号码 private static final String PHONE_NUMBER_CODE_KEY = "phoneCode"; // 验证码 private boolean postOnly = true; private static final String LOGIN_URL = "/token/phoneLogin"; public MyPhoneLoginAuthenticationFilter() { super(new AntPathRequestMatcher(LOGIN_URL, "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } AbstractAuthenticationToken authRequest; // 手机验证码登陆 String principal =RequestUtil(request,PHONE_NUMBER_KEY); String credentials =RequestUtil(request,PHONE_NUMBER_CODE_KEY); principal = principal.trim(); authRequest = new MyPhoneAuthenticationToken(principal, credentials); // 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 RequestUtil(HttpServletRequest request, String parameter) { String result = request.getParameter(parameter); return result == null ? "" : result; } }
第九步: 重点 修改MySecurityConfig 配置,装配 两个数据接口,装配 登录成功处理器 装配配置,以及把MyPhoneLoginAuthenticationFilter加入到过滤链。
/** * @Description security 配置 * ResourceServerConfigurerAdapter 是比WebSecurityConfigurerAdapter 的优先级低的 * @Author wwz * @Date 2019/07/28 * @Param * @Return */ @Configuration @EnableWebSecurity @Order(2) // WebSecurityConfigurerAdapter 默认为100 这里配置为2设置比资源认证器高 public class MySecurityConfig extends WebSecurityConfigurerAdapter { // 自定义用户验证数据 @Autowired private MyUsernameUserDetailsService myUsernameUserDetailsService; @Autowired private MyPhoneUserDetailsService myPhoneUserDetailsService; // 装配登录成功处理器 生成token用 通用, 下方配置的时候不能用new 的形式加入 不然里面的接口注入会报空指针 @Autowired private MyLoginAuthSuccessHandler myLoginAuthSuccessHandler; @Autowired private MyLoginAuthFailureHandler myLoginAuthFailureHandler; // 配置登录失败处理器 // 加密方式 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 验证器加载 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(getPhoneLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 匹配oauth相关,匹配健康,匹配默认登录登出 在httpSecurity处理,,其他到ResourceServerConfigurerAdapter OAuth2处理 1 .requestMatchers().antMatchers("/oauth/**", "/actuator/health", "/client/**","/token/phoneLogin") .and() // 匹配的全部无条件通过 permitAll 2 .authorizeRequests().antMatchers("/oauth/**", "/actuator/health", "/client/**","/token/phoneLogin").permitAll() // 匹配条件1的 并且不再条件2通过范围内的其他url全部需要验证登录 .and().authorizeRequests().anyRequest().authenticated() // 启用登录验证 .and().formLogin().permitAll(); // 不启用 跨站请求伪造 默认为启用, 需要启用的话得在form表单中生成一个_csrf http.csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuthenticationProvider()); auth.authenticationProvider(myPhoneAuthenticationProvider()); } @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); // 设置userDetailsService provider.setUserDetailsService(myUsernameUserDetailsService); // 禁止隐藏用户未找到异常 provider.setHideUserNotFoundExceptions(false); // 使用BCrypt进行密码的hash provider.setPasswordEncoder(passwordEncoder()); return provider; } @Bean public MyPhoneAuthenticationProvider myPhoneAuthenticationProvider() { MyPhoneAuthenticationProvider provider = new MyPhoneAuthenticationProvider(); provider.setUserDetailsService(myPhoneUserDetailsService); provider.setHideUserNotFoundExceptions(false); return provider; } /** * 手机验证码登陆过滤器 */ @Bean public MyPhoneLoginAuthenticationFilter getPhoneLoginAuthenticationFilter() { MyPhoneLoginAuthenticationFilter filter = new MyPhoneLoginAuthenticationFilter(); try { filter.setAuthenticationManager(this.authenticationManagerBean()); } catch (Exception e) { e.printStackTrace(); } filter.setAuthenticationSuccessHandler(myLoginAuthSuccessHandler); filter.setAuthenticationFailureHandler(myLoginAuthFailureHandler); return filter; } }
验证码错误返回:
登录成功返回:
最后,原来登录方式,因为修改了 MyUserDetailsService 接口,无法为原来的登录方式提供数据,所以改为MyUsernameUserDetailsService来提供数据,在MySecurityOAuth2Config中调整
补充,因为自定义了MyPhoneAuthenticationToken,在资源服务器中使用该模式的token需要把文件放置到对应的资源项目中,不然会报找不到文件异常。
账号密码登录,可以参照步骤执行,或者按前面的方法来完成。