SpringSecurity(四):自定義登陸認證實現手機號登陸
阿新 • • 發佈:2018-12-31
SpringSecurity預設提供了兩種登陸,一種basic登陸一種表單登陸(分別在一三章有講到),但是如果我們要實現其他方式的登陸(例如郵箱登陸,手機號登陸)又該怎麼做呢?
第二章中講到了Security的登入原來,以及最後給出的流程圖,結合它們這章來實現自定義登陸認證
1.MobileAuthenticationToken
2.MobileAuthenticationFilter/** * 手機登入認證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; } }
3.MobileAuthenticationProvider/** * 手機登入過濾器 * 實現同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; } }
第二章講過,只有一個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,最後啟動專案測試。
原始碼地址: