springsecurity基于token的認證方式
前言
上一篇博客簡析了一下spring security oauth中生成AccessToken的源碼,目的就是為了方便我們將原有的表單登錄,短信登錄以及社交登錄的認證方法,都改造成基于AccessToken的認證方式
基于token的表單登錄
在簡析了spring security oauth的源碼之后,我們發(fā)現(xiàn),其實有些源碼我們并不能用,至少,TokenEndPoint這個組件,我們就沒法用,因為這個組件只會響應(yīng)/oauth/token的請求,而且spring security oauth會根據(jù)OAuth協(xié)議中常用的4種授權(quán)模式去生成令牌,而我們這里是自定義的登錄,自然用不上OAuth協(xié)議中的授權(quán)模式,因此我們改造自定義的登錄,只能借鑒其令牌生成方式。
如果有印象,在前幾篇博客中總結(jié)過自定義登錄成功處理的方式,無論前面登錄邏輯如何認證,我們只需要在認證成功之后,自定義生成AccessToken 即可,因此我們只需要重新處理我們自定義登錄成功的處理方式即可。
那么如何處理,依舊是一個問題,這就回到了上一篇博客中的內(nèi)容,構(gòu)造AccessToken需要OAuth2Request和Authentication,其中Authentication是登錄成功后的認證詳情信息,在登錄成功處理器中,會有相關(guān)參數(shù)傳遞進來。OAuth2Request由ClientDeatails和TokenRequest組成,這在上一篇博客中我們已經(jīng)總結(jié)過了,ClientDetails根據(jù)傳遞參數(shù)中的ClientId和clientSecret等client配置信息組成,TokenRequest則由請求中其他參數(shù)實例化而成,具體如下圖所示

相關(guān)改造代碼如下
/**
* autor:liman
* createtime:2021/7/10
* comment: 自定義登錄成功處理器
*/
@Component("selfAuthenticationSuccessHandler")
@Slf4j
public class SelfAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authenticationServerTokenServices;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
, Authentication authentication) throws IOException, ServletException {
log.info("自定義登錄成功的處理器");
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
throw new UnapprovedClientAuthenticationException("請求頭中沒有client相關(guān)的信息");
}
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
//得到clientDeatils信息
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);//得到clientDetails信息
if (null == clientDetails) {
throw new UnapprovedClientAuthenticationException("clientid對應(yīng)的信息不存在" + clientId);
} else if (!StringUtils.equals(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("clientSecret信息不匹配" + clientSecret);
}
//構(gòu)建自己的tokenRequest,由于這里不能使用OAuth2中的四種授權(quán)模式,因此這里第四個參數(shù)設(shè)置為"customer"
//同理,第一個參數(shù)主要用于組裝并生成Authentication,而這里的Authentication已經(jīng)通過參數(shù)傳遞進來,因此可以直接賦一個空的Map
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "customer");
//構(gòu)建OAuth2Request
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
//構(gòu)建 OAuth2Authentication
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
//生成accessToken,這里依舊使用的是spring security oauth中默認的DefaultTokenService
OAuth2AccessToken accessToken = authenticationServerTokenServices.createAccessToken(oAuth2Authentication);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(accessToken));//將authentication作為json寫到前端
}
/**
* Decodes the header into a username and password.
*
* @throws BadCredentialsException if the Basic header is not present or is not valid
* Base64
*/
//TODO:解碼請求頭中的Base64編碼的 appId和AppSecret
private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
throws IOException {
//格式:Basic+空格+Base64加密的appid和AppSecret,所以這里substring(6)
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
基于token的短信驗證碼登錄
之前提到過,由于基于token的認證交互,其實不一定會有session會話的概念,如果我們的驗證碼依舊存于session中,則并不能正常校驗,因此在基于token的短信驗證碼登錄的重構(gòu)中,我們唯一要做的,就是將驗證碼存于Redis等緩存中間件中,驗證碼的key值為deviceid。
方案比較簡單,這里只貼出Redis操作驗證碼的方法
/**
* 基于redis的驗證碼存取器,避免由于沒有session導(dǎo)致無法存取驗證碼的問題
*/
@Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/*
* (non-Javadoc)
*/
@Override
public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType type) {
redisTemplate.opsForValue().set(buildKey(request, type), code, 30, TimeUnit.MINUTES);
}
/*
* (non-Javadoc)
*/
@Override
public ValidateCode get(ServletWebRequest request, ValidateCodeType type) {
Object value = redisTemplate.opsForValue().get(buildKey(request, type));
if (value == null) {
return null;
}
return (ValidateCode) value;
}
/*
* (non-Javadoc)
*
*/
@Override
public void remove(ServletWebRequest request, ValidateCodeType type) {
redisTemplate.delete(buildKey(request, type));
}
/**
* @param request
* @param type
* @return
*/
private String buildKey(ServletWebRequest request, ValidateCodeType type) {
String deviceId = request.getHeader("deviceId");
if (StringUtils.isBlank(deviceId)) {
throw new ValidateCodeException("請在請求頭中攜帶deviceId參數(shù)");
}
return "code:" + type.toString().toLowerCase() + ":" + deviceId;
}
}
基于token的社交登錄
在調(diào)通微信社交登錄之后,再進行總結(jié),只是需要明確的是,這里分為兩種情況,一種是簡化模式,一種是標準的OAuth2授權(quán)模式(這兩種的區(qū)別,在QQ登錄和微信登錄流程中有詳細的體現(xiàn))。
簡化的OAuth的授權(quán)改造
簡化的OAuth模式,OAuth協(xié)議簡化的認證模式,與標準最大的不同,其實就是在獲取授權(quán)碼的時候,順帶將openId(第三方用戶id)和accessToken(獲取用戶信息的令牌),在這種前后端徹底分離的架構(gòu)中,前三步前端可以通過服務(wù)提供商的SDK完成openId和AccessToken的獲取。但是并不能根據(jù)openId作為我們自己登錄系統(tǒng)憑證,因此我們需要提供一個根據(jù)openId進行登錄的方式這個與之前短信登錄方式大同小異

1、OpenIdAuthenticationToken
/**
* autor:liman
* createtime:2021/8/4
* comment:OpenIdAuthenticationToken
*/
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private String providerId;
/**
openId,和providerId作為principal
*/
public OpenIdAuthenticationToken(String openId, String providerId) {
super(null);
this.principal = openId;
this.providerId = providerId;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public OpenIdAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public String getProviderId() {
return providerId;
}
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();
}
}
2、OpenIdAuthenticationFilter
/**
* autor:liman
* createtime:2021/8/4
* comment:基于openId登錄的過濾器
*/
@Slf4j
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String openIdParameter = "openId";
private String providerIdParameter = "providerId";
private boolean postOnly = true;
public OpenIdAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/openid", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//獲取請求中的openId和providerId
String openid = obtainOpenId(request);
String providerId = obtainProviderId(request);
if (openid == null) {
openid = "";
}
if (providerId == null) {
providerId = "";
}
openid = openid.trim();
providerId = providerId.trim();
//構(gòu)造OpenIdAuthenticationToken
OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openid, providerId);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//交給AuthenticationManager進行認證
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 獲取openId
*/
protected String obtainOpenId(HttpServletRequest request) {
return request.getParameter(openIdParameter);
}
/**
* 獲取提供商id
*/
protected String obtainProviderId(HttpServletRequest request) {
return request.getParameter(providerIdParameter);
}
protected void setDetails(HttpServletRequest request, OpenIdAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setOpenIdParameter(String openIdParameter) {
Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
this.openIdParameter = openIdParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getOpenIdParameter() {
return openIdParameter;
}
public String getProviderIdParameter() {
return providerIdParameter;
}
public void setProviderIdParameter(String providerIdParameter) {
this.providerIdParameter = providerIdParameter;
}
}
3、OpenIdAuthenticationProvider
/**
*
*/
package com.learn.springsecurity.app.social.openid;
/**
* @author zhailiang
*
*/
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
private SocialUserDetailsService userDetailsService;
private UsersConnectionRepository usersConnectionRepository;
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.AuthenticationProvider#
* authenticate(org.springframework.security.core.Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
Set<String> providerUserIds = new HashSet<>();
providerUserIds.add((String) authenticationToken.getPrincipal());
//之前社交登錄中介紹的usersConnectionRepository,從user_connection表中根據(jù)providerId和openId查詢用戶id
Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);
if(CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
//獲取到userId了
String userId = userIds.iterator().next();
//利用UserDetailsService根據(jù)userId查詢用戶信息
UserDetails user = userDetailsService.loadUserByUserId(userId);
if (user == null) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.AuthenticationProvider#
* supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> authentication) {
return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
}
public SocialUserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(SocialUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UsersConnectionRepository getUsersConnectionRepository() {
return usersConnectionRepository;
}
public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
this.usersConnectionRepository = usersConnectionRepository;
}
}
4、配置類
/**
* @author zhailiang
*
*/
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler selfAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler selfAuthenticationFailureHandler;
@Autowired
private SocialUserDetailsService userDetailsService;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Override
public void configure(HttpSecurity http) throws Exception {
OpenIdAuthenticationFilter OpenIdAuthenticationFilter = new OpenIdAuthenticationFilter();
OpenIdAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
OpenIdAuthenticationFilter.setAuthenticationSuccessHandler(selfAuthenticationSuccessHandler);
OpenIdAuthenticationFilter.setAuthenticationFailureHandler(selfAuthenticationFailureHandler);
OpenIdAuthenticationProvider OpenIdAuthenticationProvider = new OpenIdAuthenticationProvider();
OpenIdAuthenticationProvider.setUserDetailsService(userDetailsService);
OpenIdAuthenticationProvider.setUsersConnectionRepository(usersConnectionRepository);
http.authenticationProvider(OpenIdAuthenticationProvider)
.addFilterAfter(OpenIdAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
測試結(jié)果

標準的OAuth授權(quán)改造
標準的OAuth模式

針對標準的授權(quán)模式,我們并不需要做多少改動,因為在社交登錄那一節(jié)中我們已經(jīng)做了相關(guān)開發(fā),只是需要說明的是,只是在spring-social的過濾器——SocialAuthenticationFilter中,在正常社交登錄流程完成之后會默認跳轉(zhuǎn)到某個頁面,而這個并不適用于前后端分離的項目,因此要針對這個問題定制化解決。這需要回到之前SocialAuthenticationFilter加入到認證過濾器鏈上的代碼。之前我們說過社交登錄的過濾器鏈不需要我們手動配置,只需要初始化SpringSocialConfiguer的時候,會自動加入到社交登錄的認證過濾器鏈上
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Bean
public SpringSocialConfigurer selfSocialSecurityConfig(){
SpringSocialConfigurer selfSpringSocialConfig = new SpringSocialConfigurer();
return selfSpringSocialConfig;
}
}
我們只需要改變SocialAuthenticationFilter的默認處理即可,因此我們給他加一個后置處理器,但是這個后置處理器是在SpringSocialConfigurer的postProcess函數(shù)中進行處理
/**
* autor:liman
* createtime:2021/7/15
* comment:自定義的springsocial配置類
*/
public class SelfSpringSocialConfig extends SpringSocialConfigurer {
private String processFilterUrl;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
@Autowired(required = false)
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
public SelfSpringSocialConfig(String processFilterUrl) {
this.processFilterUrl = processFilterUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter socialAuthenticationFilter = (SocialAuthenticationFilter) super.postProcess(object);
socialAuthenticationFilter.setFilterProcessesUrl(processFilterUrl);
if(null!=socialAuthenticationFilterPostProcessor){
socialAuthenticationFilterPostProcessor.process(socialAuthenticationFilter);
}
return (T) socialAuthenticationFilter;
}
public ConnectionSignUp getConnectionSignUp() {
return connectionSignUp;
}
public void setConnectionSignUp(ConnectionSignUp connectionSignUp) {
this.connectionSignUp = connectionSignUp;
}
public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
return socialAuthenticationFilterPostProcessor;
}
public void setSocialAuthenticationFilterPostProcessor(SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
}
}
//將我們自定義的 SpringSocialConfigurer交給spring托管
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Bean
public SpringSocialConfigurer selfSocialSecurityConfig(){
String processFilterUrl = securityProperties.getSocial().getProcessFilterUrl();
SelfSpringSocialConfig selfSpringSocialConfig = new SelfSpringSocialConfig(processFilterUrl);
//指定第三方用戶信息認證不存在的注冊頁
selfSpringSocialConfig.signupUrl(securityProperties.getBrowser().getSiguUpPage());
selfSpringSocialConfig.setConnectionSignUp(connectionSignUp);
selfSpringSocialConfig.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return selfSpringSocialConfig;
}
}
我們自定義的過濾器后置處理器如下
/**
* autor:liman
* createtime:2021/8/7
* comment:APP社交登錄認證后置處理器
*/
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {
@Autowired
private AuthenticationSuccessHandler selfAuthenticationSuccessHandler;
@Override
public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
socialAuthenticationFilter.setAuthenticationSuccessHandler(selfAuthenticationSuccessHandler);
}
}
關(guān)于用戶的綁定
這里需要總結(jié)一下之前的社交登錄中用戶注冊綁定的操作。
之前的社交登錄綁定用戶
在之前的社交登錄中,如果spring social發(fā)現(xiàn)用戶是第一次登錄,則會跳轉(zhuǎn)到相關(guān)的頁面,這個頁面我們其實也可以自己定義并配置
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Bean
public SpringSocialConfigurer selfSocialSecurityConfig(){
String processFilterUrl = securityProperties.getSocial().getProcessFilterUrl();
SelfSpringSocialConfig selfSpringSocialConfig = new SelfSpringSocialConfig(processFilterUrl);
//指定第三方用戶信息認證不存在的注冊頁
selfSpringSocialConfig.signupUrl(securityProperties.getBrowser().getSiguUpPage());
selfSpringSocialConfig.setConnectionSignUp(connectionSignUp);
selfSpringSocialConfig.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return selfSpringSocialConfig;
}
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(connectionFactoryLocator,
getUsersConnectionRepository(connectionFactoryLocator));
}
}
我們配置的代碼中,可以自定義頁面路徑,我們自定義頁面如下(一個簡單的登錄綁定頁面)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登錄</title> </head> <body> <h2>Demo注冊頁</h2> <form action="user/regist" method="post"> <table> <tr> <td>用戶名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密碼:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"> <button type="submit" name="type" value="regist">注冊</button> <button type="submit" name="type" value="binding">綁定</button> </td> </tr> </table> </form> </body> </html>
在用戶第一次跳轉(zhuǎn)到這個頁面的用戶選擇注冊,或者綁定,都會請求/user/register接口,這個接口借助providerSignInUtils完成會話中的用戶數(shù)據(jù)更新
@Autowired
private ProviderSignInUtils providerSignInUtils;
@PostMapping("/register")
public void userRegister(@RequestBody User user, HttpServletRequest request) {
//利用providerSignInUtils,將注冊之后的用戶信息,關(guān)聯(lián)到會話中
providerSignInUtils.doPostSignUp(user.getId(),new ServletWebRequest(request));
}
在跳轉(zhuǎn)之前,spring social已經(jīng)幫我們將用戶信息存入會話(在SocialAuthenticationFilter中可以看到相關(guān)代碼)
//以下代碼位于:org.springframework.social.security.SocialAuthenticationFilter#doAuthentication
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
token.setDetails(authenticationDetailsSource.buildDetails(request));
Authentication success = getAuthenticationManager().authenticate(token);
Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
updateConnections(authService, token, success);
return success;
} catch (BadCredentialsException e) {
// connection unknown, register new user?
if (signupUrl != null) {
//這里就是將社交用戶信息存入會話
// store ConnectionData in session and redirect to register page
sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
}
throw e;
}
}
但是基于前后端分離,且并沒有會話對象交互的系統(tǒng),這種方式并不適用,因為并不存在會話,如何處理,需要用其他方案,其實我們可以在驗證碼登錄的改造中受到啟發(fā),將用戶數(shù)據(jù)存入會話即可,我們自定義實現(xiàn)一個providerSignInUtils將用戶信息存入Redis即可。
自定義providerSignUtils
1、將第三方用戶數(shù)據(jù)存入Redis的工具類
/**
* autor:liman
* createtime:2021/8/7
* comment:app端用戶信息存入Redis的工具類
*/
@Component
public class AppSignUpUtils {
public static final String SOCIAL_REDIS_USER_PREFIX = "self:security:social:connectionData";
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
public void saveConnectionData(WebRequest webRequest, ConnectionData connectionData) {
redisTemplate.opsForValue().set(getKey(webRequest), connectionData, 10, TimeUnit.MINUTES);
}
/**
* 將用戶與數(shù)據(jù)庫中的信息進行綁定
* @param request
* @param userId
*/
public void doPostSignUp(WebRequest request,String userId){
String key = getKey(request);
if(!redisTemplate.hasKey(key)){
throw new RuntimeException("無法找到緩存的用戶社交賬號信息");
}
ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
//根據(jù)ConnectionData實例化創(chuàng)建一個Connection
Connection<?> connection = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId())
.createConnection(connectionData);
//將數(shù)據(jù)庫中的用戶與Redis中的用戶信息關(guān)聯(lián)
usersConnectionRepository.createConnectionRepository(userId).addConnection(connection);
}
/**
* 獲取設(shè)備id作為key
*
* @param webRequest
* @return
*/
public String getKey(WebRequest webRequest) {
String deviceId = webRequest.getHeader("deviceId");
if (StringUtils.isBlank(deviceId)) {
throw new RuntimeException("設(shè)備id不能為空");
}
return SOCIAL_REDIS_USER_PREFIX + deviceId;
}
}
2、復(fù)寫掉原來的配置類
為了避免對原有代碼的侵入性處理,這里我們需要自定義一個實現(xiàn)BeanPostProcessor接口的類
/**
* autor:liman
* createtime:2021/8/7
* comment:由于app端的社交用戶綁定,不能采用跳轉(zhuǎn),也不能操作會話,需要用自定義的providerSignUpUtils工具類
* 因此需要定義一個后置處理器,針對SpringSocialConfigurer進行一些后置處理
*/
@Component
public class AppSpringSocialConfigurerPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(StringUtils.equals(beanName,"selfSocialSecurityConfig")){
SelfSpringSocialConfig configurer = (SelfSpringSocialConfig) bean;
//復(fù)寫掉原有的SelfSpringSocialConfig的signupUrl
configurer.signupUrl("/app/social/signup");
return configurer;
}
return bean;
}
}
針對上述的請求路徑,我們也要寫一個對應(yīng)路徑的controller處理方法
@RestController
@Slf4j
public class AppSecurityController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@Autowired
private AppSignUpUtils appSignUpUtils;
@GetMapping("/app/social/signup")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public BaseResponse getSocialUserInfo(HttpServletRequest request){
BaseResponse result = new BaseResponse(StatusCode.Success);
log.info("【app模式】開始獲取會話中的第三方用戶信息");
//先從其中拿出數(shù)據(jù),畢竟這個時候還沒有完全跳轉(zhuǎn),下一個會話,就沒有該數(shù)據(jù)了
Connection<?> connectionFromSession = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
SocialUserInfo socialUserInfo = new SocialUserInfo();
socialUserInfo.setProviderId(connectionFromSession.getKey().getProviderId());
socialUserInfo.setProviderUserId(connectionFromSession.getKey().getProviderUserId());
socialUserInfo.setNickName(connectionFromSession.getDisplayName());
socialUserInfo.setHeadImg(connectionFromSession.getImageUrl());
//轉(zhuǎn)存到自己的工具類中
appSignUpUtils.saveConnectionData(new ServletWebRequest(request),connectionFromSession.createData());
result.setData(socialUserInfo);
return result;
}
}
對于用戶注冊的接口也需要做調(diào)整
@PostMapping("/register")
public void userRegister(@RequestBody User user, HttpServletRequest request) {
//如果是瀏覽器的應(yīng)用利用providerSignInUtils,將注冊之后的用戶信息,關(guān)聯(lián)到會話中
providerSignInUtils.doPostSignUp(user.getId(),new ServletWebRequest(request));
//如果是app的應(yīng)用,則利用appSignUpUtils 將注冊之后的用戶信息,關(guān)聯(lián)到會話中
appSignUpUtils.doPostSignUp(new ServletWebRequest(request),user.getId());
}
總結(jié)
總結(jié)了基于token認證的三種登錄方式,最為復(fù)雜的為社交登錄方式
到此這篇關(guān)于springsecurity基于token的認證方式的文章就介紹到這了,更多相關(guān)springsecurity token認證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
application作用域?qū)崿F(xiàn)用戶登錄擠掉之前登錄用戶代碼
這篇文章主要介紹了application作用域?qū)崿F(xiàn)用戶登錄擠掉之前登錄用戶代碼,具有一定參考價值,需要的朋友可以了解下。2017-11-11
Spring動態(tài)數(shù)據(jù)源實現(xiàn)讀寫分離詳解
這篇文章主要為大家詳細介紹了Spring動態(tài)數(shù)據(jù)源實現(xiàn)讀寫分離,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
Java使用RandomAccessFile類對文件進行讀寫
本篇文章主要介紹了Java使用RandomAccessFile類對文件進行讀寫,詳細的介紹了RandomAccessFile類的使用技巧和實例應(yīng)用,有興趣的可以了解一下2017-04-04
詳解SpringBoot結(jié)合策略模式實戰(zhàn)套路
這篇文章主要介紹了詳解SpringBoot結(jié)合策略模式實戰(zhàn)套路,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10

