距離上一次更新,不知不覺已經過去了半個月了,人真的是不能放鬆,一放鬆就肆意妄爲了。希望這個月內可以把 SpringSecurity 系列更新完畢吧,加油!。
OK,言歸正傳上一章我們利用 SpringSecurity 提供的一些可選配置,實現了自定義表單登錄。但是在我們的日常需求中,僅僅是表單登錄時滿足不了的。所以這一章,我給大家帶來 SpringSecurity 下自定義登錄方式的示例。
首先我們選定我們的自定義登錄方式,這裏我們選擇手機短信登錄。顯而易見,SpringSecurity 並沒有給我們提供手機短信登錄的簡單配置集成方式,所以需要我們自己來進行實現。
我們先來分析一波,手機短信登錄我們可以分爲兩個部分:
- 手機驗證碼校驗
- 手機驗證碼校驗應該是一個複用模塊,因爲不光登錄可能會用到,註冊、綁定等很多場景也都可能用到,並且這一塊和 SpringSecurity 關係不大,我們放到後面,將其專門開發成一個 Lib。
- 手機號登錄
- 通過了手機驗證碼校驗,其實就是一個手機號登錄了,按用戶的手機號去數據庫查詢。所以我們現在主要完成第二塊,手機號登錄。
要自定義手機號登錄,我們這裏必須分析一下 SpringSecurity 的認證流程,具體源碼在後面的章節我會帶着大家去詳細看一下,這裏我們先來找一下 SpringSecurity 的認證流程,我們前面的章節已經可以使用表單登錄了,那麼我們就以表單登錄的方式來跟蹤一下原發,分析出認證流程,我們會以一下我們之前做了那些事:
- 我們指明瞭登錄方式爲 formLogin
- 我們通過設置配置,自定義認證路徑
- 我們自定義了 UserDetailService 從數據庫中查詢用戶信息
- 我們自定義了認證成功或失敗處理器
然後我們來猜測一下可能的認證流程
-
用戶發起認證請求,服務端從請求中取出參數
-
去數據庫按參數進行查詢,然後進行校驗
-
最後做認證結果處理。
我們從 IDEA 點擊查看 formLogin
方法
HttpSecurity 類
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
// 發現這裏 new 了一個 FormLoginConfigurer
return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer());
}
繼續點擊 FormLoginConfigurer
類進行查看,發現在 FormLoginConfigurer
的構造函數中創建了一個 UsernamePasswordAuthenticationFilter
,並且設置了表單登錄的參數名。
FormLoginConfigurer 類
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), (String)null);
this.usernameParameter("username");
this.passwordParameter("password");
}
我們繼續進入 UsernamePasswordAuthenticationFilter
,我們發現這是一個過濾器,其次在它的構造函數裏面指定了 /login 和 Post。(結合之前我們配置時說的,默認登錄地址是 login + post),我們猜測這裏是設置了攔截的 Url 和 Method,那麼這個 Filter 應該就是認證的入口
UsernamePasswordAuthenticationFilter 類
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
我們繼續看 Filter 的 處理方法,可以出在這個方法裏面,從請求中取出了表單參數,並且將參數封裝到了 UsernamePasswordAuthenticationToken
中,最後使用getAuthenticationManager().authenticate(authRequest);
進行認證,getAuthenticationManager
獲取到的是一個 AuthenticationManager
對象,實際上是使用 AuthenticationManager
的 authenticate
方法進行認證。
UsernamePasswordAuthenticationFilter 類
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
我們繼續進入 authenticate
方法,發現其是一個接口方法,有很多實現類。沒辦法,我們只好將應用啓動,進行 debug 斷點跟蹤。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
經過斷點跟蹤,我們發現實際上調用的是 ProviderManager
的 authenticate
方法,我們發現在該方法中,獲取所有的 Providers
,然後遍歷,找出與封裝的 Token
匹配的 Provider
,調用其 authenticate
方法。
ProviderManager 類
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 獲取之前封裝的 Token 類型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
// 獲取所有 providers,遍歷之
for (AuthenticationProvider provider : getProviders()) {
// 判斷 Provider 是否支持封裝的 Token
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 調用 Provider 的認證方法
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
// 認證沒有獲取到結果,使用 parent 進行認證
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
// 認證完畢後,調用 Token 的方法擦除掉敏感信息(eg: password...)
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// 認證通過,發佈認證成功消息
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
我們繼續追蹤 Provider
的 authenticate
方法,進入AbstractUserDetailsAuthenticationProvider
的 authenticate
方法,我們重點關注一下 retrieveUser
方法。
AbstractUserDetailsAuthenticationProvider 類
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 從緩存中嘗試獲取用戶信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 緩存沒獲取到,使用封裝的 Token 嘗試獲取用戶信息
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// UserDetail 支持設置賬戶凍結、啓用等四個狀態,這裏是對賬戶狀態進行校驗
preAuthenticationChecks.check(user);
// 進行密碼校驗,之前如果使用了 passwordEncoder 對密碼進行加密,那麼從數據庫中取出來的應該是加
//密過的密碼,這裏會對參數中的明文密碼與數據庫密碼進行校驗
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 將獲取到的用戶信息封裝成一個 Authentication 返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
我們發現 retrieveUser
方法是一個抽象方法,具體實現應該在子類中,繼續追蹤,發現實現在 DaoAuthenticationProvider
中,在 retrieveUser
方法中調用 UserDetailService
的 loadUserByUsername
方法,到了這裏,大概的流程我們基本上就知道了。PS: UserDetailService 在系統中有多個實現,這裏會使用哪個要看實際情況與設置,這個後面有機會說一下。
AbstractUserDetailAuthenticationProvider 類
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
DaoAuthenticationProcider 類
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
// 調用 UserDetailService 的 loadUserByUsername 方法
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
我們來總結一下整個認證流程,通過過濾器獲取到請求參數,封裝 Token
,調用 ProviderManager
的 authenticate
方法,該方法實際上調用的 ProviderManager
管理的 Provider
(認證邏輯的實現類) 的 authenticate
方法,最後調用 UserDetailService
去獲取用戶的信息。
- 首先通過 Filter 攔截用戶請求,獲取到參數
- 將參數封裝成 Token
- 調用 AuthenticationManager 的 authenticated 方法。這裏 AuthenticationManager 是接口,實際上調用的是 ProviderManager 的 authenticated 方法。從名字我們可以猜測出 ProviderManager 管理了很多 Provider
- 在 ProviderManager 的 authenticated 方法中,獲取所有 Provider,遍歷,按 Token 匹配,調用匹配到的 Provider 的 authentication 方法(這裏表單登錄實際上調用的是 DaoAuthenticationProvider 的方法)
- 最終調用的是 UserDetailService 的 loadUserByUsername 方法
- 查詢出用戶信息後,進行校驗
- 校驗通過後,發佈認證成功信息。如果認證失敗,會拋出異常,最終也會發布認證失敗信息。
所以我們要自定義手機號登陸,需要做一下操作:
- 自定義一個 Filter 用來攔截手機號登陸
- 自定義一個 Token
- 自定義一個 Provider
- 自定義一個 UserDetailService 的實現
- 最終把上面這些自定義的類作爲配置,加入到 SpringSecurity 的校驗流程中去
OK,我們一步一步來:
自定義 SmsCodeAuthenticationToken
繼承 AbstractAuthenticationToken
類,父類裏面主要三個屬性
- 權限集合 (Collection<GrantedAuthority)
- 客戶端信息(Object detail)
- 是否通過認證(authenticated)
自己實現的 Token 在此基礎上按照認證方式進行擴展,比如如果是表單登錄,需要添加用戶名、密碼等。我們這裏是短信驗證碼認證,只需要手機號即可。
- Token 主要在認證流程中裝載數據。
- 下面單參數的構造方法,傳遞一個 mobile,是認證前用來存儲認證參數的,此時默認將 authenticated 置爲 false
- 雙參數的構造方法,是用來狀態獲取到的用戶信息,此時默認將 authenticate 置爲 true,但是並不代表當前認證已經通過了。因爲可能後面還有密碼校驗(表單登錄時)、賬號狀態校驗等。
/**
* @author: hblolj
* @Date: 2019/3/15 10:58
* @Description: 認證前用來裝載認證參數,認證通過後用來裝載用戶信息,因爲短信驗證碼登錄沒有密碼,將 credentials 移除了
* @Version:
**/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken{
private final Object principal;
public SmsCodeAuthenticationToken(Object mobile) {
super((Collection)null);
this.principal = mobile;
this.setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
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");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
@Override
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
}
自定義 SmsCodeAuthenticationFilter
主要參考 UsernamePasswordAuthenticationFilter
來實現自定義 Filter。在 Filter 中主要要做的事情有以下幾點:
-
指定 Filter 攔截的 Url 和 HttpMethod
-
完成攔截的邏輯代碼
-
從請求中獲取參數(手機號)
-
將參數封裝成自定義的 Token,同時設置一下 Detail(主要是發起請求的客戶端信息)
-
調用
AuthenticationManager
的authenticated
方法進行認證
-
/**
* @author: hblolj
* @Date: 2019/3/15 10:58
* @Description:
* @Version:
**/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// TODO: 2019/3/15 該參數可以抽取成配置,最後通過配置文件進行修改,這樣作爲共用組件只需要實現一個 default,具體值可以有調用者指定
private String mobileParameter = "mobile";
private boolean postOnly = true;
/**
* 通過構造函數指定該 Filter 要攔截的 url 和 httpMethod
*/
protected SmsCodeAuthenticationFilter() {
// TODO: 2019/3/15 pattern 可以抽取成配置,最後通過配置文件進行修改,這樣作爲共用組件只需要實現一個 default,具體值可以有調用者指定
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// 當設置該 filter 只攔截 post 請求時,符合 pattern 的非 post 請求會觸發異常
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 1. 從請求中獲取參數 mobile + smsCode
String mobile = obtainMobile(request);
if (mobile == null){
mobile = "";
}
mobile = mobile.trim();
// 2. 封裝成 Token 調用 AuthenticationManager 的 authenticated 方法,該方法中根據 Token 的類型去調用對應 Provider 的 authenticated
SmsCodeAuthenticationToken token = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, token);
// 3. 返回 authenticated 方法的返回值
return this.getAuthenticationManager().authenticate(token);
}
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public String getMobileParameter() {
return mobileParameter;
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobileParameter parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
自定義 SmsCodeAuthenticationProvider
SmsCodeAuthenticationProvider
實現 AuthenticationProvider
接口,其中有兩個方法:
- authenticate: 主要是認證邏輯實現
- 在 authenticate 方法中主要的邏輯
- 從 token 取出參數,調用 UserDetailService 進行查詢用戶信息。UserDetailService 需要我們根據不同的業務實現不同的實現類,去數據庫做不同的查詢操作。
- 使用查詢出的用戶信息構造新的 SmsCodeAuthenticationToken
- 如果是表單登錄,還要使用 PasswordEncoder 進行密碼校驗
- 如果系統有設置賬號凍結相關設置,這裏可以進行校驗(按取出的用戶信息)
- 最後返回 token。(如果返回的 result 不爲 null,最後回去做密碼擦除等操作,然後調用登錄成功處理。)
- 在 authenticate 方法中主要的邏輯
- supports: 對 authenticate 的 參數進行校驗,與 Provider 對應的 Token 進行比較,看是否是其子類或子接口。
PS: 這裏註明一下,短信驗證碼校驗應該在 SmsCodeAuthenticationFilter
之前就被校驗了
/**
* @author: hblolj
* @Date: 2019/3/15 10:58
* @Description: 短信驗證碼認證的真正校驗邏輯,實際上是按手機號查詢用戶,短信驗證碼過濾器在這之前
* @Version:
**/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider{
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
// 對用戶進行認證
UserDetails userDetails = userDetailsService.loadUserByUsername((String) token.getPrincipal());
if (userDetails == null){
throw new InternalAuthenticationServiceException("未找到對應的用戶信息!");
}
// 構造新的 Token,採用該構造函數時,會默認將 authenticated 參數置爲 true
SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationToken.setDetails(token.getDetails());
// TODO: 2019/3/15 如果認證方式與密碼相關,這裏可以對密碼進行校驗 @PasswordEncoder
// TODO: 2019/3/15 可以校驗賬號狀態: 啓用、凍結等等
// userDetails.isAccountNonExpired(); 賬號是否過期
// userDetails.isAccountNonLocked(); 賬號有無凍結
// userDetails.isCredentialsNonExpired(); 賬號密碼是否過期
// userDetails.isEnabled(); 賬號是否啓用
return authenticationToken;
}
@Override
public boolean supports(Class<?> aClass) {
// aClass 是 authenticate 方法參數的類型
// 此處判斷 aClass 是否是該 Provider 對應的 Token 的子類或者子接口,只有通過了,纔會調用 authenticate 方法去認證
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
自定義 UserDetailService
重寫 loadUserByUsername
方法,按手機號查詢。在實際開發中,這裏可以提供一個默認缺省實現,真正的實現交給業務開發人員去實現。
/**
* @author: hblolj
* @Date: 2019/3/15 14:08
* @Description:
* @Version:
**/
@Component("mobileUserDetailService")
public class MobileUserDetailService implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
// TODO: 2019/3/15 按手機號查詢用戶信息
return new User("4000368163", "123", true, true, true,
true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
自定義 SmsCodeAuthenticationSecurityConfig
經過上面的幾步準備,現在萬事俱備,只欠東風。我們只需要將 Filter 和 Provider 添加到 SpringSecurity 的認證鏈路當中(就可以召喚神龍了)即可。
- 繼承
SecurityConfigurerAdapter
類,重寫該類中的configure(HttpSecurity)
方法。(後面源碼分析時,會分析這個類是怎樣作用於配置的) - 在
configure
方法中,首先初始化自定義的 Filter 和 Provider,最後使用 HttpSecurity 進行設置添加
/**
* @author: hblolj
* @Date: 2019/3/15 10:59
* @Description:
* @Version:
**/
@Component
public class SmsCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>{
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private UserDetailsService mobileUserDetailService;
@Override
public void configure(HttpSecurity builder) throws Exception {
// 1. 初始化 SmsCodeAuthenticationFilter
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationFailureHandler(failureHandler);
// 2. 初始化 SmsCodeAuthenticationProvider
SmsCodeAuthenticationProvider provider = new SmsCodeAuthenticationProvider();
provider.setUserDetailsService(mobileUserDetailService);
// 3. 將設置完畢的 Filter 與 Provider 添加到配置中,將自定義的 Filter 加到 UsernamePasswordAuthenticationFilter 之前
builder.authenticationProvider(provider).addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
最後,將自定義的 config 添加到配置中,主要使用 apply
方法將我們自定義的 config 加入到 SpringSecurity 中,同時設置手機登錄地址訪問不需要認證,不然就沒法使用了。
@Autowired
private SmsCodeAuthenticationConfig smsCodeAuthenticationConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(smsCodeAuthenticationConfig) // 加載短信驗證
.and()
.formLogin() // 指定登錄認證方式爲表單登錄
//.loginPage("http://www.baidu.com") //指定自定義登錄頁面地址,一般前後端分離,這裏就用不到了
.loginProcessingUrl("/authentication/form") // 自定義表單登錄的 action 地址,默認是 /login
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.authorizeRequests()
// 設置手機登錄地址不需要校驗
.antMatchers("/authentication/mobile").permitAll()
.antMatchers(
securityProperties.getBrowser().getSignUpUrl()).permitAll() // 允許登錄頁面不需要認證就可以訪問,不然會死循環導致重定向次數過多
.anyRequest() // 對所有的請求
.authenticated() // 都進行認證
.and()
.csrf()
.disable();
}
然後啓動服務,因爲是 post 請求,我們打開 postman 進行模擬,這裏我對DefaultAuthenticationSuccessHandler
做了一下處理,使其返回 principal.toString()
如圖所示,證明我們配置的手機號認證流程已經生效了。
同理,除了手機號的自定義登錄,我們還可以自定義其他的登錄方式,比如微信公衆號開發中,我們需要使用用戶的 OpenId 來登錄,就可以按這個模式來處理(最終更新完後,我會將代碼實現上傳到 Github 上,到時候會包含這個 weixin openId 登錄,這裏大家感興趣的話,可以自己先實現以下)。
不過 QQ 登錄和 WeiXin 的快捷登錄又不一樣,屬於第三方登錄,要使用 SpringSocial + OAuth2 來開發。這個我會放到後面幾章去講解。
下一章的話,會帶大家處理一下在 SpringSecurity 下的 Session 管理與登出。To Be Continue!