溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式

發(fā)布時(shí)間:2021-08-11 09:37:09 來源:億速云 閱讀:214 作者:小新 欄目:開發(fā)技術(shù)

這篇文章主要為大家展示了“springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式”,內(nèi)容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式”這篇文章吧。

基于token的表單登錄

在簡析了spring security oauth的源碼之后,我們發(fā)現(xiàn),其實(shí)有些源碼我們并不能用,至少,TokenEndPoint這個(gè)組件,我們就沒法用,因?yàn)檫@個(gè)組件只會(huì)響應(yīng)/oauth/token的請(qǐng)求,而且spring security oauth會(huì)根據(jù)OAuth協(xié)議中常用的4種授權(quán)模式去生成令牌,而我們這里是自定義的登錄,自然用不上OAuth協(xié)議中的授權(quán)模式,因此我們改造自定義的登錄,只能借鑒其令牌生成方式。

如果有印象,在前幾篇博客中總結(jié)過自定義登錄成功處理的方式,無論前面登錄邏輯如何認(rèn)證,我們只需要在認(rèn)證成功之后,自定義生成AccessToken 即可,因此我們只需要重新處理我們自定義登錄成功的處理方式即可。

那么如何處理,依舊是一個(gè)問題,這就回到了上一篇博客中的內(nèi)容,構(gòu)造AccessToken需要OAuth3Request和Authentication,其中Authentication是登錄成功后的認(rèn)證詳情信息,在登錄成功處理器中,會(huì)有相關(guān)參數(shù)傳遞進(jìn)來。OAuth3Request由ClientDeatails和TokenRequest組成,這在上一篇博客中我們已經(jīng)總結(jié)過了,ClientDetails根據(jù)傳遞參數(shù)中的ClientId和clientSecret等client配置信息組成,TokenRequest則由請(qǐng)求中其他參數(shù)實(shí)例化而成,具體如下圖所示

springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式

相關(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("請(qǐng)求頭中沒有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對(duì)應(yīng)的信息不存在" + clientId);
        } else if (!StringUtils.equals(clientSecret, clientDetails.getClientSecret())) {
            throw new UnapprovedClientAuthenticationException("clientSecret信息不匹配" + clientSecret);
        }

        //構(gòu)建自己的tokenRequest,由于這里不能使用OAuth3中的四種授權(quán)模式,因此這里第四個(gè)參數(shù)設(shè)置為"customer"
        //同理,第一個(gè)參數(shù)主要用于組裝并生成Authentication,而這里的Authentication已經(jīng)通過參數(shù)傳遞進(jìn)來,因此可以直接賦一個(gè)空的Map
        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "customer");

        //構(gòu)建OAuth3Request
        OAuth3Request oAuth3Request = tokenRequest.createOAuth3Request(clientDetails);
		//構(gòu)建 OAuth3Authentication
        OAuth3Authentication oAuth3Authentication = new OAuth3Authentication(oAuth3Request, authentication);
		//生成accessToken,這里依舊使用的是spring security oauth中默認(rèn)的DefaultTokenService
        OAuth3AccessToken accessToken = authenticationServerTokenServices.createAccessToken(oAuth3Authentication);
        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:解碼請(qǐng)求頭中的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的短信驗(yàn)證碼登錄

之前提到過,由于基于token的認(rèn)證交互,其實(shí)不一定會(huì)有session會(huì)話的概念,如果我們的驗(yàn)證碼依舊存于session中,則并不能正常校驗(yàn),因此在基于token的短信驗(yàn)證碼登錄的重構(gòu)中,我們唯一要做的,就是將驗(yàn)證碼存于Redis等緩存中間件中,驗(yàn)證碼的key值為deviceid。

方案比較簡單,這里只貼出Redis操作驗(yàn)證碼的方法

/**
 * 基于redis的驗(yàn)證碼存取器,避免由于沒有session導(dǎo)致無法存取驗(yàn)證碼的問題
 */
@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("請(qǐng)?jiān)谡?qǐng)求頭中攜帶deviceId參數(shù)");
		}
		return "code:" + type.toString().toLowerCase() + ":" + deviceId;
	}

}

基于token的社交登錄

在調(diào)通微信社交登錄之后,再進(jìn)行總結(jié),只是需要明確的是,這里分為兩種情況,一種是簡化模式,一種是標(biāo)準(zhǔn)的OAuth3授權(quán)模式(這兩種的區(qū)別,在QQ登錄和微信登錄流程中有詳細(xì)的體現(xiàn))。

簡化的OAuth的授權(quán)改造

簡化的OAuth模式,OAuth協(xié)議簡化的認(rèn)證模式,與標(biāo)準(zhǔn)最大的不同,其實(shí)就是在獲取授權(quán)碼的時(shí)候,順帶將openId(第三方用戶id)和accessToken(獲取用戶信息的令牌),在這種前后端徹底分離的架構(gòu)中,前三步前端可以通過服務(wù)提供商的SDK完成openId和AccessToken的獲取。但是并不能根據(jù)openId作為我們自己登錄系統(tǒng)憑證,因此我們需要提供一個(gè)根據(jù)openId進(jìn)行登錄的方式這個(gè)與之前短信登錄方式大同小異

springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式

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());
        }
		//獲取請(qǐng)求中的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進(jìn)行認(rèn)證
        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é)果

springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式

標(biāo)準(zhǔn)的OAuth授權(quán)改造

標(biāo)準(zhǔn)的OAuth模式

springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式

針對(duì)標(biāo)準(zhǔn)的授權(quán)模式,我們并不需要做多少改動(dòng),因?yàn)樵谏缃坏卿浤且还?jié)中我們已經(jīng)做了相關(guān)開發(fā),只是需要說明的是,只是在spring-social的過濾器——SocialAuthenticationFilter中,在正常社交登錄流程完成之后會(huì)默認(rèn)跳轉(zhuǎn)到某個(gè)頁面,而這個(gè)并不適用于前后端分離的項(xiàng)目,因此要針對(duì)這個(gè)問題定制化解決。這需要回到之前SocialAuthenticationFilter加入到認(rèn)證過濾器鏈上的代碼。之前我們說過社交登錄的過濾器鏈不需要我們手動(dòng)配置,只需要初始化SpringSocialConfiguer的時(shí)候,會(huì)自動(dòng)加入到社交登錄的認(rèn)證過濾器鏈上

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
	@Bean
	public SpringSocialConfigurer selfSocialSecurityConfig(){ 
		SpringSocialConfigurer selfSpringSocialConfig = new SpringSocialConfigurer();
		return selfSpringSocialConfig;
	}
}

我們只需要改變SocialAuthenticationFilter的默認(rèn)處理即可,因此我們給他加一個(gè)后置處理器,但是這個(gè)后置處理器是在SpringSocialConfigurer的postProcess函數(shù)中進(jìn)行處理

/**
 * 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);

        //指定第三方用戶信息認(rèn)證不存在的注冊(cè)頁
        selfSpringSocialConfig.signupUrl(securityProperties.getBrowser().getSiguUpPage());
        selfSpringSocialConfig.setConnectionSignUp(connectionSignUp);
        selfSpringSocialConfig.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        return selfSpringSocialConfig;
    }
}

我們自定義的過濾器后置處理器如下

/**
 * autor:liman
 * createtime:2021/8/7
 * comment:APP社交登錄認(rèn)證后置處理器
 */
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {

    @Autowired
    private AuthenticationSuccessHandler selfAuthenticationSuccessHandler;

    @Override
    public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
        socialAuthenticationFilter.setAuthenticationSuccessHandler(selfAuthenticationSuccessHandler);
    }
}

關(guān)于用戶的綁定

這里需要總結(jié)一下之前的社交登錄中用戶注冊(cè)綁定的操作。

之前的社交登錄綁定用戶

在之前的社交登錄中,如果spring social發(fā)現(xiàn)用戶是第一次登錄,則會(huì)跳轉(zhuǎn)到相關(guān)的頁面,這個(gè)頁面我們其實(shí)也可以自己定義并配置

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
    @Bean
    public SpringSocialConfigurer selfSocialSecurityConfig(){
        String processFilterUrl = securityProperties.getSocial().getProcessFilterUrl();
        SelfSpringSocialConfig selfSpringSocialConfig = new SelfSpringSocialConfig(processFilterUrl);

        //指定第三方用戶信息認(rèn)證不存在的注冊(cè)頁
        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));
}
}

我們配置的代碼中,可以自定義頁面路徑,我們自定義頁面如下(一個(gè)簡單的登錄綁定頁面)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
	<h3>Demo注冊(cè)頁</h3>
	
	<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">注冊(cè)</button>
					<button type="submit" name="type" value="binding">綁定</button>
				</td>
			</tr>
		</table>
	</form>
</body>
</html>

在用戶第一次跳轉(zhuǎn)到這個(gè)頁面的用戶選擇注冊(cè),或者綁定,都會(huì)請(qǐng)求/user/register接口,這個(gè)接口借助providerSignInUtils完成會(huì)話中的用戶數(shù)據(jù)更新

@Autowired
private ProviderSignInUtils providerSignInUtils;

@PostMapping("/register")
public void userRegister(@RequestBody User user, HttpServletRequest request) {

    //利用providerSignInUtils,將注冊(cè)之后的用戶信息,關(guān)聯(lián)到會(huì)話中
    providerSignInUtils.doPostSignUp(user.getId(),new ServletWebRequest(request));

}

在跳轉(zhuǎn)之前,spring social已經(jīng)幫我們將用戶信息存入會(huì)話(在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) {
			//這里就是將社交用戶信息存入會(huì)話
			// 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;
	}
}

但是基于前后端分離,且并沒有會(huì)話對(duì)象交互的系統(tǒng),這種方式并不適用,因?yàn)椴⒉淮嬖跁?huì)話,如何處理,需要用其他方案,其實(shí)我們可以在驗(yàn)證碼登錄的改造中受到啟發(fā),將用戶數(shù)據(jù)存入會(huì)話即可,我們自定義實(shí)現(xiàn)一個(gè)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ù)庫中的信息進(jìn)行綁定
     * @param request
     * @param userId
     */
    public void doPostSignUp(WebRequest request,String userId){
        String key = getKey(request);
        if(!redisTemplate.hasKey(key)){
            throw new RuntimeException("無法找到緩存的用戶社交賬號(hào)信息");
        }
        ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);

        //根據(jù)ConnectionData實(shí)例化創(chuàng)建一個(gè)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ù)寫掉原來的配置類

為了避免對(duì)原有代碼的侵入性處理,這里我們需要自定義一個(gè)實(shí)現(xiàn)BeanPostProcessor接口的類

/**
 * autor:liman
 * createtime:2021/8/7
 * comment:由于app端的社交用戶綁定,不能采用跳轉(zhuǎn),也不能操作會(huì)話,需要用自定義的providerSignUpUtils工具類
 * 因此需要定義一個(gè)后置處理器,針對(duì)SpringSocialConfigurer進(jìn)行一些后置處理
 */
@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;
    }
}

針對(duì)上述的請(qǐng)求路徑,我們也要寫一個(gè)對(duì)應(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模式】開始獲取會(huì)話中的第三方用戶信息");
        //先從其中拿出數(shù)據(jù),畢竟這個(gè)時(shí)候還沒有完全跳轉(zhuǎn),下一個(gè)會(huì)話,就沒有該數(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;
    }

}

對(duì)于用戶注冊(cè)的接口也需要做調(diào)整

@PostMapping("/register")
public void userRegister(@RequestBody User user, HttpServletRequest request) {

    //如果是瀏覽器的應(yīng)用利用providerSignInUtils,將注冊(cè)之后的用戶信息,關(guān)聯(lián)到會(huì)話中
    providerSignInUtils.doPostSignUp(user.getId(),new ServletWebRequest(request));
	//如果是app的應(yīng)用,則利用appSignUpUtils 將注冊(cè)之后的用戶信息,關(guān)聯(lián)到會(huì)話中
    appSignUpUtils.doPostSignUp(new ServletWebRequest(request),user.getId());

}

以上是“springsecurity如何實(shí)現(xiàn)基于token的認(rèn)證方式”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI