java单点登录解决方案(单点登录失败怎么解决)
一、什么是单点登陆
单点登录(single sign on),简称为 sso,是目前比较流行的企业业务整合的解决方案之一。sso的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
二、简单的运行机制
单点登录的机制其实是比较简单的,用一个现实中的例子做比较。某公园内部有许多独立的景点,游客可以在各个景点门口单独买票。
对于需要游玩所有的景点的游客,这种买票方式很不方便,需要在每个景点门口排队买票,钱包拿 进拿出的,容易丢失,很不安全。
于是绝大多数游客选择在大门口买一张通票(也叫套票),就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门 口出示一下刚才买的套票就能够被允许进入每个独立的景点。
单点登录的机制也一样,如下图所示,
用户认证:这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌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实现认证和校验身份的过滤器!
回顾集中式认证流程
用户认证:使用
过滤器中
usernamepasswordauthenticationfilterattemptauthentication
方法实现认证功能,该过滤器父类中successfulauthentication
方法实现认证成功后的操作。
身份校验:使用basicauthenticationfilter
过滤器中dofilterinternal
方法验证是否登录,以决定能否进入后续过滤器。
分析分布式认证流程
用户认证:
由于分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改
过滤器中
usernamepasswordauthenticationfilterattemptauthentication
方法,让其能够接收请求体。
另外,默认successfulauthentication
方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。
身份校验:
原来basicauthenticationfilter过滤器中dofilterinternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给springsecurity,以便于后续的授权功能可以正常使用。
2.具体实现
为了演示单点登录的效果,我们设计如下项目结构
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工程,其他工程依赖此系统
导入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>
创建相关的工具类
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);}}
2.3认证系统创建
接下来我们创建我们的认证服务。
导入相关的依赖
<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
提供公钥私钥的配置类
@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);}}
启动服务测试
启动服务
通过postman来访问测试
根据token信息我们访问其他资源
2.4资源系统创建
说明
资源服务可以有很多个,这里只拿产品服务为例,记住,资源服务中只能通过公钥验证认证。不能签发token!创建产品服务并导入jar包根据实际业务导包即可,咱们就暂时和认证服务一样了。
接下来我们再创建一个资源服务
导入相关的依赖
<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";}}
测试
推荐阅读