/*
* Copyright (C) 2013-2017 NTT DATA Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.terasoluna.gfw.web.token.transaction;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.WebUtils;
import org.terasoluna.gfw.web.token.TokenStringGenerator;
/**
* {@code HandlerInterceptor} implementation class that introduces TransactionTokenCheck functionality for each incoming HTTP
* Request.
*/
public class TransactionTokenInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory
.getLogger(TransactionTokenInterceptor.class);
/**
* attribute name of {@link TransactionTokenContext} in the request scope
*/
public static final String TOKEN_CONTEXT_REQUEST_ATTRIBUTE_NAME = TransactionTokenInterceptor.class
.getName()
+ ".TOKEN_CONTEXT";
/**
* attribute name of next {@link TransactionToken} in the request scope
*/
public static final String NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME = TransactionTokenInterceptor.class
.getName()
+ ".NEXT_TOKEN";
/**
* request parameter of token value to check
*/
public static final String TOKEN_REQUEST_PARAMETER = "_TRANSACTION_TOKEN";
/**
* invalid token constant
*/
private static final TransactionToken INVALID_TOKEN = new TransactionToken(null, null, null) {
/**
* serial version UID.
*/
private static final long serialVersionUID = 674844591801033738L;
@Override
public boolean valid() {
return false;
}
};
/**
* Store for TransactionTokenInfo objects
*/
private final TransactionTokenInfoStore tokenInfoStore;
/**
* Store for TransactionToken objects
*/
private final TransactionTokenStore tokenStore;
/**
* Token string generator
*/
private final TokenStringGenerator generator;
/**
* Constructor that takes sizePerTokenName.
* <p>
* sizePerTokenName is the number of tokenKeys that are allowed to be generated per tokenName.<br>
* {@link TokenStringGenerator} is used to generate token string.<br>
* {@link TransactionTokenInfoStore} is used to store {@link TransactionTokenInfo}.<br>
* {@link HttpSessionTransactionTokenStore} is used to store transaction tokens. Default size per tokenName(namespace) is
* used.
* </p>
* @see HttpSessionTransactionTokenStore#HttpSessionTransactionTokenStore()
*/
public TransactionTokenInterceptor() {
this(new TokenStringGenerator(), new TransactionTokenInfoStore(),
new HttpSessionTransactionTokenStore());
}
/**
* Constructor that takes sizePerTokenName.
* <p>
* sizePerTokenName is the number of tokenKeys that are allowed to be generated per tokenName.<br>
* {@link TokenStringGenerator} is used to generate token string.<br>
* {@link TransactionTokenInfoStore} is used to store {@link TransactionTokenInfo}.<br>
* {@link HttpSessionTransactionTokenStore} is used to store transaction tokens. The size per tokenName(namespace) is given.
* </p>
* @param sizePerTokenName size per tokenName(must be greater than 0)
* @see HttpSessionTransactionTokenStore#HttpSessionTransactionTokenStore(int)
*/
public TransactionTokenInterceptor(int sizePerTokenName) {
this(new TokenStringGenerator(), new TransactionTokenInfoStore(),
new HttpSessionTransactionTokenStore(sizePerTokenName));
}
/**
* Constructor that takes tokenStringGenerator, transactionTokenInfoStore and transactionTokenStore as parameters
* @param generator token string generator
* @param tokenInfoStore store for {@link TransactionTokenInfo}
* @param tokenStore store for {@link TransactionToken}
*/
public TransactionTokenInterceptor(TokenStringGenerator generator,
TransactionTokenInfoStore tokenInfoStore,
TransactionTokenStore tokenStore) {
this.generator = generator;
this.tokenInfoStore = tokenInfoStore;
this.tokenStore = tokenStore;
}
/**
* Validates the token received from request. <br>
* <p>
* If token check passes, sets context information of the token in the "TransactionTokenInterceptor.TOKEN_CONTEXT" request
* attribute and returns true.
* <p>
* This method expects the handler argument to be an instance of <code>HandlerMethod</code> class. If the handler is not an
* instance of <code>HandlerMethod</code> class, the method returns true without executing the validation. <br>
* @throws InvalidTransactionTokenException in case of Transaction Token Validation error.<br>
* @see org.springframework.web.servlet.HandlerInterceptor#preHandle(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse, java.lang.Object)
*/
@Override
public boolean preHandle(final HttpServletRequest request,
final HttpServletResponse response, final Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
logger.trace("preHandle");
HandlerMethod handlerMethod = (HandlerMethod) handler;
TransactionTokenInfo tokenInfo = tokenInfoStore
.getTransactionTokenInfo(handlerMethod);
TransactionToken receivedToken = INVALID_TOKEN;
if (tokenInfo.needValidate()) {
receivedToken = createReceivedToken(request);
if (!validateToken(receivedToken, tokenStore, tokenInfo)) {
processTransactionTokenError(receivedToken);
}
}
if (tokenInfo.getTransactionTokenType() == TransactionTokenType.BEGIN) {
// This logic is added later to remove existing token sent in the request in case of Transaction BEGIN
// When transactions BEGIN, the transaction token sent from the request are usually those which are generated
// due to input errors. (Since method with BEGIN may be called in spite of input errors, and token will be generated
// in the intercepter postHandle method execution.
// This logic is added here to minimize the impact to any other existing logic
String tokenStr = request.getParameter(TOKEN_REQUEST_PARAMETER);
if (null != tokenStr) {
removeToken(new TransactionToken(tokenStr));
}
}
TransactionTokenContextImpl tokenContext = new TransactionTokenContextImpl(tokenInfo, receivedToken);
request.setAttribute(TOKEN_CONTEXT_REQUEST_ATTRIBUTE_NAME, tokenContext);
return true;
}
protected void processTransactionTokenError(TransactionToken receivedToken) {
removeToken(receivedToken);
throw new InvalidTransactionTokenException();
}
/**
* Retrieves the value of transactionToken fetched from a request parameter
* @param request
* @return currentToken transactionToken received from the request
*/
TransactionToken createReceivedToken(final HttpServletRequest request) {
String tokenStr = request.getParameter(TOKEN_REQUEST_PARAMETER);
TransactionToken currentToken = new TransactionToken(tokenStr);
return currentToken;
}
/**
* Validates the instance of receivedToken. <br>
* <p>
* Returns false if the instance of receivedToken is not valid or token namespace of the target method and passed token does
* not match. <br>
* Returns true if <br>
* a) it is saved in tokenStore AND <br>
* b) Value of receivedToken is same as the value of token stored in tokenStore <br>
* Returns false otherwise <br>
* <p>
* Take note that once a token is fetched from tokenStore, its value is cleared in tokenStore. Hence this method can return
* true only once. All further invocations to this method for the same receivedToken will return false.
* @param receivedToken
* @param tokenStore
* @param tokenInfo
* @return if valid token, return true
*/
boolean validateToken(final TransactionToken receivedToken,
final TransactionTokenStore tokenStore,
final TransactionTokenInfo tokenInfo) {
if (receivedToken.valid()
&& receivedToken.getTokenName()
.equals(tokenInfo.getTokenName())) {
String storedToken = tokenStore.getAndClear(receivedToken);
if (storedToken != null
&& storedToken.equals(receivedToken.getTokenValue())) {
return true;
}
}
return false;
}
/**
* Based on context information from the request attribute named <code>TransactionTokenInterceptor.TOKEN_CONTEXT</code>,
* creates, updates or keeps the token stored with the request attribute <code>TransactionTokenInterceptor.NEXT_TOKEN</code>
* and also in the <code>TransactionTokenStore</code> or removes the token from <code>TransactionTokenStore</code>
* <p>
* modelAndView is not used in the implementation
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance examination
* @param modelAndView the <code>ModelAndView</code> that the handler returned (can also be <code>null</code>)
* @see org.springframework.web.servlet.HandlerInterceptor#postHandle(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse, java.lang.Object, org.springframework.web.servlet.ModelAndView)
*/
@Override
public void postHandle(final HttpServletRequest request,
final HttpServletResponse response, final Object handler,
final ModelAndView modelAndView) {
logger.trace("postHandle");
if (!(handler instanceof HandlerMethod)) {
return;
}
TransactionTokenContextImpl tokenContext = (TransactionTokenContextImpl) request
.getAttribute(TOKEN_CONTEXT_REQUEST_ATTRIBUTE_NAME);
switch (tokenContext.getReserveCommand()) {
case CREATE_TOKEN:
createToken(request, request.getSession(true), tokenContext
.getTokenInfo(), generator, tokenStore);
break;
case UPDATE_TOKEN:
updateToken(request, request.getSession(true), tokenContext
.getReceivedToken(), tokenContext.getTokenInfo(),
generator, tokenStore);
break;
case REMOVE_TOKEN:
removeToken(tokenContext.getReceivedToken());
break;
case KEEP_TOKEN:
keepToken(request, tokenContext.getReceivedToken(), tokenContext
.getTokenInfo(), tokenStore);
break;
default:
// noop
break;
}
}
/**
* If exception occurred during request processing, the token is removed from request as well as
* <code>TransactionTokenStore</code>
* <p>
* Token Context is fetched from the request attribute named <code>TransactionTokenInterceptor.TOKEN_CONTEXT</code>
* Arguments <code>response</code> and <code>handler</code> are not used in this implementation
* @see org.springframework.web.servlet.HandlerInterceptor#afterCompletion(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse, java.lang.Object, java.lang.Exception)
*/
@Override
public void afterCompletion(final HttpServletRequest request,
final HttpServletResponse response, final Object handler,
final Exception ex) {
logger.trace("afterCompletion");
if (ex != null) {
TransactionTokenContextImpl tokenContext = (TransactionTokenContextImpl) request
.getAttribute(TOKEN_CONTEXT_REQUEST_ATTRIBUTE_NAME);
if (tokenContext != null) {
TransactionToken token = tokenContext.getReceivedToken();
removeToken(token);
}
}
}
/**
* Updates the value of existing token in <code>TransactionTokenStore</code> as well as in the request
* <p>
* Updated <code>TransactionToken</code> instance is set to request attribute
* <code>TransactionTokenInterceptor.NEXT_TOKEN</code>.
* <p>
* @param request
* @param session
* @param receivedToken
* @param tokenInfo
* @param generator
* @param tokenStore
*/
void updateToken(HttpServletRequest request, HttpSession session,
TransactionToken receivedToken, TransactionTokenInfo tokenInfo,
TokenStringGenerator generator, TransactionTokenStore tokenStore) {
TransactionToken nextToken = new TransactionToken(tokenInfo
.getTokenName(), receivedToken.getTokenKey(), generator
.generate(session.getId()));
tokenStore.store(nextToken);
request.setAttribute(NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME, nextToken);
}
/**
* Creates a new <code>TransactionToken</code> <br>
* <p>
* Generated <code>TransactionToken</code> instance is stored in <code>TransactionTokenStore</code> and also set to request
* attribute <code>TransactionTokenInterceptor.NEXT_TOKEN</code>.
* <p>
* @param request
* @param session
* @param tokenInfo TransactionTokenInfo
* @param generator TokenStringGenerator
* @param tokenStore TransactionTokenStore
*/
void createToken(HttpServletRequest request, HttpSession session,
TransactionTokenInfo tokenInfo, TokenStringGenerator generator,
TransactionTokenStore tokenStore) {
TransactionToken nextToken;
synchronized (WebUtils.getSessionMutex(session)) {
String tokenKey = tokenStore.createAndReserveTokenKey(tokenInfo
.getTokenName());
nextToken = new TransactionToken(tokenInfo.getTokenName(), tokenKey, generator
.generate(session.getId()));
tokenStore.store(nextToken);
}
request.setAttribute(NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME, nextToken);
}
/**
* Removes the receivedToken received as parameter to this method, from the tokenStore
* @param receivedToken
*/
void removeToken(TransactionToken receivedToken) {
if (receivedToken.valid()) {
tokenStore.remove(receivedToken);
}
}
/**
* Set the receivedToken to the attributes named <code>TransactionTokenInterceptor.NEXT_TOKEN</code> of request. And
* receivedToken stored in <code>TransactionTokenStore</code> without updates.
* @param request current HTTP request
* @param receivedToken {@link TransactionToken} got from the current HTTP request
* @param tokenInfo meta-information about a TransactionToken
* @param tokenStore store for {@link TransactionToken}
*/
void keepToken(HttpServletRequest request, TransactionToken receivedToken,
TransactionTokenInfo tokenInfo, TransactionTokenStore tokenStore) {
tokenStore.store(receivedToken);
request.setAttribute(NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME, receivedToken);
}
}