1. 程式人生 > >SpringSecurity(四):自定義登陸認證實現手機號登陸

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,最後啟動專案測試。

原始碼地址: