欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

java单点登录解决方案(单点登录失败怎么解决)

程序员文章站 2023-11-24 10:34:04
一、什么是单点登陆单点登录(single sign on),简称为 sso,是目前比较流行的企业业务整合的解决方案之一。sso的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系...

一、什么是单点登陆

单点登录(single sign on),简称为 sso,是目前比较流行的企业业务整合的解决方案之一。sso的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

二、简单的运行机制

单点登录的机制其实是比较简单的,用一个现实中的例子做比较。某公园内部有许多独立的景点,游客可以在各个景点门口单独买票。

对于需要游玩所有的景点的游客,这种买票方式很不方便,需要在每个景点门口排队买票,钱包拿 进拿出的,容易丢失,很不安全。

于是绝大多数游客选择在大门口买一张通票(也叫套票),就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门 口出示一下刚才买的套票就能够被允许进入每个独立的景点。

单点登录的机制也一样,如下图所示,

java单点登录解决方案(单点登录失败怎么解决)

用户认证:这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌token,主要在认证服务器中完成,即图中的认证系统,注意认证系统只能有一个。

身份校验:这一环节是用户携带token去访问其他服务器时,在其他服务器中要对token的真伪进行检验,主要在资源服务器中完成,即图中的应用系统2 3

三、jwt介绍

概念说明

从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里我们选择使用jwt来实现token的生成和校验。

jwt,全称json web token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可以解析检验token。

jwt生成的token由三部分组成:

  • 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
  • 载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
  • 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

jwt生成token的安全性分析

从jwt生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐上面了!

试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!

非对称加密rsa介绍

基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端

  • 私钥加密,持有私钥或公钥才可以解密
  • 公钥加密,持有私钥才可解密

优点:安全,难以破解

缺点:算法比较耗时,为了安全,可以接受

历史:三位数学家rivest、shamir 和 adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:rsa。

四、springsecurity整合jwt

1.认证思路分析

springsecurity主要是通过过滤器来实现功能的!我们要找到springsecurity实现认证和校验身份的过滤器!

回顾集中式认证流程

用户认证:使用
usernamepasswordauthenticationfilter
过滤器中attemptauthentication方法实现认证功能,该过滤器父类中successfulauthentication方法实现认证成功后的操作。

身份校验:使用basicauthenticationfilter过滤器中dofilterinternal方法验证是否登录,以决定能否进入后续过滤器。

分析分布式认证流程

用户认证:

由于分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改
usernamepasswordauthenticationfilter
过滤器中attemptauthentication方法,让其能够接收请求体。

另外,默认successfulauthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。

身份校验:

原来basicauthenticationfilter过滤器中dofilterinternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给springsecurity,以便于后续的授权功能可以正常使用。

2.具体实现

为了演示单点登录的效果,我们设计如下项目结构

java单点登录解决方案(单点登录失败怎么解决)

2.1父工程创建

因为本案例需要创建多个系统,所以我们使用maven聚合工程来实现,首先创建一个父工程,导入springboot的父依赖即可

<parent><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-parent</artifactid><version>2.1.3.release</version><relativepath/></parent>

2.2公共工程创建

然后创建一个common工程,其他工程依赖此系统

java单点登录解决方案(单点登录失败怎么解决)

导入jwt相关的依赖

<dependencies><dependency><groupid>io.jsonwebtoken</groupid><artifactid>jjwt-api</artifactid><version>0.10.7</version></dependency><dependency><groupid>io.jsonwebtoken</groupid><artifactid>jjwt-impl</artifactid><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupid>io.jsonwebtoken</groupid><artifactid>jjwt-jackson</artifactid><version>0.10.7</version><scope>runtime</scope></dependency><!--jackson包--><dependency><groupid>com.fasterxml.jackson.core</groupid><artifactid>jackson-databind</artifactid><version>2.9.9</version></dependency><!--日志包--><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-logging</artifactid></dependency><dependency><groupid>joda-time</groupid><artifactid>joda-time</artifactid></dependency><dependency><groupid>org.projectlombok</groupid><artifactid>lombok</artifactid></dependency><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-test</artifactid></dependency></dependencies>

创建相关的工具类

java单点登录解决方案(单点登录失败怎么解决)

payload

@datapublic class payload <t>{private string id;private t userinfo;private date expiration;}

jsonutils

public classjsonutils{public static final objectmapper mapper = new objectmapper;private static final logger logger = loggerfactory.getlogger(jsonutils.class);public static string tostring(object obj) {if (obj == ) {return ;}if (obj.getclass == string.class) {return (string) obj;}try {return mapper.writevalueasstring(obj);} catch (jsonprocessingexception e) {logger.error("json序列化出错:" + obj, e);return ;}}public static <t> t tobean(string json, class<t> tclass) {try {return mapper.readvalue(json, tclass);} catch (ioexception e) {logger.error("json解析出错:" + json, e);return ;}}public static <e> list<e> tolist(string json, class<e> eclass) {try {return mapper.readvalue(json, mapper.gettypefactory.constructcollectiontype(list.class, eclass));} catch (ioexception e) {logger.error("json解析出错:" + json, e);return ;}}public static <k, v> map<k, v> tomap(string json, class<k> kclass, class<v> vclass) {try {return mapper.readvalue(json, mapper.gettypefactory.constructmaptype(map.class, kclass, vclass));} catch (ioexception e) {logger.error("json解析出错:" + json, e);return ;}}public static <t> t nativeread(string json, typereference<t> type) {try {return mapper.readvalue(json, type);} catch (ioexception e) {logger.error("json解析出错:" + json, e);return ;}}}

jwtutils

public classjwtutils{private static final string jwt_payload_user_key = "user";/*** 私钥加密token** @param userinfo 载荷中的数据* @param privatekey 私钥* @param expire 过期时间,单位分钟* @return jwt*/public static string generatetokenexpireinminutes(object userinfo, privatekey privatekey, int expire) {return jwts.builder.claim(jwt_payload_user_key, jsonutils.tostring(userinfo)).setid(createjti).setexpiration(datetime.now.plusminutes(expire).todate).signwith(privatekey, signaturealgorithm.rs256).compact;}/*** 私钥加密token** @param userinfo 载荷中的数据* @param privatekey 私钥* @param expire 过期时间,单位秒* @return jwt*/public static string generatetokenexpireinseconds(object userinfo, privatekey privatekey, int expire) {return jwts.builder.claim(jwt_payload_user_key, jsonutils.tostring(userinfo)).setid(createjti).setexpiration(datetime.now.plusseconds(expire).todate).signwith(privatekey, signaturealgorithm.rs256).compact;}/*** 公钥解析token** @param token 用户请求中的token* @param publickey 公钥* @return jws<claims>*/private static jws<claims> parsertoken(string token, publickey publickey) {return jwts.parser.setsigningkey(publickey).parseclaimsjws(token);}private static string createjti {return new string(base64.getencoder.encode(uuid.randomuuid.tostring.getbytes));}/*** 获取token中的用户信息** @param token 用户请求中的令牌* @param publickey 公钥* @return 用户信息*/public static <t> payload<t> getinfofromtoken(string token, publickey publickey, class<t> usertype) {jws<claims> claimsjws = parsertoken(token, publickey);claims body = claimsjws.getbody;payload<t> claims = new payload<>;claims.setid(body.getid);claims.setuserinfo(jsonutils.tobean(body.get(jwt_payload_user_key).tostring, usertype));claims.setexpiration(body.getexpiration);return claims;}/*** 获取token中的载荷信息** @param token 用户请求中的令牌* @param publickey 公钥* @return 用户信息*/public static <t> payload<t> getinfofromtoken(string token, publickey publickey) {jws<claims> claimsjws = parsertoken(token, publickey);claims body = claimsjws.getbody;payload<t> claims = new payload<>;claims.setid(body.getid);claims.setexpiration(body.getexpiration);return claims;}}

rsautils

public classrsautils{private static final int default_key_size = 2048;/*** 从文件中读取公钥** @param filename 公钥保存路径,相对于classpath* @return 公钥对象* @throws exception*/public static publickey getpublickey(string filename) throws exception {byte bytes = readfile(filename);return getpublickey(bytes);}/*** 从文件中读取密钥** @param filename 私钥保存路径,相对于classpath* @return 私钥对象* @throws exception*/public static privatekey getprivatekey(string filename) throws exception {byte bytes = readfile(filename);return getprivatekey(bytes);}/*** 获取公钥** @param bytes 公钥的字节形式* @return* @throws exception*/private static publickey getpublickey(byte[] bytes) throws exception {bytes = base64.getdecoder.decode(bytes);x509encodedkeyspec spec = new x509encodedkeyspec(bytes);keyfactory factory = keyfactory.getinstance("rsa");return factory.generatepublic(spec);}/*** 获取密钥** @param bytes 私钥的字节形式* @return* @throws exception*/private static privatekey getprivatekey(byte[] bytes) throws nosuchalgorithmexception, invalidkeyspecexception {bytes = base64.getdecoder.decode(bytes);pkcs8encodedkeyspec spec = new pkcs8encodedkeyspec(bytes);keyfactory factory = keyfactory.getinstance("rsa");return factory.generateprivate(spec);}/*** 根据密文,生存rsa公钥和私钥,并写入指定文件** @param publickeyfilename 公钥文件路径* @param privatekeyfilename 私钥文件路径* @param secret 生成密钥的密文*/public static void generatekey(string publickeyfilename, string privatekeyfilename, string secret, int keysize) throws exception {keypairgenerator keypairgenerator = keypairgenerator.getinstance("rsa");securerandom securerandom = new securerandom(secret.getbytes);keypairgenerator.initialize(math.max(keysize, default_key_size), securerandom);keypair keypair = keypairgenerator.genkeypair;// 获取公钥并写出byte publickeybytes = keypair.getpublic.getencoded;publickeybytes = base64.getencoder.encode(publickeybytes);writefile(publickeyfilename, publickeybytes);// 获取私钥并写出byte privatekeybytes = keypair.getprivate.getencoded;privatekeybytes = base64.getencoder.encode(privatekeybytes);writefile(privatekeyfilename, privatekeybytes);}private static byte readfile(string filename) throws exception {return files.readallbytes(new file(filename).topath);}private static void writefile(string destpath, byte[] bytes) throws ioexception {file dest = new file(destpath);if (!dest.exists) {dest.createnewfile;}files.write(dest.topath, bytes);}}

在通用子模块中编写测试类生成rsa公钥和私钥

public classjwttest{private string privatekey = "c:/tools/auth_key/id_key_rsa";private string publickey = "c:/tools/auth_key/id_key_rsa.pub";@testpublic void test1 throws exception{rsautils.generatekey(publickey,privatekey,"dpb",1024);}}
java单点登录解决方案(单点登录失败怎么解决)

2.3认证系统创建

接下来我们创建我们的认证服务。

java单点登录解决方案(单点登录失败怎么解决)

导入相关的依赖

<dependencies><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-web</artifactid></dependency><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-security</artifactid></dependency><dependency><artifactid>security-jwt-common</artifactid><groupid>com.dpb</groupid><version>1.0-snapshot</version></dependency><dependency><groupid>mysql</groupid><artifactid>mysql-connector-java</artifactid><version>5.1.47</version></dependency><dependency><groupid>org.mybatis.spring.boot</groupid><artifactid>mybatis-spring-boot-starter</artifactid><version>2.1.0</version></dependency><dependency><groupid>com.alibaba</groupid><artifactid>druid</artifactid><version>1.1.10</version></dependency><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-configuration-processor</artifactid><optional>true</optional></dependency></dependencies>

创建配置文件

spring:datasource:driver-class-name: com.mysql.jdbc.driverurl: jdbc:mysql://localhost:3306/srmusername: rootpassword: 123456type: com.alibaba.druid.pool.druiddatasourcemybatis:type-aliases-package: com.dpb.domainmapper-locations: classpath:mapper/*.xmllogging:level:com.dpb: debugrsa:key:pubkeyfile: c:toolsauth_keyid_key_rsa.pubprikeyfile: c:toolsauth_keyid_key_rsa
java单点登录解决方案(单点登录失败怎么解决)

提供公钥私钥的配置类

@data@configurationproperties(prefix = "rsa.key")publicclassrsakeyproperties{private string pubkeyfile;private string prikeyfile;private publickey publickey;private privatekey privatekey;/*** 系统启动的时候触发* @throws exception*/@postconstructpublic void creatersakey throws exception {publickey = rsautils.getpublickey(pubkeyfile);privatekey = rsautils.getprivatekey(prikeyfile);}}

创建启动类

@springbootapplication@mapperscan("com.dpb.mapper")@enableconfigurationproperties(rsakeyproperties.class)public class app {public static void main(string[] args) {springapplication.run(app.class,args);}}

完成数据认证的逻辑

pojo

@datapublic class rolepojo implements grantedauthority {private integer id;private string rolename;private string roledesc;@jsonignore@overridepublic string getauthority {return rolename;}}
@datapublicclassuserpojoimplementsuserdetails{private integer id;private string username;private string password;private integer status;private list<rolepojo> roles;@jsonignore@overridepublic collection<? extends grantedauthority> getauthorities {list<simplegrantedauthority> auth = new arraylist<>;auth.add(new simplegrantedauthority("admin"));return auth;}@overridepublic string getpassword {return this.password;}@overridepublic string getusername {return this.username;}@jsonignore@overridepublicbooleanisaccountnonexpired {return true;}@jsonignore@overridepublicbooleanisaccountnonlocked {return true;}@jsonignore@overridepublicbooleaniscredentialsnonexpired {return true;}@jsonignore@overridepublicbooleanisenabled {return true;}}

mapper接口

public interface usermapper {public userpojo querybyusername(@param("username") string username);}

mapper映射文件

<?xml version="1.0" encoding="utf-8" ?><!doctype mapperpublic "-//mybatis.org//dtd mapper 3.0//en""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.dpb.mapper.usermapper"><select id="querybyusername" resulttype="userpojo">select * from t_user where username = #{username}</select></mapper>

service

public interfaceuserserviceextendsuserdetailsservice{}
@service@transactionalpublicclassuserserviceimplimplementsuserservice{@autowiredprivate usermapper mapper;@overridepublic userdetails loaduserbyusername(string s) throws usernamenotfoundexception {userpojo user = mapper.querybyusername(s);return user;}}

自定义认证过滤器

public class tokenloginfilter extends usernamepasswordauthenticationfilter {private authenticationmanager authenticationmanager;private rsakeyproperties prop;public tokenloginfilter(authenticationmanager authenticationmanager, rsakeyproperties prop) {this.authenticationmanager = authenticationmanager;this.prop = prop;}public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception {try {userpojo sysuser = new objectmapper.readvalue(request.getinputstream, userpojo.class);usernamepasswordauthenticationtoken authrequest = new usernamepasswordauthenticationtoken(sysuser.getusername, sysuser.getpassword);return authenticationmanager.authenticate(authrequest);}catch (exception e){try {response.setcontenttype("application/json;charset=utf-8");response.setstatus(httpservletresponse.sc_unauthorized);printwriter out = response.getwriter;map resultmap = new hashmap;resultmap.put("code", httpservletresponse.sc_unauthorized);resultmap.put("msg", "用户名或密码错误!");out.write(new objectmapper.writevalueasstring(resultmap));out.flush;out.close;}catch (exception outex){outex.printstacktrace;}throw new runtimeexception(e);}}public void successfulauthentication(httpservletrequest request, httpservletresponse response, filterchain chain, authentication authresult) throws ioexception, servletexception {userpojo user = new userpojo;user.setusername(authresult.getname);user.setroles((list<rolepojo>)authresult.getauthorities);string token = jwtutils.generatetokenexpireinminutes(user, prop.getprivatekey, 24 * 60);response.addheader("authorization", "bearer "+token);try {response.setcontenttype("application/json;charset=utf-8");response.setstatus(httpservletresponse.sc_ok);printwriter out = response.getwriter;map resultmap = new hashmap;resultmap.put("code", httpservletresponse.sc_ok);resultmap.put("msg", "认证通过!");out.write(new objectmapper.writevalueasstring(resultmap));out.flush;out.close;}catch (exception outex){outex.printstacktrace;}}}

自定义校验token的过滤器

public class tokenverifyfilter extends basicauthenticationfilter {private rsakeyproperties prop;public tokenverifyfilter(authenticationmanager authenticationmanager, rsakeyproperties prop) {super(authenticationmanager);this.prop = prop;}public void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain chain) throws ioexception, servletexception {string header = request.getheader("authorization");if (header == || !header.startswith("bearer ")) {//如果携带错误的token,则给用户提示请登录!chain.dofilter(request, response);response.setcontenttype("application/json;charset=utf-8");response.setstatus(httpservletresponse.sc_forbidden);printwriter out = response.getwriter;map resultmap = new hashmap;resultmap.put("code", httpservletresponse.sc_forbidden);resultmap.put("msg", "请登录!");out.write(new objectmapper.writevalueasstring(resultmap));out.flush;out.close;} else {//如果携带了正确格式的token要先得到tokenstring token = header.replace("bearer ", "");//验证tken是否正确payload<userpojo> payload = jwtutils.getinfofromtoken(token, prop.getpublickey, userpojo.class);userpojo user = payload.getuserinfo;if(user!=){usernamepasswordauthenticationtoken authresult = new usernamepasswordauthenticationtoken(user.getusername, , user.getauthorities);securitycontextholder.getcontext.setauthentication(authresult);chain.dofilter(request, response);}}}}

编写springsecurity的配置类

@configuration@enablewebsecurity@enableglobalmethodsecurity(securedenabled=true)public class websecurityconfig extends websecurityconfigureradapter {@autowiredprivate userservice userservice;@autowiredprivate rsakeyproperties prop;@beanpublic bcryptpasswordencoder passwordencoder{return new bcryptpasswordencoder;}//指定认证对象的来源public void configure(authenticationmanagerbuilder auth) throws exception {auth.userdetailsservice(userservice).passwordencoder(passwordencoder);}//springsecurity配置信息public void configure(httpsecurity http) throws exception {http.csrf.disable.authorizerequests.antmatchers("/user/query").hasanyrole("admin").anyrequest.authenticated.and.addfilter(new tokenloginfilter(super.authenticationmanager, prop)).addfilter(new tokenverifyfilter(super.authenticationmanager, prop)).sessionmanagement.sessioncreationpolicy(sessioncreationpolicy.stateless);}}

启动服务测试

启动服务

java单点登录解决方案(单点登录失败怎么解决)

通过postman来访问测试

java单点登录解决方案(单点登录失败怎么解决)
java单点登录解决方案(单点登录失败怎么解决)

根据token信息我们访问其他资源

java单点登录解决方案(单点登录失败怎么解决)

2.4资源系统创建

说明

资源服务可以有很多个,这里只拿产品服务为例,记住,资源服务中只能通过公钥验证认证。不能签发token!创建产品服务并导入jar包根据实际业务导包即可,咱们就暂时和认证服务一样了。

接下来我们再创建一个资源服务

java单点登录解决方案(单点登录失败怎么解决)

导入相关的依赖

<dependencies><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-web</artifactid></dependency><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-security</artifactid></dependency><dependency><artifactid>security-jwt-common</artifactid><groupid>com.dpb</groupid><version>1.0-snapshot</version></dependency><dependency><groupid>mysql</groupid><artifactid>mysql-connector-java</artifactid><version>5.1.47</version></dependency><dependency><groupid>org.mybatis.spring.boot</groupid><artifactid>mybatis-spring-boot-starter</artifactid><version>2.1.0</version></dependency><dependency><groupid>com.alibaba</groupid><artifactid>druid</artifactid><version>1.1.10</version></dependency><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-configuration-processor</artifactid><optional>true</optional></dependency></dependencies>

编写产品服务配置文件

切记这里只能有公钥地址!

server:port: 9002spring:datasource:driver-class-name: com.mysql.jdbc.driverurl: jdbc:mysql://localhost:3306/srmusername: rootpassword: 123456type: com.alibaba.druid.pool.druiddatasourcemybatis:type-aliases-package: com.dpb.domainmapper-locations: classpath:mapper/*.xmllogging:level:com.dpb: debugrsa:key:pubkeyfile: c:toolsauth_keyid_key_rsa.pub

编写读取公钥的配置类

@data@configurationproperties(prefix = "rsa.key")publicclassrsakeyproperties{private string pubkeyfile;private publickey publickey;/*** 系统启动的时候触发* @throws exception*/@postconstructpublic void creatersakey throws exception {publickey = rsautils.getpublickey(pubkeyfile);}}

编写启动类

@springbootapplication@mapperscan("com.dpb.mapper")@enableconfigurationproperties(rsakeyproperties.class)public class app {public static void main(string[] args) {springapplication.run(app.class,args);}}

复制认证服务中,用户对象,角色对象和校验认证的接口

复制认证服务中的相关内容即可

复制认证服务中springsecurity配置类做修改

@configuration@enablewebsecurity@enableglobalmethodsecurity(securedenabled=true)public class websecurityconfig extends websecurityconfigureradapter {@autowiredprivate userservice userservice;@autowiredprivate rsakeyproperties prop;@beanpublic bcryptpasswordencoder passwordencoder{return new bcryptpasswordencoder;}//指定认证对象的来源public void configure(authenticationmanagerbuilder auth) throws exception {auth.userdetailsservice(userservice).passwordencoder(passwordencoder);}//springsecurity配置信息public void configure(httpsecurity http) throws exception {http.csrf.disable.authorizerequests//.antmatchers("/user/query").hasanyrole("user").anyrequest.authenticated.and.addfilter(new tokenverifyfilter(super.authenticationmanager, prop))// 禁用掉session.sessionmanagement.sessioncreationpolicy(sessioncreationpolicy.stateless);}}

去掉“增加自定义认证过滤器”即可!

编写产品处理器

@restcontroller@requestmapping("/user")publicclassusercontroller{@requestmapping("/query")public string query{return "success";}@requestmapping("/update")public string update{return "update";}}

测试

java单点登录解决方案(单点登录失败怎么解决)