以帆软fineReport为例,它本质上是一个Web项目,自然少不了相关的servlet,filter等相关概念。
第一步:一个要求登录的请求当没有登录时,如果重定向到登录请求的。
帆软报表里的每个请求都会进入到com.fr.third.springframework.web.servlet.DispatcherServlet的doDispatch中。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
try {
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
this.applyDefaultViewName(request, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var27) {
dispatchException = var27;
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception var28) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var28);
} catch (Error var29) {
this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var29);
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
return;
} else {
if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
}

1 if (!mappedHandler.applyPreHandle(processedRequest, response)) 这里 会进入到com.fr.decision.webservice.interceptor.preHandle方法,如果没通过后面就不走下去了。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
EventDispatcher.fire(RequestBeginEndEvent.REQUEST_BEGIN, new RequestBeginEndEventInfo(request, response));
HandlerMethod handlerMethod = (HandlerMethod)handler;
RequestChecker checker = PreHandlerFactory.getInstance().getRequestChecker(request, handlerMethod);
return checker.checkRequest(request, response, handlerMethod);
}

PreHandlerFactory.getInstance().getRequestChecker(request, handlerMethod); 默认会返回com.fr.decision.webservice.interceptor.handler.DecisionRequestChecker对象,如果我们想要改变DecisionRequestChecker的逻辑,可以使用 PreHandlerFactory.getInstance().registerRequestCheckers添加我们自己的RequestChecker,
看看DecisionRequestChecker的checkRequest方法:
public boolean checkRequest(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
LoginStatusValidator loginStatusValidator = this.getLoginStatusValidator(handlerMethod);
if (!loginStatusValidator.isNeedCheck()) {
return true;
} else if (!SystemService.getInstance().checkSystemInit()) {
PreHandlerFactory.getInstance().getRequestInterceptorAction(request).dealServerInitStatus(response);
return false;
} else {
LoginClientBean loginClientBean = this.checkLogin(request, response, handlerMethod, loginStatusValidator);
if (loginClientBean == null) {
return false;
} else {
this.detectVisit(handlerMethod, loginClientBean.getUserId());
this.checkFunctionSupport(handlerMethod);
this.checkWebAppName(request);
return true;
}
}
}

1 this.getLoginStatusValidator(handlerMethod) 这里是检查我们当前访问的请求方法或者方法所在的类上是不是加了LoginStatusChecker注解,比如,在Controller上加上:
@LoginStatusChecker(
required = false
)
表示该controller上的方法丢不需要登录验证。
2 检查系统是否初始化完成,如果没有,返回false
3 检查登录状态,如果没有登录,返回false,如果登录了,检查visit状态,visit是检查我们的方法是否添加了VisitRefer注解,这个注解是对登录用户的userid来做判断。看是否该用户可以访问这个请求
下面看看this.checkLogin(request, response, handlerMethod, loginStatusValidator);
LoginClientBean checkLogin(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, LoginStatusValidator loginStatusValidator) throws Exception {
try {
TerminalHandler terminal = TerminalHandler.getTerminal(request, NetworkHelper.getDevice(request));
return LoginService.getInstance().loginStatusValid(loginStatusValidator.getTokenResource().getToken(request), terminal);
} catch (Exception var8) {
JSONPResponseBody jsonpResponseBody = (JSONPResponseBody)handlerMethod.getMethod().getAnnotation(JSONPResponseBody.class);
if (jsonpResponseBody != null) {
JSONPMethodProcessor jsonpMethodProcessor = new JSONPMethodProcessor();
jsonpMethodProcessor.handleJsonpRequest(this.getErrorMap(var8), request, response);
} else {
PreHandlerFactory.getInstance().getRequestInterceptorAction(request).dealLoginInvalidStatus(request, response, var8);
}
return null;
}
}

1如果没有登录,就会进入异常,如果该方法没有JSONResponseBody注解,就会进入到PreHandlerFactory.getInstance().getRequestInterceptorAction(request).dealLoginInvalidStatus(request, response, var8);
2 PreHandlerFactory.getInstance().getRequestInterceptorAction(request)默认返回com.fr.decision.webservice.interceptor.handler.redirect.RedirectAction,我们也可以PreHandlerFactory.getInstance().registerRequestInterceptorActions来注册自己的action。
下面来看RedirectAction的dealLoginInvalidStatus:
public void dealLoginInvalidStatus(HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception {
FineLoggerFactory.getLogger().info(ex.getMessage());
OriginUrlResponseBean originUrlResponseBean = getOriginalRedirectedUrl(request);
String originUrlKey = UUIDUtil.generate();
DecisionStatusService.originUrlStatusService().put(originUrlKey, originUrlResponseBean, (int)FSConfig.getInstance().getLoginConfig().getLoginTimeout());
String urlWithOrigin = HttpToolbox.appendQuery("${fineServletURL}/login", "origin", originUrlKey);
response.sendRedirect(TemplateUtils.render(urlWithOrigin));
}

这里就会将我们的请求重定向到login请求上。
第二步,重定向到登录后会发生什么?
当请求/login时,会进入到com.fr.web.controller.decision.api.auth.LoginResource的page方法上,该类加了如下注解:
@LoginStatusChecker(
required = false
)
public class LoginResource {

所以它里面定义的方法都不需要验证登录情况。所以直接就能进入login请求方法,该方法为:
@RequestMapping(
value = {"/login"},
method = {RequestMethod.GET},
produces = {"text/html"}
)
@ResponseBody
public String page(HttpServletRequest req, HttpServletResponse res) throws Exception {
if (LoginService.getInstance().isLogged(req) && FSConfig.getInstance().getLoginConfig().isForceRedirectAfterLogin()) {
res.sendRedirect(TemplateUtils.render("${fineServletURL}"));
return "";
} else if (AppearanceConfig.getInstance().getLoginType() == LoginAppearanceType.LOGIN_URL.toInteger()) {
String url = this.dealWithLoginUrl(AppearanceConfig.getInstance().getLoginUrl());
String origin = req.getParameter("origin");
if (origin != null) {
url = HttpToolbox.appendQuery(url, "origin", origin);
}
res.sendRedirect(url);
return "";
} else {
Map<String, Object> param = new HashMap();
ObjectMapper mapper = new ObjectMapper();
param.put("title", AppearanceConfig.getInstance().getPlatformTitle());
param.put("loginConfig", mapper.writeValueAsString(ConfigService.getInstance().getLoginAppearanceConfig()));
param.put("charset", ServerConfig.getInstance().getServerCharset());
PathGroup group = AtomBuilder.create().buildAssembleFilePath(Browser.resolve(req), LoginComponent.KEY);
param.put("styleTag", AtomBuilder.create().toHtmlTag(group.toStylePathGroup()));
param.put("scriptTag", AtomBuilder.create().toHtmlTag(group.toScriptPathGroup()));
Map<String, Object> system = new HashMap();
system.put("frontSeed", SecurityConfig.getInstance().getFrontSeed());
system.put("transmissionEncryption", SystemEncryptionManager.getInstance().getTransmissionEncryption().getType());
system.put("frontSM4Key", SM4TransmissionEncryption.getInstance().getTransmissionKey());
system.put("cloudEnabled", CloudCenterConfig.getInstance().isOnline());
system.put("urlIP", CloudCenter.getInstance().acquireConf("decision.queryip", ""));
if (AppearanceConfig.getInstance().isCopyrightInfoDisplay()) {
system.putAll(LoginService.getInstance().getCopyrightInfo(req));
}
param.put("system", mapper.writeValueAsString(system));
return WebServiceUtils.parseWebPageResourceSafe("/com/fr/web/controller/decision/entrance/resources/login.html", param);
}
}

1:判断是否已经登录了,如果登录了并且isForceRedirectAfterLogin为true,则直接重定向到主页面下。
2:判断loginType与loginUrl,如果满足直接重定向到loginUrl,这里我们可以做自己的登录界面。定制。
3:跳转到系统默认的登录界面/com/fr/web/controller/decision/entrance/resources/login.html。并且会为前端界面设置一些参数,以及样式,js脚本渲染到login.html
第三步:当渲染到前端界面/com/fr/web/controller/decision/entrance/resources/login.html,login.html大致如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>数据决策系统</title>
<!--css文件-->
<link rel="stylesheet" type="text/css" href="/webroot/decision/file?path=/com/fr/web/ui/fineui.min.css&type=plain&parser=plain"/>
<link rel="stylesheet" type="text/css" href="/webroot/decision/file?path=/com/fr/web/ui/materials.min.css&type=plain&parser=plain"/>
<link rel="stylesheet" type="text/css" href="/webroot/decision/file?path=/com/fr/web/resources/dist/login.min.css&type=plain&parser=dynamic"/>
</head>
<body>
<div id="wrapper"></div>
<script type="text/javascript">
window.Dec = window.Dec || {};
window.Dec.injection = window.Dec.injection || {};
</script>
<script type="text/javascript">
Dec.fineServletURL = "/webroot/decision";
Dec.loginConfig = JSON.parse('{\"loginImg\":false,
\"loginImgId\":\"\",
\"loginLogoImgId\":\"\",
\"loginLogoImgName\":\"\",
\"loginUrl\":\"\",
\"loginType\":0,
\"loginTitle\":\"数据决策系统\",
\"loginColor\":\"#3685F2\",
\"loginPages\":[],
\"loginPageId\":\"\",
\"cookiePath\":\"\/\",
\"copyrightInfoDisplay\":true
}');
Dec.system = JSON.parse('{
\"urlIP\":\"https:\/\/cloud.fanruan.com\/api\/query\/ip?timeout=10000\",
\"loginCopyright\":\"Powered by 帆软\",
\"transmissionEncryption\":2,
\"frontSM4Key\":\"46910b85bcb2caf4db5a5dbabec3ccfa\",
\"frontSeed\":\"FGljWYEQgXJGEhTP\",
\"cloudEnabled\":true,
\"url\":\"http:\/\/www.fanruan.com\/?utm_source=frexe&utm_medium=trial&utm_campaign=platform\",
\"templateCopyright\":\"上BI选帆软,专注BI十五年。 Powered by 帆软\"
}');
}
</script>
<script type="text/javascript" src="/webroot/decision/file?path=/com/fr/web/ui/fineui.min.js&type=plain&parser=plain"></script> //UI框架
<script type="text/javascript" src="/webroot/decision/file?path=com.fr.decision.web.i18n.I18nTextGenerator&type=class&parser=plain"></script>
<script type="text/javascript" src="/webroot/decision/file?path=com.fr.decision.web.constant.ConstantGenerator&type=class&parser=plain"></script>
<script type="text/javascript" src="/webroot/decision/file?path=/com/fr/web/ui/materials.min.js&type=plain&parser=plain"></script>
<script type="text/javascript" src="/webroot/decision/file?path=/com/fr/web/resources/dist/login.min.js&type=plain&parser=plain"></script> //登录界面构建
<script>
Dec.start();
</script>
</body>
</html>

这个界面是通过帆软的 fineUI框架来构建页面的。当点击登录按钮时,会调用POST的/login请求来登录,当登录成功后会将返回的token写入cookie,界面定位到返回得originUrlResponse.originUrl上。