一、什么是单点登陆
单点登录(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被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部残雪和载荷的ba64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐上面了!
试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!
基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
私钥加密,持有私钥或公钥才可以解密公钥加密,持有私钥才可解密优点:安全,难以破解
缺点:算法比较耗时,为了安全,可以接受
历史:三位数学家rivest、shamir 和 adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:rsa。
四、springcurity整合jwt
springcurity主要是通过过滤器来实现功能的!我们要找到springcurity实现认证和校验身份的过滤器!
用户认证:使用过滤器中
urnamepasswordauthenticationfilterattemptauthentication方法实现认证功能,该过滤器父类中successfulauthentication方法实现认证成功后的操作。
身份校验:使用basicauthenticationfilter过滤器中dofilterinternal方法验证是否登录,以决定能否进入后续过滤器。
用户认证:
由于分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改过滤器中
urnamepasswordauthenticationfilterattemptauthentication方法,让其能够接收请求体。
另外,默认successfulauthentication方法在认证通过后,是把用户信息直接放入ssion就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。
身份校验:
原来basicauthenticationfilter过滤器中dofilterinternal方法校验用户是否登录,就是看ssion中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给springcurity,以便于后续的授权功能可以正常使用。
为了演示单点登录的效果,我们设计如下项目结构
因为本案例需要创建多个系统,所以我们使用maven聚合工程来实现,首先创建一个父工程,导入springboot的父依赖即可
<parent><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-parent</artifactid><version>2.1.3.relea</version><relativepath/></parent>然后创建一个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 urinfo;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_ur_key = "ur";/*** 私钥加密token** @param urinfo 载荷中的数据* @param privatekey 私钥* @param expire 过期时间,单位分钟* @return jwt*/public static string generatetokenexpireinminutes(object urinfo, privatekey privatekey, int expire) {return jwts.builder.claim(jwt_payload_ur_key, jsonutils.tostring(urinfo)).tid(createjti).texpiration(datetime.now.plusminutes(expire).todate).signwith(privatekey, signaturealgorithm.rs256).compact;}/*** 私钥加密token** @param urinfo 载荷中的数据* @param privatekey 私钥* @param expire 过期时间,单位秒* @return jwt*/public static string generatetokenexpireinconds(object urinfo, privatekey privatekey, int expire) {return jwts.builder.claim(jwt_payload_ur_key, jsonutils.tostring(urinfo)).tid(createjti).texpiration(datetime.now.plusconds(expire).todate).signwith(privatekey, signaturealgorithm.rs256).compact;}/*** 公钥解析token** @param token 用户请求中的token* @param publickey 公钥* @return jws<claims>*/private static jws<claims> parrtoken(string token, publickey publickey) {return jwts.parr.tsigningkey(publickey).parclaimsjws(token);}private static string createjti {return new string(ba64.getencoder.encode(uuid.randomuuid.tostring.getbytes));}/*** 获取token中的用户信息** @param token 用户请求中的令牌* @param publickey 公钥* @return 用户信息*/public static <t> payload<t> getinfofromtoken(string token, publickey publickey, class<t> urtype) {jws<claims> claimsjws = parrtoken(token, publickey);claims body = claimsjws.getbody;payload<t> claims = new payload<>;claims.tid(body.getid);claims.turinfo(jsonutils.tobean(body.get(jwt_payload_ur_key).tostring, urtype));claims.texpiration(body.getexpiration);return claims;}/*** 获取token中的载荷信息** @param token 用户请求中的令牌* @param publickey 公钥* @return 用户信息*/public static <t> payload<t> getinfofromtoken(string token, publickey publickey) {jws<claims> claimsjws = parrtoken(token, publickey);claims body = claimsjws.getbody;payload<t> claims = new payload<>;claims.tid(body.getid);claims.texpiration(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 = ba64.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 = ba64.getdecoder.decode(bytes);pkcs8encodedkeyspec spec = new pkcs8encodedkeyspec(bytes);keyfactory factory = keyfactory.getinstance("rsa");return factory.generateprivate(spec);}/*** 根据密文,生存rsa公钥和私钥,并写入指定文件** @param publickeyfilename 公钥文件路径* @param privatekeyfilename 私钥文件路径* @param cret 生成密钥的密文*/public static void generatekey(string publickeyfilename, string privatekeyfilename, string cret, int keysize) throws exception {keypairgenerator keypairgenerator = keypairgenerator.getinstance("rsa");curerandom curerandom = new curerandom(cret.getbytes);keypairgenerator.initialize(math.max(keysize, default_key_size), curerandom);keypair keypair = keypairgenerator.genkeypair;// 获取公钥并写出byte publickeybytes = keypair.getpublic.getencoded;publickeybytes = ba64.getencoder.encode(publickeybytes);writefile(publickeyfilename, publickeybytes);// 获取私钥并写出byte privatekeybytes = keypair.getprivate.getencoded;privatekeybytes = ba64.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);}}接下来我们创建我们的认证服务。
导入相关的依赖
<dependencies><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-web</artifactid></dependency><dependency><groupid>org.springframework.boot</groupid><artifactid>spring-boot-starter-curity</artifactid></dependency><dependency><artifactid>curity-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/srmurname: rootpassword: 123456type: com.alibaba.druid.pool.druiddatasourcemybatis:type-alias-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
提供公钥私钥的配置类中国211大学
@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;}}@datapublicclassurpojoimplementsurdetails{private integer id;private string urname;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 geturname {return this.urname;}@jsonignore@overridepublicbooleanisaccountnonexpired {return true;}@jsonignore@overridepublicbooleanisaccountnonlocked {return true;}@jsonignore@overridepublicbooleaniscredentialsnonexpired {return true;}@jsonignore@overridepublicbooleaninabled {return true;}}mapper接口
public interface urmapper {public urpojo querybyurname(@param("urname") string urname);}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.urmapper"><lect id="querybyurname" resulttype="urpojo">lect * from t_ur where urname = #{urname}</lect></mapper>rvice
public interfaceurrviceextendsurdetailsrvice{}@rvice@transactionalpublicclassurrviceimplimplementsurrvice{@autowiredprivate urmapper mapper;@overridepublic urdetails loadurbyurname(string s) throws urnamenotfoundexception {urpojo ur = mapper.querybyurname(s);return ur;}}自定义认证过滤器
public class tokenloginfilter extends urnamepasswordauthenticationfilter {private authenticationmanager authenticationmanager;private rsakeyproperties prop;public tokenloginfilter(authenticationmanager authenticationmanager, rsakeyproperties prop) {this.authenticationmanager = authenticationmanager;this.prop = prop;}public authentication attemptauthentication(httprvletrequest request, httprvletrespon respon) throws authenticationexception {try {urpojo sysur = new objectmapper.readvalue(request.getinputstream, urpojo.class);urnamepasswordauthenticationtoken authrequest = new urnamepasswordauthenticationtoken(sysur.geturname, sysur.getpassword);return authenticationmanager.authenticate(authrequest);}catch (exception e){try {respon.tcontenttype("application/json;chart=utf-8");respon.tstatus(httprvletrespon.sc_unauthorized);printwriter out = respon.getwriter;map resultmap = new hashmap;resultmap.put("code", httprvletrespon.sc_unauthorized);resultmap.put("msg", "用户名或密码错误!");out.write(new objectmapper.writevalueasstring(resultmap));out.flush;out.clo;}catch (exception outex){outex.printstacktrace;}throw new runtimeexception(e);}}public void successfulauthentication(httprvletrequest request, httprvletrespon respon, filterchain chain, authentication authresult) throws ioexception, rvletexception {urpojo ur = new urpojo;ur.turname(authresult.getname);ur.troles((list<rolepojo>)authresult.getauthorities);string token = jwtutils.generatetokenexpireinminutes(ur, prop.getprivatekey, 24 * 60);respon.addheader("authorization", "bearer "+token);try {respon.tcontenttype("application/json;chart=utf-8");respon.tstatus(httprvletrespon.sc_ok);printwriter out = respon.getwriter;map resultmap = new hashmap;resultmap.put("code", httprvletrespon.sc_ok);resultmap.put("msg", "认证通过!");out.write(new objectmapper.writevalueasstring(resultmap));out.flush;out.clo;}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(httprvletrequest request, httprvletrespon respon, filterchain chain) throws ioexception, rvletexception {string header = request.getheader("authorization");if (header == || !header.startswith("bearer ")) {//如果携带错误的token,则给用户提示请登录!chain.dofilter(request, respon);respon.tcontenttype("application/json;chart=utf-8");respon.tstatus(httprvletrespon.sc_forbidden);printwriter out = respon.getwriter;map resultmap = new hashmap;resultmap.put("code", httprvletrespon.sc_forbidden);resultmap.put("msg", "请登录!");out.write(new objectmapper.writevalueasstring(resultmap));out.flush;out.clo;} el {//如果携带了正确格式的token要先得到tokenstring token = header.replace("bearer ", "");//验证tken是否正确payload<urpojo> payload = jwtutils.getinfofromtoken(token, prop.getpublickey, urpojo.class);urpojo ur = payload.geturinfo;if(ur!=){urnamepasswordauthenticationtoken authresult = new urnamepasswordauthenticationtoken(ur.geturname, , ur.getauthorities);curitycontextholder.getcontext.tauthentication(authresult);chain.dofilter(request, respon);}}}}编写springcurity的配置类
@configuration@enablewebcurity@enableglobalmethodcurity(curedenabled=true)public class webcurityconfig extends webcurityconfigureradapter {@autowiredprivate urrvice urrvice;@autowiredprivate rsakeyproperties prop;@beanpublic bcryptpasswordencoder passwordencoder{return new bcryptpasswordencoder;}//指定认证对象的来源public void configure(authenticationmanagerbuilder auth) throws exception {auth.urdetailsrvice(urrvice).passwordencoder(passwordencoder);}//springcurity配置信息public void configure(httpcurity http) throws exception {http.csrf.disable.authorizerequests.antmatchers("/ur/query").hasanyrole("admin").anyrequest.authenticated.and.addfilter(new tokenloginfilter(super.authenticationmanager, prop)).addfilter(new tokenverifyfilter(super.authenticationmanager, prop)).ssionmanagement.ssioncreationpolicy(ssioncreationpolicy.stateless);}}启动服务测试
启动服务
通过postman来访问测试
根据token信息我们访问其他资源
说明
资源服务可以有很多个,这里只拿产品服务为例,记住,资源服务中只能通过公钥验证认证。不能签发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-curity</artifactid></dependency><dependency><artifactid>curity-jwt-common</artifactid><groupid>com.dpb</groupid><version>1.0-snapshot</versio笑话五年级下册n></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><artifac大规模的海水运动tid>spring-boot-configuration-processor</artifactid><optional>true</optional></dependency></dependencies>编写产品服务配置文件
切记这里只能有公钥地址!
rver:port: 9002spring:datasource:driver-class-name: com.mysql.jdbc.driverurl: jdbc:mysql://localhost:3306/srmurname: rootpassword: 123456type: com.alibaba.druid.pool.druiddatasourcemybatis:type-alias-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);}}复制认证服务中,用户对象,角色对象和校验认证的接口
复制认证服务中的相关内容即可
复制认证服务中springcurity配置类做修改
@configuration@enablewebcurity@enableglobalmethodcurity(curedenabled=true)public class webcurityconfig extends webcurityconfigureradapter {@autowiredprivate urrvice urrvice;@autowiredprivate rsakeyproperties prop;@beanpublic bcryptpasswordencoder passwordencoder{return new bcryptpasswordencoder;}//指定认证对象的来源public void configure(aut步履维艰的意思henticationmanagerbuilder auth) throws exception {auth.urdetailsrvice(urrvice).passwordencoder(passwordencoder);}//springcurity配置信息public void configure(httpcurity http) throws exception {http.csrf.disable.authorizerequests//.antmatchers("/ur/query").hasanyrole("ur").anyrequest.authenticated.and.addfilter(new tokenverifyfilter(super.authenticationmanager, prop))// 禁用掉ssion.ssionmanagement.ssioncreationpolicy(ssioncreationpolicy.stateless);}}去掉“增加自定义认证过滤器”即可!
编写产品处理器
@restcontroller@requestmapping("/ur")publicclassurcontroller{@requestmapping("/query")public string query{return "success";}@requestmapping("/update")public string update{return "update";}}测试
本文发布于:2023-04-05 04:45:54,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/zuowen/b694877e7b44522e595b2d4914a666d5.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:java单点登录解决方案(单点登录失败怎么解决).doc
本文 PDF 下载地址:java单点登录解决方案(单点登录失败怎么解决).pdf
| 留言与评论(共有 0 条评论) |