1. 程式人生 > >Spring Security框架下Restful Token的驗證方案

Spring Security框架下Restful Token的驗證方案

false rri blob 返回 sch date html 官方 form

項目使用Restful的規範,權限內容的訪問,考慮使用Token驗證的權限解決方案。

驗證方案(簡要概括):

首先,用戶需要登陸,成功登陸後返回一個Token串;

然後用戶訪問有權限的內容時需要上傳Token串進行權限驗證

代碼方案:

Spring MVC + Spring Security + Redis的框架下實現權限驗證,此文重點談談Spring Security下的Token驗證實現。

首先,看看spring security的配置:

<http pattern="/service/secure/**"
          entry-point-ref="serviceUnauthorizedEntryPoint"
create-session="stateless"> <!-- Added after moving to Spring Boot 1.3 + Spring Security 4.x, otherwise we could not login with basic auth because of: Expected CSRF token not found TODO: Please, mind, that I did not migrate this XML to Spring Security 4.x except for this element
--> <csrf disabled="true"/> <intercept-url pattern="/service/secure/admin/login*" access="permitAll"/> <custom-filter ref="preTokenAuthenticationFilter" before="PRE_AUTH_FILTER" /> </http>

接下來詳細說明配置以及訪問流程:

1. 考慮到支持Restful規範,所以spring security需要設置create-session為stateless狀態

2. 當訪問權限驗證失敗是,根據Restful規範返回401 Unauthorized,因此需要設定entry-point-ref,重新指向一個自定義的entrypoint如下:

public class ServiceUnauthorizedEntryPoint implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(ServiceTokenAuthenticationFilter.class);
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException arg2) throws IOException, ServletException {
        // return 401 UNAUTHORIZED status code if the user is not authenticated
        logger.debug(" *** UnauthorizedEntryPoint.commence: " + request.getRequestURI());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }

}

3. Token的驗證重點在於PRE_AUTH_FILTER的攔截器

關於FllterChain請看官方解釋security-filter-chain

另外關於PRE_AUTH_FILTER攔截器的官方解釋preauth,我們在此就采取已經被可靠的驗證系統驗證過的流程即驗證Token的合法性,我們看一下這個攔截器的bean設置

<b:bean id="preTokenAuthenticationFilter"
            class="com.will.security.token.PreRequestHeaderAuthenticationFilter">
            <b:property name="authenticationManager" ref="preAuthenticationManager" />
            <b:property name="authenticationFailureHandler" ref="authFailureHandler"/>
            <b:property name="principalRequestHeader" value="X-Auth-Token"/>
            <b:property name="continueFilterChainOnUnsuccessfulAuthentication" value="false" />
    </b:bean>
    
     <!-- PreAuthentication manager. -->
    <b:bean id="authFailureHandler" class="org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler" >
         <b:constructor-arg value="/service/secure/admin/login/failed" />
    </b:bean>
    
    <b:bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
        <b:property name="preAuthenticatedUserDetailsService">
            <b:bean id="userDetailsServiceWrapper"  class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
                <b:property name="userDetailsService" ref="tokenUserDetailsService"/>
            </b:bean>
        </b:property>
        <b:property name="userDetailsChecker">
            <b:bean id="tokenUserDetailsChecker" class="com.will.security.token.TokenUserDetailsChecker" />
        </b:property>
    </b:bean>
    <authentication-manager id="preAuthenticationManager">
         <authentication-provider ref="preauthAuthProvider" />
    </authentication-manager>

PreRequestHeaderAuthenticationFilter裏,截取訪問的request,然後獲取上傳的Token串,這裏的Token串儲存在“SM_UER”的header裏,
代碼如下:
public class PreRequestHeaderAuthenticationFilter extends
        AbstractCustomPreAuthenticatedProcessingFilter {

    private String principalRequestHeader = "SM_USER";
    private String credentialsRequestHeader;
    private boolean exceptionIfHeaderMissing = true;
    
    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
     /*獲取principal信息*/ String principal
= request.getHeader(principalRequestHeader); if (principal == null && exceptionIfHeaderMissing) { // 對於request進行BadException處理 request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new BadCredentialsException("No pre-authenticated credentials found in request.")); return "N/A"; } return principal; } @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { if (credentialsRequestHeader != null) { return request.getHeader(credentialsRequestHeader); } return "N/A"; } public void setPrincipalRequestHeader(String principalRequestHeader) { Assert.hasText(principalRequestHeader, "principalRequestHeader must not be empty or null"); this.principalRequestHeader = principalRequestHeader; } public void setCredentialsRequestHeader(String credentialsRequestHeader) { Assert.hasText(credentialsRequestHeader, "credentialsRequestHeader must not be empty or null"); this.credentialsRequestHeader = credentialsRequestHeader; } /** * Defines whether an exception should be raised if the principal header is missing. * Defaults to {@code true}. * * @param exceptionIfHeaderMissing set to {@code false} to override the default * behaviour and allow the request to proceed if no header is found. */ public void setExceptionIfHeaderMissing(boolean exceptionIfHeaderMissing) { this.exceptionIfHeaderMissing = exceptionIfHeaderMissing; } }

如果需要詳細了解認證流程建議查看PreAuthenticatedAuthenticationProvider的源碼,對於provider的配置也就一目了然了

TokenUserDetailsService的代碼如下

public class TokenUserDetailsService implements UserDetailsService {

    private TokenManager tokenManager;
    
    @Override
    public UserDetails loadUserByUsername(String token)
            throws UsernameNotFoundException {
        if (token.equalsIgnoreCase("N/A")) {
            return null;
        }
        
        return tokenManager != null ? tokenManager.getUserDetails(token) : null;
    }
    
    public void setTokenManager(TokenManager tm) {
        this.tokenManager = tm;
    }

}

TokenManager負責Token的生成,驗證以及刪除等等操作

public interface TokenManager {

    /**
     * Creates a new token for the user and returns its {@link TokenInfo}.
     * It may add it to the token list or replace the previous one for the user. Never returns {@code null}.
     */
    TokenInfo createNewToken(UserDetails userDetails);

    /** Removes all tokens for user. */
    //void removeUserDetails(UserDetails userDetails);

    /** Removes a single token. */
    UserDetails removeToken(String token);

    /** Returns user details for a token. */
    UserDetails getUserDetails(String token);
    
    /** Returns user details for a username. */
    UserDetails getUserDetailsByUsername(String username);

    /** Returns a collection with token information for a particular user. */
    Collection<TokenInfo> getUserTokens(UserDetails userDetails);
    
    
    Boolean validateToken(String token);
    
}

我們可以根據自己的需要比如借助Redis做緩存,或者使用JWT等等,具體可實現自己的TokenManager

Spring Security框架下Restful Token的驗證方案