GitHub地址
码云地址
首先会进入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
* UsernamePasswordAuthenticationToken
, as the {@link
* #isAuthenticated()} will return false
.
*
*/
public MyAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
/**
* This constructor should only be used by AuthenticationManager
or AuthenticationProvider
* implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = true
)
* 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 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需要把文件放置到对应的资源项目中,不然会报找不到文件异常。
账号密码登录,可以参照步骤执行,或者按前面的方法来完成。