有的時(shí)候,我們需要為用戶提供多種認(rèn)證方式。如:用戶名密碼登錄功蜓、手術(shù)號(hào)驗(yàn)證碼登錄园爷。下面實(shí)現(xiàn)Spring Security支持這兩種登錄方式。
增加Token
Spring Security默認(rèn)使用UsernamePasswordAuthenticationToken包裝登錄請(qǐng)求的信息式撼,Token繼承之AbstractAuthenticationToken童社。Spring Security將信息封裝成Token交給Provider處理。
這里增加一個(gè)MobileCodeAuthenticationToken類著隆,繼承之AbstractAuthenticationToken叠洗。用于封裝手機(jī)號(hào)驗(yàn)證碼的請(qǐng)求參數(shù)。后面會(huì)有相應(yīng)的Provider處理這個(gè)Token旅东。
創(chuàng)建包c(diǎn)om.biboheart.demos.security.tokens灭抑,在包下創(chuàng)建類MobileCodeAuthenticationToken〉执可以查看UsernamePasswordAuthenticationToken類源碼腾节,參考它完成類的代碼,內(nèi)容如下:
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實(shí)現(xiàn)AuthenticationProvider接口荤牍,它執(zhí)行身份認(rèn)證工作案腺。前面用的是Spring Security默認(rèn)的Provider進(jìn)行認(rèn)證,我們沒(méi)有控制認(rèn)證過(guò)程康吵。在這里我們實(shí)現(xiàn)兩個(gè)Provider劈榨,UsernamePasswordAuthenticationProvider用于替換系統(tǒng)默認(rèn)的用戶名密碼認(rèn)證業(yè)務(wù),MobileCodeAuthenticationProvider用于執(zhí)行手機(jī)號(hào)驗(yàn)證碼認(rèn)證業(yè)務(wù)晦嵌。這兩個(gè)類創(chuàng)建在包c(diǎn)om.biboheart.demos.security.provider下同辣。實(shí)現(xiàn)接口AuthenticationProvider,其中Authentication authenticate函數(shù)用于執(zhí)行認(rèn)證惭载,supports函數(shù)用于篩選Token旱函,如果在這里返回true,所有Token都會(huì)認(rèn)證描滔。
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();
// 認(rèn)證用戶名
if (!"user".equals(username) && !"admin".equals(username)) {
throw new BadCredentialsException("用戶不存在");
}
// 認(rèn)證密碼棒妨,暫時(shí)不加密
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("驗(yàn)證碼不能為空");
}
if (!"13999990000".equals(mobile)) {
throw new BadCredentialsException("用戶不存在");
}
// 手機(jī)號(hào)驗(yàn)證碼業(yè)務(wù)還沒(méi)有開(kāi)發(fā),先用4個(gè)0驗(yàn)證
if (!code.equals("0000")) {
throw new BadCredentialsException("驗(yàn)證碼不正確");
}
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后含长,要將兩個(gè)Provider加入配置中券腔,使它們加入工作。修改SecurityConfiguration配置文件拘泞。首先實(shí)例化這兩個(gè)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 {
// 創(chuàng)建內(nèi)存用戶
/*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() // 這三個(gè)目錄不做安全控制
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")// 自定義的登錄頁(yè)面
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/");
}
@Bean
public UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider() {
return new UsernamePasswordAuthenticationProvider();
}
@Bean
public MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {
return new MobileCodeAuthenticationProvider();
}
// spring security 必須有一個(gè)passwordEncoder
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
增加過(guò)濾器
在這個(gè)時(shí)候,UsernamePasswordAuthenticationProvider已經(jīng)起作用了田弥。因?yàn)镾pring Security用默認(rèn)有一個(gè)UsernamePasswordAuthenticationFilter過(guò)濾器過(guò)濾login涛酗,在過(guò)濾器中會(huì)創(chuàng)建UsernamePasswordAuthenticationToken對(duì)象,UsernamePasswordAuthenticationProvider能夠得到Token進(jìn)行處理偷厦。雖然MobileCodeAuthenticationProvider已經(jīng)在認(rèn)證隊(duì)列中商叹,但是MobileCodeAuthenticationProvider是不會(huì)執(zhí)行認(rèn)證工作。MobileCodeAuthenticationToken是自定義的只泼,沒(méi)有地方生成它的實(shí)例剖笙,return (MobileCodeAuthenticationToken.class.isAssignableFrom(authentication));執(zhí)行完成這名后就漂過(guò)了。
參考Spring Security UsernamePasswordAuthenticationToken的認(rèn)證方式请唱,我們也在UsernamePasswordAuthenticationFilter之前加一個(gè)過(guò)濾器弥咪,用戶判斷MobileCodeAuthenticationToken認(rèn)證方式∈螅可以用指定的參數(shù)聚至,或者指定的URL,這里用的是URL判斷本橙,提供“/mobileCodeLogin”為手機(jī)號(hào)驗(yàn)證碼登錄的URL扳躬。這個(gè)Filter參考用戶名密碼的Filter實(shí)現(xiàn),名稱為MobileCodeAuthenticationFilter甚亭,從AbstractAuthenticationProcessingFilter繼承贷币。接收兩個(gè)參數(shù)分別為“mobile”和“code”。如果比較下亏狰,會(huì)發(fā)現(xiàn)與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實(shí)現(xiàn)后,需要將它加入到Filter序列中暇唾。加入方法是在SecurityConfiguration文件中促脉,實(shí)例化Filter,然后在configure(HttpSecurity http)配置下加入http.addFilterBefore(mobileCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);即在UsernamePasswordAuthenticationFilter之前加入一個(gè)過(guò)濾器策州。記得將“/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 {
// 創(chuàng)建內(nèi)存用戶
/*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() // 這三個(gè)目錄不做安全控制
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")// 自定義的登錄頁(yè)面
.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 必須有一個(gè)passwordEncoder
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
修改界面
在登錄界面中加入手機(jī)號(hào)驗(yàn)證碼登錄方式,試下效果抽活。手機(jī)號(hào)和驗(yàn)證碼在寫(xiě)在代碼中的硫戈,分別是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/>
驗(yàn)證碼登錄:
<hr/>
<form th:action="@{/mobileCodeLogin}" method="post">
<div>
<label> 手機(jī)號(hào): <input type="text" name="mobile" />
</label>
</div>
<div>
<label> 驗(yàn)證碼: <input type="password" name="code" />
</label>
</div>
<div>
<input type="submit" value="登錄" />
</div>
</form>
</body>
</html>
完成開(kāi)發(fā)
如果需要更多的認(rèn)證方式下硕,同手機(jī)號(hào)驗(yàn)證碼丁逝。步驟如下:
- 創(chuàng)建Token,繼承之AbstractAuthenticationToken
- 創(chuàng)建Provider梭姓,實(shí)現(xiàn)AuthenticationProvider
- 創(chuàng)建Filter霜幼,繼承之AbstractAuthenticationProcessingFilter
- 在配置類中實(shí)例化Filter和Provider
- 在Filter中處理請(qǐng)求包裝Token
- Provider實(shí)例加入到auth.authenticationProvider
- 使用http.addFilterBefore在UsernamePasswordAuthenticationFilter之前加入Filter
啟動(dòng)服務(wù),訪問(wèn)界面誉尖。使用流程與之前相同罪既。區(qū)別是登錄界面多了驗(yàn)證碼登錄表單,輸入13999990000,驗(yàn)證碼0000后也可以成功登錄琢感。
登錄界面
此時(shí)丢间,目錄結(jié)構(gòu)如下圖。
目錄結(jié)構(gòu)
源碼地址:https://gitee.com/biboheart/bh-springboot-demos.git