From 009fe7b21fe285fc4ed24bdf76a49aaace8db31d Mon Sep 17 00:00:00 2001 From: hugh Date: Tue, 18 May 2021 11:35:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4demo=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- build.gradle | 122 +++++++++++++ encrypt.xml | 13 ++ plugin.xml | 18 ++ .../course/hg/login/username/LoginFilter.java | 76 ++++++++ .../hg/login/username/LoginMd5Filter.java | 35 ++++ .../username/oauth2/DecisionDomainLogin.java | 30 ++++ .../username/oauth2/EntryDomainLogin.java | 30 ++++ .../hg/login/username/oauth2/OAuth2Login.java | 114 ++++++++++++ .../username/oauth2/ReportDomainLogin.java | 33 ++++ .../login/username/oauth2/WeChatConfig.java | 46 +++++ .../com/tptj/project/hg/sso/BaseUtils.java | 20 +++ .../tptj/project/hg/sso/LoginProvider.java | 167 ++++++++++++++++++ .../com/tptj/project/hg/sso/ReLoader.java | 9 + .../project/hg/sso/RedirectException.java | 20 +++ .../tptj/project/hg/sso/SkipException.java | 10 ++ .../project/hg/sso/SpLoginClientBean.java | 31 ++++ .../tptj/project/hg/sso/TimeoutObject.java | 29 +++ .../project/hg/sso/TimeoutObjectHolder.java | 61 +++++++ 19 files changed, 868 insertions(+), 1 deletion(-) create mode 100644 build.gradle create mode 100644 encrypt.xml create mode 100644 plugin.xml create mode 100644 src/main/java/com/tptj/course/hg/login/username/LoginFilter.java create mode 100644 src/main/java/com/tptj/course/hg/login/username/LoginMd5Filter.java create mode 100644 src/main/java/com/tptj/course/hg/login/username/oauth2/DecisionDomainLogin.java create mode 100644 src/main/java/com/tptj/course/hg/login/username/oauth2/EntryDomainLogin.java create mode 100644 src/main/java/com/tptj/course/hg/login/username/oauth2/OAuth2Login.java create mode 100644 src/main/java/com/tptj/course/hg/login/username/oauth2/ReportDomainLogin.java create mode 100644 src/main/java/com/tptj/course/hg/login/username/oauth2/WeChatConfig.java create mode 100644 src/main/java/com/tptj/project/hg/sso/BaseUtils.java create mode 100644 src/main/java/com/tptj/project/hg/sso/LoginProvider.java create mode 100644 src/main/java/com/tptj/project/hg/sso/ReLoader.java create mode 100644 src/main/java/com/tptj/project/hg/sso/RedirectException.java create mode 100644 src/main/java/com/tptj/project/hg/sso/SkipException.java create mode 100644 src/main/java/com/tptj/project/hg/sso/SpLoginClientBean.java create mode 100644 src/main/java/com/tptj/project/hg/sso/TimeoutObject.java create mode 100644 src/main/java/com/tptj/project/hg/sso/TimeoutObjectHolder.java diff --git a/README.md b/README.md index d2a257e..db3ebe9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # video-demo-sso-login -登录视频课程一讲demo \ No newline at end of file +登录视频课程一讲demo\ +[专题文档](https://wiki.fanruan.com/pages/viewpage.action?pageId=53125965)\ +注:com.tptj.project.hg.sso 路径下的代码为通用的单点实现方案的一个封装,在搞清楚原理后可直接使用!\ +注:在没有弄清楚原理前请勿使用这个通用方案,以免超出个人的维护能力! \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..89632b2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,122 @@ + +apply plugin: 'java' + + +ext { + /** + * 项目中依赖的jar的路径 + * 1.如果依赖的jar需要打包到zip中,放置在lib根目录下 + * 2.如果依赖的jar仅仅是编译时需要,防止在lib下子目录下即可 + */ + libPath = "$projectDir/../webroot/WEB-INF/lib" + + /** + * 是否对插件的class进行加密保护,防止反编译 + */ + guard = true + + def pluginInfo = getPluginInfo() + pluginPre = "fine-plugin" + pluginName = pluginInfo.id + pluginVersion = pluginInfo.version + + outputPath = "$projectDir/../webroot/WEB-INF/plugins/plugin-" + pluginName + "-1.0/classes" +} + +group = 'com.fr.plugin' +version = '10.0' +sourceCompatibility = '8' + +sourceSets { + main { + java.outputDir = file(outputPath) + output.resourcesDir = file(outputPath) + } +} + +ant.importBuild("encrypt.xml") +//定义ant变量 +ant.projectDir = projectDir +ant.references["compile.classpath"] = ant.path { + fileset(dir: libPath, includes: '**/*.jar') + fileset(dir: ".",includes:"**/*.jar" ) +} + +classes.dependsOn('clean') + +task copyFiles(type: Copy,dependsOn: 'classes'){ + from outputPath + into "$projectDir/classes" +} + +task preJar(type:Copy,dependsOn: guard ? 'compile_encrypt_javas' : 'compile_plain_javas'){ + from "$projectDir/classes" + into "$projectDir/transform-classes" + include "**/*.*" +} +jar.dependsOn("preJar") + +task makeJar(type: Jar,dependsOn: preJar){ + from fileTree(dir: "$projectDir/transform-classes") + baseName pluginPre + appendix pluginName + version pluginVersion + destinationDir = file("$buildDir/libs") + + doLast(){ + delete file("$projectDir/classes") + delete file("$projectDir/transform-classes") + } +} + +task copyFile(type: Copy,dependsOn: ["makeJar"]){ + from "$buildDir/libs" + from("$projectDir/lib") { + include "*.jar" + } + from "$projectDir/plugin.xml" + into file("$buildDir/temp/plugin") +} + +task zip(type:Zip,dependsOn:["copyFile"]){ + from "$buildDir/temp/plugin" + destinationDir file("$buildDir/install") + baseName pluginPre + appendix pluginName + version pluginVersion +} + +//控制build时包含哪些文件,排除哪些文件 +processResources { +// exclude everything +// 用*.css没效果 +// exclude '**/*.css' +// except this file +// include 'xx.xml' +} + +/*读取plugin.xml中的version*/ +def getPluginInfo(){ + def xmlFile = file("plugin.xml") + if (!xmlFile.exists()) { + return ["id":"none", "version":"1.0.0"] + } + def plugin = new XmlParser().parse(xmlFile) + def version = plugin.version[0].text() + def id = plugin.id[0].text() + return ["id":id,"version":version] +} + +repositories { + mavenLocal() + maven { + url = uri('http://mvn.finedevelop.com/repository/maven-public/') + } +} + +dependencies { + //使用本地jar + implementation fileTree(dir: 'lib', include: ['**/*.jar']) + implementation fileTree(dir: libPath, include: ['**/*.jar']) +} + diff --git a/encrypt.xml b/encrypt.xml new file mode 100644 index 0000000..1401cd1 --- /dev/null +++ b/encrypt.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..17ff8f8 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,18 @@ + + com.tptj.course.hg.login.username + + yes + 1.0 + 10.0 + 2018-07-31 + tptj + + + com.tptj.course.hg.login.username + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/tptj/course/hg/login/username/LoginFilter.java b/src/main/java/com/tptj/course/hg/login/username/LoginFilter.java new file mode 100644 index 0000000..1a8afe3 --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/LoginFilter.java @@ -0,0 +1,76 @@ +package com.tptj.course.hg.login.username; + +import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider; +import com.fr.decision.webservice.bean.authentication.LoginClientBean; +import com.fr.decision.webservice.login.LogInOutResultInfo; +import com.fr.decision.webservice.utils.DecisionServiceConstants; +import com.fr.decision.webservice.v10.login.LoginService; +import com.fr.decision.webservice.v10.login.TokenResource; +import com.fr.decision.webservice.v10.login.event.LogInOutEvent; +import com.fr.event.EventDispatcher; +import com.fr.intelli.record.Focus; +import com.fr.log.FineLoggerFactory; +import com.fr.record.analyzer.EnableMetrics; +import com.fr.stable.StringUtils; +import com.fr.web.utils.WebUtils; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + **/ +@EnableMetrics +public class LoginFilter extends AbstractGlobalRequestFilterProvider { + @Override + public String filterName() { + return "login by username"; + } + + @Override + public String[] urlPatterns() { + return new String[]{"/*"}; + } + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) { + try{ + checkLogin(req,res); + }catch(Exception e){ + FineLoggerFactory.getLogger().error(e,"LoginFilter[Login By Username]: {}",e.getMessage()); + } + + try { + chain.doFilter(req,res); + }catch (Exception e){ + FineLoggerFactory.getLogger().error(e,e.getMessage()); + } + } + + protected String getUsername( HttpServletRequest req )throws Exception{ + return WebUtils.getHTTPRequestParameter(req,"username"); + } + + private void checkLogin(HttpServletRequest req, HttpServletResponse res )throws Exception{ + String username = getUsername(req); + try{ + LoginClientBean client = LoginService.getInstance().loginStatusValid(req, TokenResource.COOKIE); + String crt_username = client.getUsername(); + if( StringUtils.equals(username, crt_username) ){ + return; + } + }catch (Exception e){ + + } + login(req,res,username); + } + + @Focus(id="com.tptj.course.hg.login.username",text = "直接用用户名登录") + protected void login( HttpServletRequest req, HttpServletResponse res ,String username )throws Exception{ + String token = LoginService.getInstance().login(req,res,username); + req.setAttribute( DecisionServiceConstants.FINE_AUTH_TOKEN_NAME, token ); + EventDispatcher.fire(LogInOutEvent.LOGIN, new LogInOutResultInfo(req, res, username, true)); + } +} diff --git a/src/main/java/com/tptj/course/hg/login/username/LoginMd5Filter.java b/src/main/java/com/tptj/course/hg/login/username/LoginMd5Filter.java new file mode 100644 index 0000000..50a800c --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/LoginMd5Filter.java @@ -0,0 +1,35 @@ +package com.tptj.course.hg.login.username; + +import com.fr.stable.CodeUtils; +import com.fr.stable.StringUtils; +import com.fr.web.utils.WebUtils; +import javax.servlet.http.HttpServletRequest; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + **/ +public class LoginMd5Filter extends LoginFilter { + + private static final String SECRET = "123456"; + + protected String getUsername( HttpServletRequest req )throws Exception{ + String sign = WebUtils.getHTTPRequestParameter(req,"sign"); + String [] parts = sign.split("_"); + String username = parts[0]; + long timestamp = Long.parseLong(parts[1]); + long crt_time = System.currentTimeMillis(); + if( crt_time - timestamp > 300000 || crt_time < timestamp ){ + return StringUtils.EMPTY; + } + String source = username+timestamp+SECRET; + String md5 = CodeUtils.md5Encode(source,StringUtils.EMPTY,"MD5"); + //sign = username_timestamp_md5(username+timestamp+SECRET) + if( StringUtils.equals(parts[2],md5) ){ + return username; + } + return StringUtils.EMPTY; + } + +} diff --git a/src/main/java/com/tptj/course/hg/login/username/oauth2/DecisionDomainLogin.java b/src/main/java/com/tptj/course/hg/login/username/oauth2/DecisionDomainLogin.java new file mode 100644 index 0000000..17b2fba --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/oauth2/DecisionDomainLogin.java @@ -0,0 +1,30 @@ +package com.tptj.course.hg.login.username.oauth2; + +import com.tptj.project.hg.sso.SkipException; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 拦截平台入口,进行OAuth2单点 + **/ +public class DecisionDomainLogin extends OAuth2Login { + @Override + public String filterName() { + return "DecisionDomainLogin"; + } + + @Override + public String[] urlPatterns() { + return new String[]{ + "/decision" + }; + } + + @Override + protected boolean accept(HttpServletRequest req) throws SkipException { + return true; + } +} diff --git a/src/main/java/com/tptj/course/hg/login/username/oauth2/EntryDomainLogin.java b/src/main/java/com/tptj/course/hg/login/username/oauth2/EntryDomainLogin.java new file mode 100644 index 0000000..2e4b8f2 --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/oauth2/EntryDomainLogin.java @@ -0,0 +1,30 @@ +package com.tptj.course.hg.login.username.oauth2; + +import com.tptj.project.hg.sso.SkipException; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 拦截平台报表目录入口,进行OAuth2单点 + **/ +public class EntryDomainLogin extends OAuth2Login { + @Override + public String filterName() { + return "EntryDomainLogin"; + } + + @Override + public String[] urlPatterns() { + return new String[]{ + "/decision/v10/entry/access/*" + }; + } + + @Override + protected boolean accept(HttpServletRequest req) throws SkipException { + return true; + } +} diff --git a/src/main/java/com/tptj/course/hg/login/username/oauth2/OAuth2Login.java b/src/main/java/com/tptj/course/hg/login/username/oauth2/OAuth2Login.java new file mode 100644 index 0000000..7943b24 --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/oauth2/OAuth2Login.java @@ -0,0 +1,114 @@ +package com.tptj.course.hg.login.username.oauth2; + +import com.fr.general.http.HttpToolbox; +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.stable.StringUtils; +import com.fr.web.utils.WebUtils; +import com.tptj.project.hg.sso.*; +import javax.servlet.FilterConfig; +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + **/ +public abstract class OAuth2Login extends LoginProvider implements ReLoader { + /** + * 断言当前请求是微信自带浏览器发起的请求 + * @param req + * @throws SkipException + */ + private void assertWechatBrowser(HttpServletRequest req)throws SkipException{ + String ua = req.getHeader("User-Agent").toLowerCase(); + if( !ua.contains( "wechat" ) ){ + throw new SkipException(); + } + } + + /** + * 只有配置了微信登录的相关参数且请求由企业微信的浏览器发起,才会生效单点 + * @param req + * @return + * @throws Exception + */ + protected boolean isEffective(HttpServletRequest req)throws Exception{ + String corpid = WeChatConfig.getInstance().getCorpid(); + String secret = WeChatConfig.getInstance().getSecret(); + assertWechatBrowser(req); + return StringUtils.isNotEmpty(corpid) || StringUtils.isNotEmpty(secret); + } + + + /** + * 企业微信的token是有有效期的,我们不应该频繁的通过企业ID和密钥这种持久凭证,去交换它,否则容易产生安全隐患 + * @param old + * @return + * @throws Exception + */ + @Override + public TimeoutObject reload(TimeoutObject old)throws Exception { + String corpid = WeChatConfig.getInstance().getCorpid(); + String secret = WeChatConfig.getInstance().getSecret(); + String tokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid="+corpid+"&corpsecret="+secret; + String res = HttpToolbox.get(tokenUrl); + JSONObject resJo = new JSONObject(res); + if( 0 != resJo.optInt("errcode") ){ + throw new Exception( resJo.optString("errmsg") ); + } + if( null == old ){ + old = new TimeoutObject(); + } + old.setObject( resJo.optString("access_token") ); + //企业微信的token有效期是7200秒,我们这里为了留出足够的时间验证,扣掉了1000S + old.setTimeout( System.currentTimeMillis() + (resJo.optLong("expires_in")-1000)*1000 ); + return old; + } + + private TimeoutObjectHolder holder = null; + + @Override + public void init( FilterConfig config ) { + try { + //access_token的有效加载器,如果我们不怕安全隐患,也不担心性能,可以每次登录都重新获取token + holder = TimeoutObjectHolder.init( StringUtils.EMPTY,-1, this, OAuth2Login.class); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e,e.getMessage()); + } + } + + @Override + protected String getTag( HttpServletRequest req ) throws Exception { + //OAuth2最终登录凭证实际上就是这个code参数 + String code = WebUtils.getHTTPRequestParameter(req,"code"); + if( StringUtils.isEmpty( code ) ){ + //如果没有code我们就重定向到企业微信去获取凭证 + throw new RedirectException( initGetCodeUrl(req) ); + } + return code; + } + + @Override + protected String getUsername( HttpServletRequest req, String tag )throws Exception{ + //拿到凭证后调研企业微信的验证code的请求获取用户信息进行登录 + String token = holder.getObj(); + String res = HttpToolbox.get("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token="+token+"&code="+tag); + JSONObject resJo = new JSONObject(res); + if( 0 != resJo.optInt("errcode") ){ + throw new Exception( resJo.optString("errmsg") ); + } + return resJo.optString("UserId"); + } + + private String initGetCodeUrl(HttpServletRequest req)throws Exception{ + String crt_url = WebUtils.getOriginalURL(req); + crt_url = URLEncoder.encode(crt_url,"UTF-8"); + String corpid = WeChatConfig.getInstance().getCorpid(); + return "https://open.weixin.qq.com/connect/oauth2/authorize" + + "?appid="+corpid+"&redirect_uri="+crt_url+ + "&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect"; + } + +} diff --git a/src/main/java/com/tptj/course/hg/login/username/oauth2/ReportDomainLogin.java b/src/main/java/com/tptj/course/hg/login/username/oauth2/ReportDomainLogin.java new file mode 100644 index 0000000..91950b7 --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/oauth2/ReportDomainLogin.java @@ -0,0 +1,33 @@ +package com.tptj.course.hg.login.username.oauth2; + +import com.fr.stable.StringUtils; +import com.fr.web.utils.WebUtils; +import com.tptj.project.hg.sso.SkipException; +import javax.servlet.http.HttpServletRequest; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 拦截报表请求入口,进行OAuth2单点 + **/ +public class ReportDomainLogin extends OAuth2Login { + @Override + public String filterName() { + return "ReportDomainLogin"; + } + + @Override + public String[] urlPatterns() { + return new String[]{ + "/decision/view/form", + "/decision/view/report", + }; + } + + @Override + protected boolean accept( HttpServletRequest req ) throws SkipException { + String viewlet = WebUtils.getHTTPRequestParameter(req,"viewlet"); + return StringUtils.isNotEmpty(viewlet); + } +} diff --git a/src/main/java/com/tptj/course/hg/login/username/oauth2/WeChatConfig.java b/src/main/java/com/tptj/course/hg/login/username/oauth2/WeChatConfig.java new file mode 100644 index 0000000..756f9bd --- /dev/null +++ b/src/main/java/com/tptj/course/hg/login/username/oauth2/WeChatConfig.java @@ -0,0 +1,46 @@ +package com.tptj.course.hg.login.username.oauth2; + +import com.fr.config.*; +import com.fr.config.holder.Conf; +import com.fr.config.holder.factory.Holders; +import com.fr.stable.StringUtils; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 微信单点需要的配置 + **/ +@Visualization(category = "WeChat Login Config") +public class WeChatConfig extends DefaultConfiguration { + private static volatile WeChatConfig instance = null; + + public static WeChatConfig getInstance() { + if (instance == null) { + instance = ConfigContext.getConfigInstance(WeChatConfig.class); + } + return instance; + } + + @Identifier(value = "corpid", name = "CROP ID", description = "CROP ID", status = Status.SHOW) + private Conf corpid = Holders.simple(StringUtils.EMPTY); + + @Identifier(value = "secret", name = "SECRET", description = "SECRET", status = Status.SHOW) + private Conf secret = Holders.simple(StringUtils.EMPTY); + + public String getCorpid() { + return corpid.get(); + } + + public void setCorpid( String corpid) { + this.corpid.set(corpid); + } + + public String getSecret() { + return secret.get(); + } + + public void setSecret( String secret) { + this.secret.set(secret); + } +} diff --git a/src/main/java/com/tptj/project/hg/sso/BaseUtils.java b/src/main/java/com/tptj/project/hg/sso/BaseUtils.java new file mode 100644 index 0000000..80d53d0 --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/BaseUtils.java @@ -0,0 +1,20 @@ +package com.tptj.project.hg.sso; + +import com.fr.invoke.Reflect; + +import java.util.Map; +import java.util.Set; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + **/ +public class BaseUtils { + public static void copy( T from, T to ){ + Set> entries = Reflect.on(from).fields().entrySet(); + for( Map.Entry entry : entries ){ + Reflect.on(to).set( entry.getKey(), entry.getValue().get() ); + } + } +} diff --git a/src/main/java/com/tptj/project/hg/sso/LoginProvider.java b/src/main/java/com/tptj/project/hg/sso/LoginProvider.java new file mode 100644 index 0000000..b22b340 --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/LoginProvider.java @@ -0,0 +1,167 @@ +package com.tptj.project.hg.sso; + +import com.fr.decision.config.FSConfig; +import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider; +import com.fr.decision.webservice.bean.authentication.LoginClientBean; +import com.fr.decision.webservice.login.LogInOutResultInfo; +import com.fr.decision.webservice.utils.DecisionServiceConstants; +import com.fr.decision.webservice.utils.DecisionStatusService; +import com.fr.decision.webservice.v10.login.LoginService; +import com.fr.decision.webservice.v10.login.TokenResource; +import com.fr.decision.webservice.v10.login.event.LogInOutEvent; +import com.fr.event.EventDispatcher; +import com.fr.log.FineLoggerFactory; +import com.fr.stable.StringUtils; +import com.fr.store.Converter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + **/ +public abstract class LoginProvider extends AbstractGlobalRequestFilterProvider { + + /** + * 登录功能是否正式开启了,因为有的登录可能要先在平台配置某些内容或者做一些初始化的准备之后才会生效, + * 这里提供接口进行检测 + * @param req + * @return 返回true表示单点功能正式开始生效 + * @throws Exception + */ + protected abstract boolean isEffective(HttpServletRequest req)throws Exception; + + /** + * 除了URI之外的待登录请求的匹配 + * @param req + * @return 返回true就表示这个请求要进行登录检测 + * @throws SkipException + */ + protected abstract boolean accept(HttpServletRequest req)throws SkipException; + + /** + * 从请求中获取登录的凭证 + * @param req + * @return + * @throws Exception + */ + protected abstract String getTag( HttpServletRequest req )throws Exception; + + /** + * 验证凭证获取要登录的用户名 + * @param req + * @param tag + * @return + * @throws Exception + */ + protected abstract String getUsername( HttpServletRequest req,String tag )throws Exception; + + + + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) { + try{ + if( !isEffective(req) ){ + throw new SkipException(); + } + if( !accept(req) ){ + throw new SkipException(); + } + String tag = getTag(req); + LoginClientBean client = getLoginClientBean(req); + assertNoSameTag( client, tag ); + String username = getUsername( req, tag ); + login(req,res,client,username,tag); + }catch (RedirectException e){ + sendRedirect( res, e.getUrl() ); + return; + }catch (SkipException e){ + + }catch(Exception e){ + FineLoggerFactory.getLogger().error(e,"LoginFilter[Login By Username]: {}",e.getMessage()); + } + try { + chain.doFilter(req,res); + }catch (Exception e){ + FineLoggerFactory.getLogger().error(e,e.getMessage()); + } + } + + /** + * 最终登录用户的方法,登录后需要把凭证跟登录的用户绑定在一起 + * @param req + * @param res + * @param client + * @param username + * @param tag + * @throws Exception + */ + private void login( HttpServletRequest req, HttpServletResponse res ,LoginClientBean client, String username,String tag )throws Exception{ + //如果当前没有已登陆的用户,或者已登录的用户与将要登录的用户不一致,则重新登录 + if( null == client || !StringUtils.equals( client.getUsername(), username ) ){ + String token = LoginService.getInstance().login(req,res,username); + req.setAttribute( DecisionServiceConstants.FINE_AUTH_TOKEN_NAME, token ); + EventDispatcher.fire(LogInOutEvent.LOGIN, new LogInOutResultInfo(req, res, username, true)); + client = DecisionStatusService.loginStatusService().get(token); + } + //把当前凭证保存到状态服务器中 + client = SpLoginClientBean.copy( client, tag ); + DecisionStatusService.loginStatusService().put(client.getToken(),client,new Converter() { + @Override + public String[] createAlias(LoginClientBean client) { + return new String[]{ client.getUsername() }; + } + }, FSConfig.getInstance().getLoginConfig().getLoginTimeout()); + } + + /** + * 判断凭证如果已经在当前有效的登录有效期内绑定了用户,则不再重复登陆 + * @param client + * @param tag + * @throws SkipException + */ + private void assertNoSameTag( LoginClientBean client ,String tag)throws SkipException{ + //如果返回的凭证是空,表示强制认证,不用比较是否是已经绑定登录的凭证 + if( StringUtils.isEmpty(tag) ){ + return; + } + if( null == client || !(client instanceof SpLoginClientBean) ){ + return; + } + //如果当前的凭证跟状态服务器内绑定当前登录信息的凭证一致,则不用重复登录 + if( ((SpLoginClientBean)client).contain( tag ) ){ + throw new SkipException(); + } + } + + /** + * 获取当前已经登陆的有效客户端信息 + * @param req + * @return + */ + private LoginClientBean getLoginClientBean(HttpServletRequest req){ + try{ + return LoginService.getInstance().loginStatusValid(req, TokenResource.COOKIE); + }catch (Exception e){ + + } + return null; + } + + /** + * 重定向 + * @param res + * @param url + */ + private void sendRedirect( HttpServletResponse res ,String url){ + try { + res.sendRedirect( url ); + }catch (Exception e){ + FineLoggerFactory.getLogger().error(e,e.getMessage()); + } + } + +} diff --git a/src/main/java/com/tptj/project/hg/sso/ReLoader.java b/src/main/java/com/tptj/project/hg/sso/ReLoader.java new file mode 100644 index 0000000..8423f89 --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/ReLoader.java @@ -0,0 +1,9 @@ +package com.tptj.project.hg.sso; + +/** + * 数据重加载接口 + * @param + */ +public interface ReLoader { + T reload(T old)throws Exception; +} diff --git a/src/main/java/com/tptj/project/hg/sso/RedirectException.java b/src/main/java/com/tptj/project/hg/sso/RedirectException.java new file mode 100644 index 0000000..7130f51 --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/RedirectException.java @@ -0,0 +1,20 @@ +package com.tptj.project.hg.sso; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 简单的借用异常来统一处理重定向 + **/ +public class RedirectException extends Exception { + + private String url; + + public RedirectException( String url ){ + this.url = url; + } + + public String getUrl() { + return url; + } +} diff --git a/src/main/java/com/tptj/project/hg/sso/SkipException.java b/src/main/java/com/tptj/project/hg/sso/SkipException.java new file mode 100644 index 0000000..9279539 --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/SkipException.java @@ -0,0 +1,10 @@ +package com.tptj.project.hg.sso; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 简单借助异常来跳过所有不需要登录的请求 + **/ +public class SkipException extends Exception{ +} diff --git a/src/main/java/com/tptj/project/hg/sso/SpLoginClientBean.java b/src/main/java/com/tptj/project/hg/sso/SpLoginClientBean.java new file mode 100644 index 0000000..4005305 --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/SpLoginClientBean.java @@ -0,0 +1,31 @@ +package com.tptj.project.hg.sso; + +import com.fr.decision.webservice.bean.authentication.LoginClientBean; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 登录客户端信息的扩展,用于绑定额外的凭证 + **/ +public class SpLoginClientBean extends LoginClientBean { + private Set tag = new HashSet(); + + public boolean contain( String tag ){ + return -1 != tag.indexOf(tag); + } + + public void setTag(String tag) { + this.tag.add(tag); + } + + public static SpLoginClientBean copy( LoginClientBean bean, String tag ){ + SpLoginClientBean rt = new SpLoginClientBean(); + BaseUtils.copy(bean,rt); + rt.setTag(tag); + return rt; + } +} diff --git a/src/main/java/com/tptj/project/hg/sso/TimeoutObject.java b/src/main/java/com/tptj/project/hg/sso/TimeoutObject.java new file mode 100644 index 0000000..87d0d7d --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/TimeoutObject.java @@ -0,0 +1,29 @@ +package com.tptj.project.hg.sso; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 一个用于存放会过期的对象的封装 + **/ +public class TimeoutObject { + + private Object object; + private long timeout; + + public Object getObject() { + return object; + } + + public void setObject(Object object) { + this.object = object; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public boolean isTimeout(){ + return System.currentTimeMillis()>timeout; + } +} diff --git a/src/main/java/com/tptj/project/hg/sso/TimeoutObjectHolder.java b/src/main/java/com/tptj/project/hg/sso/TimeoutObjectHolder.java new file mode 100644 index 0000000..7d626aa --- /dev/null +++ b/src/main/java/com/tptj/project/hg/sso/TimeoutObjectHolder.java @@ -0,0 +1,61 @@ +package com.tptj.project.hg.sso; + +import com.fr.log.FineLoggerFactory; +import com.fr.store.StateHubManager; +import com.fr.store.StateHubService; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021-05-12 + * 一个对于会过期对象的重加载对象的封装 + **/ +public class TimeoutObjectHolder { + + private static StateHubService getService(){ + //具体会过期的对象要保存到状态服务器,防止集群情况下冲突 + return StateHubManager.applyForTenantService("SsoTimeoutObjectService"); + } + + private ReLoader loader; + private String key; + + public TimeoutObjectHolder(ReLoader loader, String key) { + this.loader = loader; + this.key = key; + } + + public static TimeoutObjectHolder init( Object object, long timeout, ReLoader loader, Class tag )throws Exception{ + if( null == loader ){ + throw new Exception("ReLoader Is Null"); + } + if( null == tag ){ + throw new Exception("tag Is Null"); + } + TimeoutObject obj = new TimeoutObject(); + obj.setObject(object); + obj.setTimeout(timeout); + getService().put( tag.getName(), obj ); + return new TimeoutObjectHolder( loader, tag.getName() ); + } + + public T getObj(){ + try{ + TimeoutObject obj = getService().get(key); + if( obj.isTimeout() ){ + TimeoutObject val = (TimeoutObject) loader.reload( obj ); + if( null != val ){ + obj = val; + getService().put( key, obj ); + }else{ + throw new Exception( "Timeout Object Reload Failed!" ); + } + } + return (T) obj.getObject(); + }catch(Exception e){ + FineLoggerFactory.getLogger().error( e, e.getMessage() ); + } + return null; + } + +}