一. 前言
學(xué)習(xí)了SpringSecurity的使用抄淑,以及跟著源碼分析了一遍認(rèn)證流程,掌握了這個(gè)登錄認(rèn)證流程贺拣,才能更方便我們做自定義操作。
下面我們來(lái)學(xué)習(xí)下怎么實(shí)現(xiàn)多種登錄方式根吁,比如新增加一種郵箱驗(yàn)證碼登錄的形式,但SpringSecurity默認(rèn)的Usernamepassword方式不影響合蔽。
二. 自定義郵件驗(yàn)證碼認(rèn)證
0. 說(shuō)明
自定義一個(gè)郵箱驗(yàn)證碼的認(rèn)證击敌,將郵箱號(hào)碼作為key,驗(yàn)證碼作為value存放到Redis中緩存拴事。
1. 回顧
首先回顧下之前源碼分析的認(rèn)證流程沃斤,如下圖:
2. 設(shè)計(jì)思路
首先前端是填寫(xiě)郵箱,點(diǎn)擊獲取驗(yàn)證碼
輸入獲取到的驗(yàn)證碼刃宵,點(diǎn)擊登錄按鈕衡瓶,發(fā)送登錄接口(/emial/login,此處不能使用默認(rèn)的
/login
,因?yàn)槲覀儗儆跀U(kuò)展)自定義過(guò)濾器
EmailCodeAuthenticationFilter
(類(lèi)似UsernamepasswordAuthenticationFilter
),獲取郵箱號(hào)碼與驗(yàn)證碼將郵箱號(hào)碼與驗(yàn)證碼封裝為一個(gè)需要認(rèn)證的自定義
Authentication
對(duì)象EmailCodeAuthenticationToken
(類(lèi)似UsernamepasswordAuthenticationToken
)將
EmailCodeAuthenticationToken
傳給AuthenticationManager
接口的authenticate
方法認(rèn)證-
因?yàn)?code>AuthenticationManager的默認(rèn)實(shí)現(xiàn)類(lèi)為
ProviderManager
,而ProviderManager
又是委托給了AuthenticationProvider
牲证,因此自定義一個(gè)
AuthenticationProvider
接口的實(shí)現(xiàn)類(lèi)EmailCodeAuthenticationProvider
,實(shí)現(xiàn)authenticate
方法認(rèn)證 認(rèn)證成功與認(rèn)證失敗的處理:一種是直接在過(guò)濾器
EmailCodeAuthenticationFilter
中重寫(xiě)successfulAuthentication
和unsuccessfulAuthentication
哮针,另一種是實(shí)現(xiàn)AuthenticationSuccessHandler
和AuthenticationFailureHandler
進(jìn)行處理總歸一句:照貓畫(huà)瓢
總結(jié):
需要實(shí)現(xiàn)以下幾個(gè)類(lèi):
- 過(guò)濾器EmailCodeAuthenticationFilter
- Authentication對(duì)象EmailCodeAuthenticationToken
- AuthenticationProvider類(lèi)EmailCodeAuthenticationProvider
- 自定義認(rèn)證成功與認(rèn)證失敗的Handler
3. 代碼實(shí)現(xiàn)
-
自定義Authentication對(duì)象(這里是EmailCodeAuthenticationToken)
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; // 郵箱賬號(hào) private final Object principal; // 郵箱驗(yàn)證碼 private Object credentials; /** * 沒(méi)有經(jīng)過(guò)驗(yàn)證時(shí),權(quán)限位空坦袍,setAuthenticated設(shè)置為不可信令牌 * @param principal * @param credentials */ public EmailCodeAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /** * 已認(rèn)證后十厢,將權(quán)限加上,setAuthenticated設(shè)置為可信令牌 * @param principal * @param credentials * @param authorities */ public EmailCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
說(shuō)明:
模仿UsernamepasswordAuthenticationToken定義捂齐,繼承AbstractAuthenticationToken蛮放,這里注意的是要定義兩個(gè)構(gòu)造器,分別對(duì)應(yīng)未認(rèn)證和已認(rèn)證的Token辛燥,已認(rèn)證的調(diào)用
super.setAuthenticated(true);
-
自定義Filter(這里是EmailCodeAuthenticationFilter)
public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // 前端傳來(lái)的參數(shù)名 private final String SPRING_SECURITY_EMAIL_KEY = "email"; private final String SPRING_SECURITY_EMAIL_CODE_KEY = "email_code"; // 自定義的路徑匹配器筛武,攔截Url為:/email/login private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/email/login", "POST"); // 是否僅POST方式 private boolean postOnly = true; public EmailCodeAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } /** * 認(rèn)證方法,在父類(lèi)的doFilter中調(diào)用 * @param request * @param response * @return * @throws AuthenticationException * @throws IOException * @throws ServletException */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not support : " + request.getMethod()); } System.out.println("email attemptAuthentication"); // 獲取郵箱號(hào)碼 String email = obtainEmail(request); email = (email != null) ? email : ""; email = email.trim(); // 獲取郵箱驗(yàn)證碼 String emailCode = obtainEmailCode(request); emailCode = (emailCode != null) ? emailCode : ""; // 構(gòu)造Token EmailCodeAuthenticationToken authRequest = new EmailCodeAuthenticationToken(email, emailCode); setDetails(request, authRequest); // 使用AuthenticationManager來(lái)進(jìn)行認(rèn)證 return this.getAuthenticationManager().authenticate(authRequest); } /** * 獲取請(qǐng)求中email參數(shù) * @param request * @return */ @Nullable protected String obtainEmail(HttpServletRequest request) { return request.getParameter(this.SPRING_SECURITY_EMAIL_KEY); } /** * 獲取請(qǐng)求中驗(yàn)證碼參數(shù)email_code * @param request * @return */ @Nullable protected String obtainEmailCode(HttpServletRequest request) { return request.getParameter(this.SPRING_SECURITY_EMAIL_CODE_KEY); } protected void setDetails(HttpServletRequest request, EmailCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } }
說(shuō)明:
模仿UsernamepasswordAuthentionFilter實(shí)現(xiàn)自定義的過(guò)濾器挎塌,核心是attemptAuthentication方法.
-
自定義AuthenticationProvider(這里是EmailCodeAuthenticationProvider)
public class EmailCodeAuthenticationProvider implements AuthenticationProvider { protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private EmailCodeUserDetailsService emailCodeUserDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(EmailCodeAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // 此時(shí)的authentication還沒(méi)認(rèn)證徘六,獲取郵箱號(hào)碼 EmailCodeAuthenticationToken unAuthenticationToken = (EmailCodeAuthenticationToken) authentication; // 做校驗(yàn) UserDetails user = this.emailCodeUserDetailsService.loadUserByEmail(unAuthenticationToken); if (user == null) { throw new InternalAuthenticationServiceException("EmailCodeUserDetailsService returned null, which is an interface contract violation"); } System.out.println("authentication successful!"); Object principalToReturn = user; return createSuccessAuthentication(principalToReturn, authentication, user); } @Override public boolean supports(Class<?> authentication) { return EmailCodeAuthenticationToken.class.isAssignableFrom(authentication); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { EmailCodeAuthenticationToken result = new EmailCodeAuthenticationToken(principal, authentication.getCredentials(), user.getAuthorities()); result.setDetails(authentication.getDetails()); return result; } public void setEmailCodeUserDetailsService(EmailCodeUserDetailsService emailCodeUserDetailsService) { this.emailCodeUserDetailsService = emailCodeUserDetailsService; } }
說(shuō)明:
Provider是真正做認(rèn)證的地方,這里調(diào)用emailCodeUserDetailsService服務(wù)去執(zhí)行驗(yàn)證榴都,因?yàn)橐玫竭@個(gè)Service待锈,所以提供了一個(gè)set方法setEmailCodeUserDetailsService用于注入。這里的這個(gè)service是我們自定義的嘴高,可以不用實(shí)現(xiàn)UserDetailsService竿音, Service里的邏輯可以自定義
-
自定義認(rèn)證成功與失敗的Handler
public class EmailCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("text/plain;charset=UTF-8"); response.getWriter().write(authentication.getName()); } } public class EmailCodeAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("text/plain;charset=UTF-8"); response.getWriter().write("郵箱驗(yàn)證碼錯(cuò)誤!"); } }
說(shuō)明:
這里的是認(rèn)證成功或失敗后的處理,需要實(shí)現(xiàn)對(duì)應(yīng)的接口以及方法拴驮。這里的邏輯只是簡(jiǎn)單測(cè)試春瞬,具體邏輯以后根據(jù)業(yè)務(wù)邏輯去編寫(xiě)。
-
添加自定義認(rèn)證的配置
為了讓我們自定義的認(rèn)證生效套啤,需要將我們的Filter和Provider加入到SpringSecurity的配置中宽气。這里我們使用
apply
這個(gè)方法將其他一些配置合并到SpringSecurity的配置中,形成插件化。比如:httpSecurity.apply(new xxxxConfig());
因此我們可以將我們的配置單獨(dú)放到一個(gè)配置類(lèi)中萄涯。
public class EmailCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { // 注入email驗(yàn)證服務(wù) @Autowired private EmailCodeUserDetailsService emailCodeUserDetailsService; @Override public void configure(HttpSecurity http) { // 配置Filter EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter(); // 設(shè)置AuthenticationManager emailCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // 設(shè)置認(rèn)證成功處理Handler emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(new EmailCodeAuthenticationSuccessHandler()); // 設(shè)置認(rèn)證失敗處理Handler emailCodeAuthenticationFilter.setAuthenticationFailureHandler(new EmailCodeAuthenticationFailureHandler()); // 配置Provider EmailCodeAuthenticationProvider emailCodeAuthenticationProvider = new EmailCodeAuthenticationProvider(); // 設(shè)置email驗(yàn)證服務(wù) emailCodeAuthenticationProvider.setEmailCodeUserDetailsService(emailCodeUserDetailsService); // 將過(guò)濾器添加到過(guò)濾器鏈路中 http.authenticationProvider(emailCodeAuthenticationProvider).addFilterAfter(emailCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
注意:
這里需要注意的是绪氛,一定要將
AuthenticationManager
提供給Filter,如果沒(méi)有這一步涝影,那么在Filter中進(jìn)行認(rèn)證的時(shí)候無(wú)法找到對(duì)應(yīng)的Provider枣察,因?yàn)锳uthenticationManger就是管理Provider的。
http.getSharedObject(AuthenticationManager.class)
解釋?zhuān)?/p>SharedObject
是在配置中進(jìn)行共享的一些對(duì)象燃逻,HttpSecurity共享了一些非常有用的對(duì)象可以供外部使用序目,比如AuthenticationManager
最后在SpringSecurity的主配置中加入我們的自定義配置:
@Configuration public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private EmailCodeAuthenticationSecurityConfig emailCodeAuthenticationSecurityConfig; @Autowired private DefaultUserDetailsService defaultUserDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(defaultUserDetailsService); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/getEmailCode", "/**/*.html"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/login") .permitAll() .and() .logout() .logoutUrl("/logout") .and() .apply(emailCodeAuthenticationSecurityConfig) .and() .csrf() .disable(); } }
說(shuō)明:
因?yàn)檫@里使用了數(shù)據(jù)庫(kù)保存用戶(hù)信息,所以在SpringSecurity的默認(rèn)表單登錄里唆樊,修改了UserDetailService宛琅,在這里進(jìn)行校驗(yàn),所以在主配置中要設(shè)置UserDetailService:
auth.userDetailsService(defaultUserDetailsService);
-
其他一些文件
查看我上傳的gitee源碼吧逗旁,整個(gè)工程都上傳了。
-
前端頁(yè)面實(shí)現(xiàn)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登錄</title> <!-- 最新版本的 Bootstrap 核心 CSS 文件 --> <link rel="stylesheet" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous"> <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script> <!-- 最新的 Bootstrap 核心 JavaScript 文件 --> <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script> <style> body { background-color: gray; } .login-div { width: 400px; /* height: 200px; */ margin: 0 auto; margin-top: 200px; border: 1px solid black; padding: 10px; } </style> </head> <body> <div class="login-div"> <ul class="nav nav-tabs" role="tablist"> <li class="active"> <a href="#usernameLogin" data-toggle="tab">用戶(hù)名登錄</a> </li> <li> <a href="#emailLogin" data-toggle="tab">郵箱驗(yàn)證碼登錄</a> </li> </ul> <!-- 用戶(hù)名登錄 --> <div class="tab-content"> <div class="tab-pane active" id="usernameLogin"> <form action="/login" method="POST"> <div class="form-group"> <label>用戶(hù)名</label> <input type="text" class="form-control" placeholder="Username" name="username"> </div> <div class="form-group"> <label>密碼</label> <input type="password" class="form-control" placeholder="Password" name="password"> </div> <div class="checkbox"> <label> <input type="checkbox" name="rememberType"> 記住我 </label> </div> <button type="submit" class="btn btn-default">登錄</button> </form> </div> <!-- 郵箱登錄 --> <div class="tab-pane" id="emailLogin"> <form action="/email/login" method="POST"> <div class="form-group" > <label>郵箱地址</label> <input type="email" class="form-control" placeholder="Email" name="email" id="email"> </div> <div class="form-group"> <label>驗(yàn)證碼</label> <input type="text" class="form-control" placeholder="Code" name="email_code"> </div> <div class="form-group"> <label> <button type="button" class="btn btn-default" id="getCode">獲取驗(yàn)證碼</button> <span id="showCode" style="margin-left: 20px;"></span> </label> </div> <button type="submit" class="btn btn-default">登錄</button> </form> </div> </div> </div> <script> $('#nav a').on('click', function(e) { e.preventDefault(); $(this).tab('show'); }); $('#getCode').on('click', function() { $.ajax({ type: "GET", url: "/getEmailCode", data: { email: $('#email').val() }, // dataType: "dataType", success: function (response) { $('#showCode').text(response); } }); }); </script> </body> </html>
說(shuō)明:
前端頁(yè)面只是簡(jiǎn)單的顯示使用兩種方式來(lái)登錄的操作舆瘪,一些輸入校驗(yàn)什么的沒(méi)有詳細(xì)實(shí)現(xiàn)片效,所以這里默認(rèn)各位大佬都是正常操作哈。
這個(gè)前端支持兩種登錄方式英古,用戶(hù)名密碼登錄方式使用的SpringSecurity默認(rèn)的UsernamepasswordAuthenticationFilter淀衣,郵箱驗(yàn)證碼使用的是自定義的EmailCodeAuthenticationFilter,在郵箱登錄頁(yè)面召调,點(diǎn)擊獲取驗(yàn)證碼按鈕膨桥,會(huì)請(qǐng)求服務(wù)器獲取一個(gè)隨機(jī)的字符串作為驗(yàn)證碼,并且存入Redis中唠叛,有效期60s(記住我功能在這里沒(méi)有實(shí)現(xiàn))
demo-emailLogin.jpg
demo-usernameLogin.jpg -
數(shù)據(jù)庫(kù)操作
因?yàn)槟壳爸皇亲远x認(rèn)證只嚣,不涉及授權(quán),所以只有一個(gè)用戶(hù)表
CREATE TABLE `user` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `username` VARCHAR(32) DEFAULT NULL, `password` VARCHAR(255) DEFAULT NULL, `email` VARCHAR(255) DEFAULT NULL, `enabled` TINYINT(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8; INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq','123456@qq.com', '1');
隨便插入一個(gè)用戶(hù)艺沼,密碼是123册舞,數(shù)據(jù)庫(kù)的是經(jīng)過(guò)加密的。