有的时候,我们需要为用户提供多种认证方式。如:用户名密码登录、手术号验证码登录。下面实现Spring Security支持这两种登录方式。
增加Token
Spring Security默认使用UsernamePasswordAuthenticationToken包装登录请求的信息,Token继承之AbstractAuthenticationToken。Spring Security将信息封装成Token交给Provider处理。
这里增加一个MobileCodeAuthenticationToken类,继承之AbstractAuthenticationToken。用于封装手机号验证码的请求参数。后面会有相应的Provider处理这个Token。
创建包com.biboheart.demos.security.tokens,在包下创建类MobileCodeAuthenticationToken。可以查看UsernamePasswordAuthenticationToken类源码,参考它完成类的代码,内容如下:
package com.biboheart.demos.security.tokens;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
public class MobileCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private String credentials;
public MobileCodeAuthenticationToken(Object principal, String credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public MobileCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public String 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");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
Provider
Provider实现AuthenticationProvider接口,它执行身份认证工作。前面用的是Spring Security默认的Provider进行认证,我们没有控制认证过程。在这里我们实现两个Provider,UsernamePasswordAuthenticationProvider用于替换系统默认的用户名密码认证业务,MobileCodeAuthenticationProvider用于执行手机号验证码认证业务。这两个类创建在包com.biboheart.demos.security.provider下。实现接口AuthenticationProvider,其中Authentication authenticate函数用于执行认证,supports函数用于筛选Token,如果在这里返回true,所有Token都会认证。
package com.biboheart.demos.security.provider;
import com.biboheart.brick.utils.CheckUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.HashSet;
import java.util.Set;
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
String password = (String) authentication.getCredentials();
// 认证用户名
if (!"user".equals(username) && !"admin".equals(username)) {
throw new BadCredentialsException("用户不存在");
}
// 认证密码,暂时不加密
if ("user".equals(username) && !"123".equals(password) || "admin".equals(username) && !"admin".equals(password)) {
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(username,
authentication.getCredentials(), listUserGrantedAuthorities(username));
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private Set<GrantedAuthority> listUserGrantedAuthorities(String username) {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
if (CheckUtils.isEmpty(username)) {
return authorities;
}
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
if ("admin".equals(username)) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
return authorities;
}
}
MobileCodeAuthenticationProvider
package com.biboheart.demos.security.provider;
import com.biboheart.brick.utils.CheckUtils;
import com.biboheart.demos.security.tokens.MobileCodeAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.HashSet;
import java.util.Set;
public class MobileCodeAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
String code = (String) authentication.getCredentials();
if (CheckUtils.isEmpty(code)) {
throw new BadCredentialsException("验证码不能为空");
}
if (!"13999990000".equals(mobile)) {
throw new BadCredentialsException("用户不存在");
}
// 手机号验证码业务还没有开发,先用4个0验证
if (!code.equals("0000")) {
throw new BadCredentialsException("验证码不正确");
}
MobileCodeAuthenticationToken result = new MobileCodeAuthenticationToken(mobile,
listUserGrantedAuthorities(mobile));
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return (MobileCodeAuthenticationToken.class.isAssignableFrom(authentication));
}
private Set<GrantedAuthority> listUserGrantedAuthorities(String username) {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
if (CheckUtils.isEmpty(username)) {
return authorities;
}
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return authorities;
}
}
完成Provider后,要将两个Provider加入配置中,使它们加入工作。修改SecurityConfiguration配置文件。首先实例化这两个Provider,然后将两Bean添加到configure(AuthenticationManagerBuilder auth)
package com.biboheart.demos.security;
import com.biboheart.demos.security.provider.MobileCodeAuthenticationProvider;
import com.biboheart.demos.security.provider.UsernamePasswordAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 创建内存用户
/*auth.inMemoryAuthentication()
.withUser("user").password(passwordEncoder.encode("123")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder.encode("admin")).roles("USER", "ADMIN");*/
auth
.authenticationProvider(usernamePasswordAuthenticationProvider())
.authenticationProvider(mobileCodeAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll() // 这三个目录不做安全控制
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")// 自定义的登录页面
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/");
}
@Bean
public UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider() {
return new UsernamePasswordAuthenticationProvider();
}
@Bean
public MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {
return new MobileCodeAuthenticationProvider();
}
// spring security 必须有一个passwordEncoder
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
增加过滤器
在这个时候,UsernamePasswordAuthenticationProvider已经起作用了。因为Spring Security用默认有一个UsernamePasswordAuthenticationFilter过滤器过滤login,在过滤器中会创建UsernamePasswordAuthenticationToken对象,UsernamePasswordAuthenticationProvider能够得到Token进行处理。虽然MobileCodeAuthenticationProvider已经在认证队列中,但是MobileCodeAuthenticationProvider是不会执行认证工作。MobileCodeAuthenticationToken是自定义的,没有地方生成它的实例,return (MobileCodeAuthenticationToken.class.isAssignableFrom(authentication));执行完成这名后就漂过了。
参考Spring Security UsernamePasswordAuthenticationToken的认证方式,我们也在UsernamePasswordAuthenticationFilter之前加一个过滤器,用户判断MobileCodeAuthenticationToken认证方式。可以用指定的参数,或者指定的URL,这里用的是URL判断,提供“/mobileCodeLogin”为手机号验证码登录的URL。这个Filter参考用户名密码的Filter实现,名称为MobileCodeAuthenticationFilter,从AbstractAuthenticationProcessingFilter继承。接收两个参数分别为“mobile”和“code”。如果比较下,会发现与UsernamePasswordAuthenticationFilter非常像。代码如下:
package com.biboheart.demos.filter;
import com.biboheart.demos.security.tokens.MobileCodeAuthenticationToken;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MobileCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
public static final String SPRING_SECURITY_FORM_CODE_KEY = "code";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY;
private boolean postOnly = true;
public MobileCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/mobileCodeLogin", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
String code = obtainCode(request);
if (mobile == null) {
mobile = "";
}
if (code == null) {
code = "";
}
mobile = mobile.trim();
code = code.trim();
AbstractAuthenticationToken authRequest = new MobileCodeAuthenticationToken(mobile, code);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected String obtainCode(HttpServletRequest request) {
return request.getParameter(codeParameter);
}
protected void setDetails(HttpServletRequest request,
AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
完成Filter实现后,需要将它加入到Filter序列中。加入方法是在SecurityConfiguration文件中,实例化Filter,然后在configure(HttpSecurity http)配置下加入http.addFilterBefore(mobileCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);即在UsernamePasswordAuthenticationFilter之前加入一个过滤器。记得将“/mobileCodeLogin”添加到允许通得中。修改后的SecurityConfiguration如下:
package com.biboheart.demos.security;
import com.biboheart.demos.filter.MobileCodeAuthenticationFilter;
import com.biboheart.demos.security.provider.MobileCodeAuthenticationProvider;
import com.biboheart.demos.security.provider.UsernamePasswordAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 创建内存用户
/*auth.inMemoryAuthentication()
.withUser("user").password(passwordEncoder.encode("123")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder.encode("admin")).roles("USER", "ADMIN");*/
auth
.authenticationProvider(usernamePasswordAuthenticationProvider())
.authenticationProvider(mobileCodeAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.antMatchers("/", "/home", "/mobileCodeLogin").permitAll() // 这三个目录不做安全控制
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")// 自定义的登录页面
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/");
http.addFilterBefore(mobileCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// @formatter:on
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public MobileCodeAuthenticationFilter mobileCodeAuthenticationFilter() {
MobileCodeAuthenticationFilter filter = new MobileCodeAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
@Bean
public UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider() {
return new UsernamePasswordAuthenticationProvider();
}
@Bean
public MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {
return new MobileCodeAuthenticationProvider();
}
// spring security 必须有一个passwordEncoder
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
修改界面
在登录界面中加入手机号验证码登录方式,试下效果。手机号和验证码在写在代码中的,分别是13999990000和0000。登录界面修改成:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Security Example</title>
</head>
<body>
密码登录:
<hr/>
<form th:action="@{/login}" method="post">
<div>
<label> 用户名: <input type="text" name="username" />
</label>
</div>
<div>
<label> 密码: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="登录" />
</div>
</form>
<hr/>
验证码登录:
<hr/>
<form th:action="@{/mobileCodeLogin}" method="post">
<div>
<label> 手机号: <input type="text" name="mobile" />
</label>
</div>
<div>
<label> 验证码: <input type="password" name="code" />
</label>
</div>
<div>
<input type="submit" value="登录" />
</div>
</form>
</body>
</html>
完成开发
如果需要更多的认证方式,同手机号验证码。步骤如下:
- 创建Token,继承之AbstractAuthenticationToken
- 创建Provider,实现AuthenticationProvider
- 创建Filter,继承之AbstractAuthenticationProcessingFilter
- 在配置类中实例化Filter和Provider
- 在Filter中处理请求包装Token
- Provider实例加入到auth.authenticationProvider
- 使用http.addFilterBefore在UsernamePasswordAuthenticationFilter之前加入Filter
启动服务,访问界面。使用流程与之前相同。区别是登录界面多了验证码登录表单,输入13999990000,验证码0000后也可以成功登录。
此时,目录结构如下图。
源码地址:https://gitee.com/biboheart/bh-springboot-demos.git