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;
}
}
/**
* 手机登录过滤器
* 实现同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,最后启动项目测试。
源码地址: