上一篇文章我們講了Spring Security 如何實(shí)現(xiàn)OAuth2.0自定義登錄頁(yè)面 + JWT Token配置,但是在有些場(chǎng)景下,我們有可能需要支持多種登錄方式,如用戶名密碼登錄、手機(jī)驗(yàn)證碼登錄等等。
此時(shí)我們需要能夠?qū)崿F(xiàn)一個(gè)認(rèn)證流程同時(shí)支持多種認(rèn)證方式,基于Spring Security認(rèn)證的原理提到的Spring Security 的認(rèn)證流程的本質(zhì)上就是新增、刪除、修改過(guò)濾器。
本文在上一篇文章的基礎(chǔ)上繼續(xù)介紹如何通過(guò)自己添加Filter的方式實(shí)現(xiàn)支持多種方式自定義登錄認(rèn)證,只需要三個(gè)步驟(知道本質(zhì)后是不是并不覺(jué)得復(fù)雜 ):
- 添加
CustomAuthenticationProcessingFilter用于處理登錄請(qǐng)求 - 添加
CustomerAuthenticationProvider進(jìn)行實(shí)際認(rèn)證 - 將
CustomAuthenticationProcessingFilter和CustomerAuthenticationProvider配置到Spring Security框架中
代碼實(shí)現(xiàn)
1. 添加CustomAuthenticationProcessingFilter用于處理登錄請(qǐng)求
CustomAuthenticationProcessingFilter通常實(shí)現(xiàn)兩件事情:
- 設(shè)置我要處理的登錄請(qǐng)求Url和方法
- 將請(qǐng)求中的認(rèn)證信息保存起來(lái)以便
CustomerAuthenticationProvider處理認(rèn)證
下面代碼attemptAuthentication首先定義了登錄請(qǐng)求的url為/oauth/custom/token POST方法;然后將認(rèn)證的信息包括認(rèn)證的類型、用戶名和憑證生成到CustomAuthenticationToken中(框架會(huì)通過(guò)SecurityContextHolder將CustomAuthenticationToken保存以用于AuthenticationProvider的實(shí)際認(rèn)證),最后通過(guò) AuthenticationManager去調(diào)用 AuthenticationProvider去認(rèn)證。
setDetail主要是為了未來(lái)方便擴(kuò)展認(rèn)證請(qǐng)求里面的信息。
public class CustomAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static final String AUTH_TYPE = "auth_type";
private static final String USERNAME = "username";
private static final String CREDENTIALS = "credentials";
private static final String OAUTH_TOKEN_URL = "/oauth/custom/token";
private static final String HTTP_METHOD_POST = "POST";
public CustomAuthenticationProcessingFilter() {
super(new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (!HTTP_METHOD_POST.equals(request.getMethod().toUpperCase())){
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
AbstractAuthenticationToken authRequest = new CustomAuthenticationToken(
request.getParameter(AUTH_TYPE), request.getParameter(USERNAME), request.getParameter(CREDENTIALS),
request.getParameterMap(),new ArrayList<>()
);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
注意因?yàn)橐С植煌牡卿浄绞?,所以這里我們自己定義了CustomAuthenticationToken用于保存認(rèn)證信息,auth_type用于記錄登錄方式用戶名密碼登錄還是手機(jī)登錄或者其它方式, principal在手機(jī)登錄中就是手機(jī)號(hào)(前端傳的是username,filter保存到了principal中),credentials是驗(yàn)證碼,你也可以自己定義,只要能從前端請(qǐng)求中獲取到就可以。authParams用以保存其它參數(shù)信息,如果有的話。
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private String authType;
private Map<String,String[]> authParams;
private Object principal;
private Object credentials;
}
2. 添加CustomerAuthenticationProvider進(jìn)行實(shí)際認(rèn)證
CustomerAuthenticationProvider通常也是實(shí)現(xiàn)兩件事情:
- 根據(jù)認(rèn)證的類型(用戶名密碼還是驗(yàn)證碼或者其它方式)進(jìn)行認(rèn)證
- 認(rèn)證成功后加入你需要的用戶信息用于生成Token
public class CustomerAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if(authentication.getPrincipal() == null){
throw new BadCredentialsException("用戶名為空");
}
CustomAuthenticationToken customAuthenticationToken =(CustomAuthenticationToken) authentication;
// 頁(yè)面調(diào)用時(shí)傳遞auth_type參數(shù),如手機(jī)驗(yàn)證碼驗(yàn)證或者其它類型。根據(jù)不同的類型的auth type采用不同的驗(yàn)證方式驗(yàn)證是否登錄成功
if("mobile".equals(customAuthenticationToken.getAuthType())) {
// 手機(jī)認(rèn)證
} else if("password".equals(customAuthenticationToken.getAuthType())) {
// 用戶名和密碼
} else {
// 其它方式
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("internal::user"));
CustomAuthenticationToken authenticationToken = new CustomAuthenticationToken
(((CustomAuthenticationToken) authentication).getAuthType(), authentication.getPrincipal(), null,
null, authorities);
Map<String, Object> details = new HashMap<>(1);
details.put("name", customAuthenticationToken.getPrincipal());
authenticationToken.setDetails(details);
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return (CustomAuthenticationToken.class.isAssignableFrom(authentication));
}
}
3. 將CustomAuthenticationProcessingFilter和CustomerAuthenticationProvider配置到Spring Security框架中
首先配置CustomAuthenticationProcessingFilter在externalAuthenticationProcessingFilter之后然后配置加入CustomerAuthenticationProvider很簡(jiǎn)單
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UsernamePasswordUserDetailService usernamePasswordUserDetailService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 默認(rèn)支持./login實(shí)現(xiàn)authorization_code認(rèn)證
http
.formLogin().loginPage("/index.html").loginProcessingUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/index.html", "/login", "/resources/**", "/static/**").permitAll()
.anyRequest() // 任何請(qǐng)求
.authenticated()// 都需要身份認(rèn)證
.and()
.logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").logoutSuccessHandler(customLogoutSuccessHandler).permitAll()
.and()
.csrf().disable();
// 自定義認(rèn)證filter,支持./oauth/custom/token實(shí)現(xiàn)authorization_code認(rèn)證
http.addFilterAfter(externalAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(AuthenticationManagerBuilder auth) {
// 定義認(rèn)證的provider用于實(shí)現(xiàn)用戶名和密碼認(rèn)證
auth.authenticationProvider(new UsernamePasswordAuthenticationProvider(usernamePasswordUserDetailService));
// 自定義provider用于實(shí)現(xiàn)自定義的登錄認(rèn)證, 如不需要其它形式認(rèn)證如短信登錄,可刪除
auth.authenticationProvider(new CustomerAuthenticationProvider());
}
@Bean
public CustomAuthenticationProcessingFilter externalAuthenticationProcessingFilter() {
// 自定義認(rèn)證filter,需要實(shí)現(xiàn)CustomAuthenticationProcessingFilter和CustomerAuthenticationProvider
// filter將過(guò)濾url并把認(rèn)證信息塞入authentication作為CustomerAuthenticationProvider.authenticate的入?yún)? CustomAuthenticationProcessingFilter filter = new CustomAuthenticationProcessingFilter();
// 默認(rèn)自定義認(rèn)證方式grant_type為authorization_code方式,如果直接返回內(nèi)容,則需自定義success和fail handler
// filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
// filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
filter.setAuthenticationManager(authenticationManager);
return filter;
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
效果
因?yàn)樯婕暗角昂蠖说呐浜霞巴ㄟ^(guò)code換取token的轉(zhuǎn)換,本項(xiàng)目實(shí)現(xiàn)了一個(gè)前端項(xiàng)目直接使用Spring Security及其登錄頁(yè)面,登錄成功后跳回到前端首頁(yè),參考文末源碼awesome-admin中admin-ui。
-
登錄頁(yè)面, url為localhost:8000( 會(huì)自動(dòng)跳轉(zhuǎn)到auth服務(wù)的登錄頁(yè)面), 用戶名和密碼用上面代碼可以看到,對(duì)用戶名和密碼沒(méi)有實(shí)質(zhì)的檢驗(yàn)可以隨便輸,目前還沒(méi)有前端加上其它認(rèn)證方式。
登錄頁(yè)面 -
請(qǐng)求到/oauth/custom/token登錄后成功, JWT Token會(huì)保存在cookie中以便前端使用。
自定義認(rèn)證請(qǐng)求
面向Copy&Paste編程
- awesome-admin源碼
https://gitee.com/awesome-engineer/awesome-admin

