SpringSecurity(四):自定义登陆认证实现手机号登陆

SpringSecurity默认提供了两种登陆,一种basic登陆一种表单登陆(分别在一三章有讲到),但是如果我们要实现其他方式的登陆(例如邮箱登陆,手机号登陆)又该怎么做呢?

第二章中讲到了Security的登录原来,以及最后给出的流程图,结合它们这章来实现自定义登陆认证

 1.MobileAuthenticationToken

/**
 * 手机登录认证token 
 * 
 * 仿UsernamePasswordAuthenticationToken
 * 
 * 手机登录不需要密码,删掉所有password相关即可
 * 
 * @author majie
 *
 */
public class MobileAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = 4376675810462015013L;

	// ~ Instance fields
	// ================================================================================================

	private final Object principal;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * 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 MobileAuthenticationToken(Object principal) {
		super(null);
		this.principal = principal;
		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>)
	 * authentication token.
	 *
	 * @param principal
	 * @param authorities
	 */
	public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		super.setAuthenticated(true); // must use super, as we override
	}

	// ~ Methods
	// ========================================================================================================

	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");
		}

		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
	}

	@Override
	public Object getCredentials() {
		return null;
	}
}


2.MobileAuthenticationFilter

/**
 * 手机登录过滤器
 * 实现同UsernamePasswordAuthenticationFilter
 * 将username相关的都改成mobile,而且手机登录只有手机号,没有密码,所以去掉密码
 * 相应的参数最好写成可配置的
 * @author majie
 *
 */
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "mobile";

	private String mobileParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	
	private boolean postOnly = true;

	// ~ Constructors
	// ===================================================================================================

	public MobileAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login/mobile", "POST"));   //路径要改
	}

	// ~ Methods
	// ========================================================================================================

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);

		if (username == null) {
			username = "";
		}

		username = username.trim();

		MobileAuthenticationToken authRequest = new MobileAuthenticationToken(username);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

	/**
	 * Enables subclasses to override the composition of the username, such as by
	 * including additional values and a separator.
	 *
	 * @param request so that request attributes can be retrieved
	 *
	 * @return the username that will be presented in the <code>Authentication</code>
	 * request token to the <code>AuthenticationManager</code>
	 */
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(mobileParameter);
	}

	/**
	 * Provided so that subclasses may configure what is put into the authentication
	 * request's details property.
	 *
	 * @param request that an authentication request is being created for
	 * @param authRequest the authentication request object that should have its details
	 * set
	 */
	protected void setDetails(HttpServletRequest request,
			MobileAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}

	/**
	 * Sets the parameter name which will be used to obtain the username from the login
	 * request.
	 *
	 * @param usernameParameter the parameter name. Defaults to "username".
	 */
	public void setUsernameParameter(String usernameParameter) {
		Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
		this.mobileParameter = usernameParameter;
	}


	/**
	 * Defines whether only HTTP POST requests will be allowed by this filter. If set to
	 * true, and an authentication request is received which is not a POST request, an
	 * exception will be raised immediately and authentication will not be attempted. The
	 * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
	 * authentication.
	 * <p>
	 * Defaults to <tt>true</tt> but may be overridden by subclasses.
	 */
	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}

	public final String getUsernameParameter() {
		return mobileParameter;
	}
}


3.MobileAuthenticationProvider

第二章讲过,只有一个Manager,然后会遍历所有provider,找到支持该authentication的

/**
 * MobileAuthenticationProvider
 * 
 * 调用userDetailsService根据用户名查询用户信息
 * 
 * @author majie
 *
 */
public class MobileAuthenticationProvider implements AuthenticationProvider {

	private UserDetailsService userDetailsService;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		MobileAuthenticationToken authenticationToken = (MobileAuthenticationToken) authentication;

		UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

		if (userDetails == null) {

			throw new UsernameNotFoundException("用户名/密码无效");

		} else if (!userDetails.isEnabled()) {

			throw new DisabledException("用户已被禁用");

		} else if (!userDetails.isAccountNonExpired()) {

			throw new AccountExpiredException("账号已过期");

		} else if (!userDetails.isAccountNonLocked()) {
			
			throw new LockedException("账号已被锁定");
			
		} else if (!userDetails.isCredentialsNonExpired()) {
			
			throw new LockedException("凭证已过期");
		}

		MobileAuthenticationToken authenticationResult = new MobileAuthenticationToken(userDetails,
				userDetails.getAuthorities());

		authenticationResult.setDetails(authenticationToken.getDetails());

		return authenticationResult;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return MobileAuthenticationToken.class.isAssignableFrom(authentication);
	}

	public UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

}

再下面,需要实现自己在数据查询用户信息,所以需要添加依赖和数据库配置信息

pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

properties.yml

spring:
  datasource:
    driver-class-name:  com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.31.26:3306/test?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update   #第一次是创建
    show-sql: true

User类

@Entity
@Table(name = "user")
@Data
public class User implements UserDetails{
	
	private static final long serialVersionUID = -1212367372911855308L;

	@Id
	@GeneratedValue
	private Integer id;
	
	private String username;
	
	@JsonIgnore   //页面不显示该值
	private String password;
	
	private String mobile;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isEnabled() {
		// TODO Auto-generated method stub
		return true;
	}
	
}

UserRepository

public interface UserRepository extends JpaRepository<User, Integer> {

	@Query(value = "select * from user where username=?1 or mobile=?1",nativeQuery = true)
	User loadUserInfo(String username);

}

MyUserDetailsService实现security的UserDetailsService来实现自己的用户信息的加载

@Service
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
	
	@Autowired
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("用户名:" + username);
		
		User user = userRepository.loadUserInfo(username);
		
		log.info("用户信息" + user);
		return user;
	}

}

用户通过手机号登录时候还需要接受验证码,然后登陆时候验证验证码等操作。

需要自己写一个发送验证码的方法,然后通过ActiveMQ发送验证码到手机上。

为了方便起见,这里就固定验证码为123456,然后需要自己去实现一个登陆时候校验验证码的过程。

VerificationCodeFilter:

/**
 * 验证码验证过滤器
 * 
 * @author majie
 *
 */
@Component
public class VerificationCodeFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		/**
		 * 如果是手机登录就去验证验证码
		 */
		if (StringUtils.pathEquals("/login/mobile", request.getRequestURI().toString())
				&& request.getMethod().equalsIgnoreCase("post")) {
			
			String parameter = request.getParameter("smscode");
			
			if (!"123456".equals(parameter)) {
				throw new ValidateException("验证码错误");
			}
		}
		filterChain.doFilter(request, response);

	}

}

修改SecurityFilter,将上面的过滤器添加到UsernamePasswordAuthenticationFilter前面,代码略


配置手机认证的配置,使之前的那些关于手机个性化登录的配置连接起来

MobileAuthenticationSecurityConfig:

/**
 * 手机认证的配置
 * 
 * @author majie
 *
 */
@Component
public class MobileAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>{
	
	@Autowired
	private UserDetailsService userDetailsService;
	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		
		MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
		mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		
		MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
		mobileAuthenticationProvider.setUserDetailsService(userDetailsService);
		
		http.authenticationProvider(mobileAuthenticationProvider)
			.addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
	}
	
}

最后的登录页面:

login.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>标准登录页面</h2>
	<h3>表单登录</h3>
	<form action="/login/form" method="post">
		<table>
			<tr>
				<td>用户名:</td>
				<td><input type="text" name="username" value="user"></td>
			</tr>
			<tr>
				<td>密码:</td>
				<td><input type="password" name="password" value="123456"></td>
			</tr>
			<tr>
				<td colspan="2"><button type="submit">登录</button></td>
			</tr>
		</table>
	</form>

	<h3>手机登录</h3>
	<form action="/login/mobile" method="post">
		<table>
			<tr>
				<td>手机号码:</td>
				<td><input type="text" name="mobile" value="12345678900"></td>
			</tr>
			<tr>
				<td>短信验证码:</td>
				<td>
					<input type="text" name="smscode" value="123456">
				</td>
			</tr>
			<tr>
				<td colspan="2"><button type="submit">登录</button></td>
			</tr>
		</table>
	</form>
</body>

</html>


最后,记得配置登陆成功和登陆失败处理器。

/**
 * 认证成功的处理
 * 通常继承SavedRequestAwareAuthenticationSuccessHandler
 * @author majie
 *
 */
@Component
@Slf4j
public class SuccessAuthenticationHandler extends SavedRequestAwareAuthenticationSuccessHandler{

	@Autowired
	private ObjectMapper objectMapper;
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws ServletException, IOException {

		log.info("登录成功");
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(authentication));
	}
	
}

/**
 * 认证失败的处理 通常继承SimpleUrlAuthenticationFailureHandler
 * 
 * @author majie
 *
 */
@Component
@Slf4j
public class FailAuthenticationHandler extends SimpleUrlAuthenticationFailureHandler {
	
	@Autowired
	private ObjectMapper objectMapper;

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {

		log.info("登录失败");

		response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
	}

}

然后再MobileAuthenticationSecurityConfig类中注入上面两个处理器

		mobileAuthenticationFilter.setAuthenticationSuccessHandler(successAuthenticationHandler);
		mobileAuthenticationFilter.setAuthenticationFailureHandler(failAuthenticationHandler);

ok,最后启动项目测试。

源码地址:

https://gitee.com/mengcan/SpringSecurity.git

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值