SpringSecurity集成第三方登錄過程詳解(最新推薦)
SpringSecurity 集成第三方登錄
認證及自定義流程

首先我們提供一個實現(xiàn)了AbstractAuthenticationProcessingFilter抽象類的過濾器,用來代替UsernamePasswordAuthenticationFilter邏輯,然后提供一個AuthenticationProvider實現(xiàn)類代替AbstractUserDetailsAuthenticationProvider或DaoAuthenticationProvider,最后再提供一個UserDetailsService實現(xiàn)類。
1.驗證碼登錄
1.通用過濾器實現(xiàn)–ThirdAuthenticationFilter
這個ThirdAuthenticationFilter過濾器我們可以仿照UsernamePasswordAuthenticationFilter來實現(xiàn)(也實現(xiàn)了AbstractAuthenticationProcessingFilter抽象類),主要是重新定義了attemptAuthentication()方法,這里需要根據(jù)“authType”參數(shù)值的類別構(gòu)建不同的AbstractAuthenticationToken,具體實現(xiàn)如下:
//驗證類型,比如Sms,uernamepassword等
private String authTypeParameter = "authType";
//對應(yīng)用戶名或手機號等
private String principalParameter = "principal";
//對應(yīng)密碼或驗證碼等
private String credentialsParameter = "credentials";
private boolean postOnly = true;
public ThirdAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/doLogin", "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 authType = request.getParameter(authTypeParameter);
if(StringUtils.isEmpty(authType)){
authType = AuthTypeEnum.AUTH_TYPE_DEFAULT.getAuthType();
}
String principal = request.getParameter(principalParameter);
String credentials = request.getParameter(credentialsParameter);
AbstractAuthenticationToken authRequest = null;
switch (authType){
case "sms":
authRequest = new SmsAuthenticationToken(principal, credentials);
((SmsAuthenticationToken)authRequest).setCode((String)request.getSession().getAttribute("code"));
break;
case "github":
authRequest = new GithubAuthenticationToken(principal, credentials);
break;
case "default":
authRequest = new UsernamePasswordAuthenticationToken(principal, credentials);
}
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(authRequest);
}
}定義了ThirdAuthenticationSecurityConfig 配置類,我們還需要在SpringSecurity配置類中應(yīng)用才能生效,具體實現(xiàn)如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/error","/login/**","/login/goLogin","/login/doLogin","/login/code","/login/authorization_code").anonymous()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login/goLogin")
.loginProcessingUrl("/login/doLogin")
.failureUrl("/login/error")
.permitAll()
.successHandler(new QriverAuthenticationSuccessHandler("/index/toIndex"));
//這里我們省略了一些配置 ……
//應(yīng)用前面定義的配置
http.apply(thirdAuthenticationSecurityConfig);
}至此,我們定義的通用第三方過濾器就完成了,并且也完成了在SpringSecurity中生效的配置。下面我們就開始分別實現(xiàn)不同類型登錄的具體過程。
在ThirdAuthenticationFilter 類的attemptAuthentication()方法中,我們通過authType類型,然后創(chuàng)建對應(yīng)的Authentication實現(xiàn)來實現(xiàn)不同方式的登錄,這里我們主要實現(xiàn)了如下三種方式,我們分別梳理一下。
3、默認的登錄過程
默認的登錄過程,即根據(jù)用戶名密碼進行登錄,需要使用到UsernamePasswordAuthenticationToken,當“authType”參數(shù)為"default"時,這里就會創(chuàng)建UsernamePasswordAuthenticationToken對象,然后后續(xù)通過ProviderManager的authenticate()方法,最后就會調(diào)用AbstractUserDetailsAuthenticationProvider(DaoAuthenticationProvider)的 authenticate()方法,最終又會調(diào)用定義的UserDetailsService實現(xiàn)類。這是默認的過程,這里就不再重復(fù)其中的邏輯,除了UserDetailsService實現(xiàn)類需要自己定義,其他都是SpringSecurity提供的實現(xiàn)類。
4、短信驗證碼登錄實現(xiàn)
短信驗證碼登錄,是最貼近用戶名密碼登錄的一種方式,所以我們完全可以仿照用戶名密碼這種方式實現(xiàn)。我們這里先梳理一下短信驗證碼登錄的業(yè)務(wù)邏輯:首先,登錄界面輸入手機號碼,然后再點擊“獲取驗證碼”按鈕獲取短信驗證碼,然后輸入收到的短信驗證碼,最后點擊“登錄”按鈕進行登錄認證。和用戶名密碼登錄相比,短信驗證碼登錄多了一個獲取驗證碼的過程,其他其實都是一樣的,我們下面逐步實現(xiàn)短信驗證碼登錄:
@RestController
@RequestMapping("/login")
public class SmsValidateCodeController {
//生成驗證碼的實例對象
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
//調(diào)用服務(wù)商接口,發(fā)送短信驗證碼的實例對象
@Autowired
private DefaultSmsCodeSender defaultSmsCodeSender;
@RequestMapping("/code")
public String createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
String mobile = (String)request.getParameter("principal");
request.getSession().setAttribute("code",smsCode.getCode());
defaultSmsCodeSender.send(mobile, smsCode.getCode());
System.out.println("驗證碼:" + smsCode.getCode());
return "驗證碼發(fā)送成功!";
}
}在上述方法中,我們注入了smsCodeGenerator和defaultSmsCodeSender兩個實例對象,分別用來生成驗證碼和發(fā)送短信驗證碼,這個可以根據(jù)項目的實際情況進行定義和實現(xiàn),這里不再貼出其中的實現(xiàn)。同時在createSmsCode()方法中,還有一點需要注意的就是,我們發(fā)出去的短信驗證碼需要進行保存,方便后續(xù)登錄時進行驗證,這個也可以選擇很多方法,比如說會話、數(shù)據(jù)庫、緩存等,我這里為了簡單,直接存到了session會話中了。
然后,我們前面定義ThirdAuthenticationFilter過濾器時,根據(jù)登錄方式不同,需要對應(yīng)的Authentication對象,這里我們還需要創(chuàng)建短信驗證登錄需要的Authentication類,這里我們可以仿照UsernamePasswordAuthenticationToken類進行編寫,實現(xiàn)如下
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
//對應(yīng)手機號碼
private final Object principal;
//對應(yīng)手機驗證碼
private Object credentials;
//后臺存儲的短信驗證碼,用于驗證前端傳過來的是否正確
private String code;
public SmsAuthenticationToken(String mobile, Object credentials){
super(null);
this.principal = mobile;
this.credentials = credentials;
this.code = code;
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, Object credentials){
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;
}
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);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}在SmsAuthenticationToken 類中,我們增加了一個code屬性,其實該屬性不是必須的,我這里是為了方便傳遞存儲在session會話中的驗證碼而添加的,如果使用緩存或數(shù)據(jù)庫進行存儲驗證碼,該屬性就可以省略。
在AuthenticationManager的authenticate()方法中,會根據(jù)Authentication類型選擇AuthenticationProvider對象,所以我們這里自定義短信驗證碼需要的AuthenticationProvider對象,實現(xiàn)如下:
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider{
@Autowired
@Qualifier("smsUserDetailsService")
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken token = (SmsAuthenticationToken) authentication;
String mobile = (String)token.getPrincipal();
//首先,驗證驗證碼是否正確
String code = (String)token.getCredentials();
String sCode = token.getCode();
if(StringUtils.isEmpty(code) || !code.equalsIgnoreCase(sCode)){
throw new BadCredentialsException("手機驗證碼錯誤(Bad credentials),請重試!");
}
//然后,查詢對應(yīng)用戶
UserDetails user = userDetailsService.loadUserByUsername(mobile);
if (Objects.isNull(user)) {
throw new InternalAuthenticationServiceException("根據(jù)手機號:" + mobile + ",無法獲取對應(yīng)的用戶信息!");
}
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(user.getUsername(), user.getAuthorities(), token.getCredentials());
authenticationResult.setDetails(token.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}在SmsAuthenticationProvider 中,supports()方法決定了該實例對象僅支持SmsAuthenticationToken對象的驗證。同時,根據(jù)authenticate()方法傳遞參數(shù)authentication對象(包括了登錄信息:手機號和驗證碼,session存儲的驗證碼),我們這里session存儲的驗證碼,是因為我們采用了會話存儲的方式,如果使用數(shù)據(jù)庫,我們這里就可以通過手機號,去數(shù)據(jù)庫或緩存查詢對應(yīng)的驗證碼,然后和authentication對象傳遞過來的驗證碼進行比對,驗證成功,說明登錄認證成功,否則登錄認證失敗。登錄成功后,我們就可以調(diào)用userDetailsService對象的loadUserByUsername()方法獲取登錄用戶的其他相關(guān)信息(權(quán)限等),具體實現(xiàn)在自定義的SmsUserDetailsService類中實現(xiàn),具體如下:
@Component("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(SmsUserDetailsService.class);
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1、查詢用戶信息
SysUser user = new SysUser();
user.setMobile(username);
SysUser qUser = sysUserService.getOne(new QueryWrapper<>(user),true);
if(qUser == null) {
logger.info("手機號為”" + username + "“的用戶不存在?。。?);
throw new UsernameNotFoundException("手機號為”" + username + "“的用戶不存在?。?!");
}
//2、封裝用戶角色
UserRole userRole = sysUserService.getRoleByUserId(qUser.getId());
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(String.valueOf(userRole.getRoleId())));
return new LoginUser(qUser.getUsername(), qUser.getPassword(),authorities);
}
}2.GitHub登錄
和短信驗證碼登錄認證相比,Github登錄又會有自己的特殊性,我們這里先梳理一下基于Github進行登錄驗證的大致邏輯:首先,點擊Github登錄認證按鈕,然后會跳轉(zhuǎn)到github登錄界面,輸入github系統(tǒng)的用戶名密碼,登錄成功,就會跳轉(zhuǎn)到我們自己的系統(tǒng)中的首頁。和基于用戶名密碼的登錄方式相比,Github登錄不需要類似用戶名和密碼這樣的輸入(在自己的系統(tǒng)中),同時又需要根據(jù)獲取到的github用戶信息,換取在自己系統(tǒng)對應(yīng)的用戶信息。具體實現(xiàn)步驟如下:
在github的配置省略
@Controller
@RequestMapping("/login")
public class GithubValidateController {
@Autowired
private GithubClientService githubClientService;
@RequestMapping("/authorization_code")
public void authorization_code(HttpServletRequest request, HttpServletResponse response, String code) throws ServletRequestBindingException, IOException {
//github登錄驗證,并獲取access_token
Map<String,String> resp = githubClientService.queryAccessToken(code);
//跳轉(zhuǎn)本系統(tǒng)的登錄流程,獲取用戶信息,實現(xiàn)兩個系統(tǒng)用戶的對接
String url = "http://localhost:8888/qriver-admin/login/doLogin";
this.sendByPost(response, url,resp.get("access_token"),"github");
//this.sendByPost(response, url,"access_token","github");
}
public void sendByPost(HttpServletResponse response,String url, String principal, String authType) throws IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
out.println("<HTML>");
out.println(" <HEAD><TITLE>Post 方法</TITLE></HEAD>");
out.println(" <BODY>");
out.println("<form name=\"submitForm\" action=\"" + url + "\" method=\"post\">");
out.println("<input type=\"hidden\" name=\"principal\" value=\"" + principal + "\"/>");
out.println("<input type=\"hidden\" name=\"authType\" value=\"" + authType + "\"/>");
out.println("</from>");
out.println("<script>window.document.submitForm.submit();</script> ");
out.println(" </BODY>");
out.println("</HTML>");
out.flush();
out.close();
}
}“/login/authorization_code”接口對應(yīng)了我們在Github中配置的回調(diào)函數(shù),即在Github登錄驗證成功后,就會回調(diào)該接口,我們就是就在回調(diào)方法中,模擬了用戶名密碼登錄的方式,調(diào)用了SpringSecurity登錄認證需要的“/login/doLogin”接口。這里,我們通過queryAccessToken()方法根據(jù)回調(diào)傳遞的code獲取對應(yīng)的accessToken,然后把accessToken作為登錄使用的principal 參數(shù)值,之而立不需要傳遞密碼,因為我們經(jīng)過Github授權(quán),就可以認為完成了登錄認證的判斷過程了。
其中GithubClientService類,提供了獲取accessToken和用戶信息的兩個方法,具體實現(xiàn)方式如下:
@Service
public class GithubClientService {
//前面在github中配置時產(chǎn)生的
private String clientId = "######";
private String clientSecret = "######";
private String state = "123";
private String redirectUri = "http://localhost:8888/qriver-admin/login/authorization_code";
@Autowired
private RestTemplate restTemplate;
@Nullable
private WebApplicationContext webApplicationContext;
//獲取accessToken
public Map<String, String> queryAccessToken(String code ){
Map<String, String> map = new HashMap<>();
map.put("client_id", clientId);
map.put("client_secret", clientSecret);
map.put("state", state);
map.put("code", code);
map.put("redirect_uri", redirectUri);
Map<String,String> resp = restTemplate.postForObject("https://github.com/login/oauth/access_token", map, Map.class);
return resp;
}
//獲取用戶信息
public Map<String, Object> queryUser(String accessToken){
HttpHeaders httpheaders = new HttpHeaders();
httpheaders.add("Authorization", "token " + accessToken);
HttpEntity<?> httpEntity = new HttpEntity<>(httpheaders);
ResponseEntity<Map> exchange = restTemplate.exchange("https://api.github.com/user", HttpMethod.GET, httpEntity, Map.class);
System.out.println("exchange.getBody() = " + exchange.getBody());
return exchange == null ? null : exchange.getBody();
}
}其實,完成了上述的配置和方式后,后續(xù)的方式就和短信驗證碼的邏輯一樣了,這里我們簡要的再梳理一下。
首先,我們也需要定義一個基于Github登錄需要的Authentication實現(xiàn)類,具體實現(xiàn)和前面的SmsAuthenticationToken類似,這里不再重復(fù)貼代碼了。
然后,我們再定義一個AuthenticationProvider實現(xiàn)類GithubAuthenticationProvider,具體實現(xiàn)如下:
@Component
public class GithubAuthenticationProvider implements AuthenticationProvider{
@Autowired
@Qualifier("githubUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private GithubClientService githubClientService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
GithubAuthenticationToken token = (GithubAuthenticationToken) authentication;
String accessToken = (String)token.getPrincipal();
//根據(jù)accessToken 獲取github用戶信息
Map<String, Object> userInfo = githubClientService.queryUser(accessToken);
//然后,根據(jù)github用戶,查詢對應(yīng)系統(tǒng)用戶信息
UserDetails user = userDetailsService.loadUserByUsername((String)userInfo.get("login"));
if (Objects.isNull(user)) {
throw new InternalAuthenticationServiceException("根據(jù)accessToken:" + accessToken + ",無法獲取對應(yīng)的用戶信息!");
}
GithubAuthenticationToken authenticationResult = new GithubAuthenticationToken(user.getUsername(), user.getAuthorities(), token.getCredentials());
authenticationResult.setDetails(token.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return GithubAuthenticationToken.class.isAssignableFrom(authentication);
}
}在GithubAuthenticationProvider 類的authenticate()方法中,參數(shù)authentication中對應(yīng)的是Github授權(quán)后傳遞的accessToken值,我們這里需要根據(jù)accessToken值換取Github用戶信息,這里通過queryUser()方法實現(xiàn),然后根據(jù)github用戶名去獲取對應(yīng)的系統(tǒng)用戶信息。如果根據(jù)github用戶名用戶獲取的系統(tǒng)用戶為空,我們可以根據(jù)自己的需求,自動生成一個用戶或者跳轉(zhuǎn)到注冊頁面,讓用戶注冊一個頁面,這里為了簡單,我們直接拋出了一個異常。
關(guān)于自定義UserDetailsService實現(xiàn)類,主要需要實現(xiàn)根據(jù)github用戶名查詢對應(yīng)系統(tǒng)用戶的功能
當認證完成后要返回token可以實現(xiàn)AuthenticationSuccessHandler
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider; // 假設(shè)你有一個JwtTokenProvider類來生成JWT
public CustomAuthenticationSuccessHandler(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 生成JWT
String token = jwtTokenProvider.generateToken(authentication);
// 將JWT添加到響應(yīng)頭中
response.setHeader("Authorization", "Bearer " + token);
// 或者將JWT添加到響應(yīng)體中(取決于你的API設(shè)計)
// response.getWriter().write(token);
response.setStatus(HttpServletResponse.SC_OK);
}
}并在securityconfig中設(shè)置
到此這篇關(guān)于SpringSecurity集成第三方登錄的文章就介紹到這了,更多相關(guān)SpringSecurity登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+SpringSecurity處理Ajax登錄請求問題(推薦)
- 解析SpringSecurity自定義登錄驗證成功與失敗的結(jié)果處理問題
- SpringSecurity動態(tài)加載用戶角色權(quán)限實現(xiàn)登錄及鑒權(quán)功能
- 解決SpringSecurity 一直登錄失敗的問題
- SpringSecurity多表多端賬戶登錄的實現(xiàn)
- SpringBoot如何整合Springsecurity實現(xiàn)數(shù)據(jù)庫登錄及權(quán)限控制
- SpringSecurity6.x多種登錄方式配置小結(jié)
- SpringSecurity表單配置之登錄成功及頁面跳轉(zhuǎn)原理解析
- Spring?Security重寫AuthenticationManager實現(xiàn)賬號密碼登錄或者手機號碼登錄
相關(guān)文章
基于spring@aspect注解的aop實現(xiàn)過程代碼實例
這篇文章主要介紹了基于spring@aspect注解的aop實現(xiàn)過程代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03
完美解決java.lang.OutOfMemoryError處理錯誤的問題
下面小編就為大家?guī)硪黄昝澜鉀Qjava.lang.OutOfMemoryError處理錯誤的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01
springboot項目連接多種數(shù)據(jù)庫該如何操作詳析
在Spring Boot應(yīng)用中連接多個數(shù)據(jù)庫或數(shù)據(jù)源可以使用多種方式,下面這篇文章主要給大家介紹了關(guān)于springboot項目連接多種數(shù)據(jù)庫該如何操作的相關(guān)資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-08-08
利用Java簡單實現(xiàn)一個代碼行數(shù)統(tǒng)計器方法實例
這篇文章主要給大家介紹了關(guān)于如何利用Java簡單實現(xiàn)一個代碼行數(shù)統(tǒng)計器的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
一篇文章總結(jié)Java虛擬機內(nèi)存區(qū)域模型
這篇文章主要介紹了一篇文章總結(jié)Java虛擬機內(nèi)存區(qū)域模型,本篇文章主要來總結(jié)一下Java虛擬機內(nèi)存的各個區(qū)域,以及這些區(qū)域的作用、服務(wù)對象以及其中可能產(chǎn)生的問題,作為大家的面試寶典。,需要的朋友可以參考下2019-06-06

