Javaparser HD(单点登录初体验)

大家好,今天小编来为大家解答Javaparser HD这个问题,单点登录初体验很多人还不知道,现在让我们一起来看看吧!

前言

登录这东西很奇怪哎,你说它难吗?好像客户端只需要调接口就行,那有啥难的?当你多多少少对登录的后台有些了解,又觉得好难啊,session,token,cookie,等等一堆东西,有老的大家都不喜欢用的,有新的一些不太懂的,根据公司项目规模不同,还要考虑成本的问题,真是有些头疼。博主今天推荐的一种登陆方式便是SpringSecurity+JWT的结合使用,为什么要两者结合呢?SpringSecurity现在已经很少用了,甚至有些人认为已经废弃了,但是因为SpringSecurity是Spring系列的东西,Spring对其支持很友好,不,是非常友好。但是我们不想使用他验证后的操作,所以我们要打断这个操作,让JWT工作。下面我们就来了解并手动操作一下吧,本篇还是集成在我们前面的微服务项目中,你也可以另起项目一起来做。

为什么要登录

我们平时都知道登录,不知道有没有思考过登录解决的是什么问题?大家会想到,不登录就不能拿到用户信息,一些用户行为和服务就没有办法关联到具体人身上,没错!比如购买行为。但我觉得这个说法不够具体,登录的具体作用应该是拿到用户的权限。

我们说,是人,就有不同的角色,一个男人,可以是儿子,可以是父亲,可以是员工,可以是老板等等。那我们就认为,一个登录体系中,必须要有一张用户表和一张角色表,还有刚刚说的权限表。这三张表之间还需要有表明其关系的关联表。

可以说,这三张表在任何一个登录体系都是必备的,甚至你还可以有临时的用户权限表,他们之间多是多对多的关系,要理清他们之间的关系并不简单。

登录的种类

登录的种类到目前为止所使用的技术大概五六种吧,其之间大同小异,从早期的Cookie-Session到现在的单点登录,中间跨越的时间不短,其中有一个时间分割点就是html5标准出现的时候,他带来了localstorage,使得跨域问题得到良好的解决,但我们并不满足于这种方式,于是token技术出现,但是本质上和基于Cookie-Session+localstorage的方式没有太大区别。为了解决微服务间的数据同步,基于Token的JWT认证诞生,其中还有一种利用session和redis的数据共享技术也能实现数据共享,这和token技术也类似。接下来,我们来简单的了解一下这几种登录方式:

Cookie-Session

这种方式要追溯到html5出现之前,那时只能利用cookie存储SessionId,但cookie在跨域问题上一言难尽,但并不是不能跨域,只是要比我们后面的方法麻烦,有localstorage你还用cookie?而且cookie退出站点后就会销毁,这点让人极不能接受。其流程是:

用户输入用户名、密码或短信验证码登录服务端验证后,返回一个SessionId,其和用户信息关联。客户端将SessionID存到cookie当客户端再发起请求时带上cookie中的信息,服务端通过cookie获取SessionID并校验,以判断用户是否登录Cookie-Session-localstorage

这种方式和以上相似,只是改了几个地方:

存储的位置不再是cookie,而是localstorage服务端不存储sessionid,而是改用redis做存储,可以解决同步问题,但也有缺点,同步会造成数据量增加,占用额外内存,我们通过一张图来说明

左边先行,获取用户信息,生成sessionid,存储在redis,右边访问其他模块,通过sessionid去redis拿用户信息,注意,用户模块和其他模块也会保存sessionid,这就是数据共享,用户量很大的情况会造成数据冗余,不适合用户量特别大的项目,中小型项目可以。对于客户端,sessionid当然是保存在localstorage内了,毕竟谁也不想去额外解决跨域的问题。

JWT令牌

这种方式是目前使用比较多的一种方式,它和上面的方式也有相似之处,只是少了数据的存储,JWT不存储session这些东西,它只负责验证jwt是否正确,验证的过程就是解码的过程,关于JWT的标准制式的解释,请大家手动百度吧,不再赘述,博主也记不住,贴了浪费篇幅。看看,大概知道是怎么做的就行。

此处必须有图:

服务端不保存信息,这一点可以节省空间,谁的信息谁自己保存,解密方式在我这里,同时提高了安全性,何乐不为?

几种登陆总结

如果细分还能再分出几种登录方式,但基本大同小异,博主合并了其中相似的登录方式,总结出来这三种,此处忽略第三方登录,可自行了解。肯定还有其他方式,但总的来说,和这三种应该是类似,并不会完全不同。看了一篇OAuth2.0单点登录相关的文章,还有一篇总结登录的文章,真是写的太好了,分享给大家:

安全验证-知乎

Java——项目常用登录方式详解_new海绵宝宝()的博客-CSDN博客

里面总结的很全面,也有一些案例,初学者可以看看。

用户身份认证与授权

从这里开始,就是我们的项目时间,首先出场的是SpringSecurity,它是用于解决认证与授权的框架。SpringSecurity有默认的登录账号和密码,用户名user,密码是随机的,每次启动项目都会重新生成一个。它要求所有的请求都必须先登录才允许访问,稍后我们集成后可以来进行测试。

创建工程

在微服务项目cloud下创建cloud-passport子项目:

添加依赖

xml复制代码

<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.codingfire</groupId><artifactId>cloud</artifactId><version>0.0.1-SNAPSHOT</version></parent><groupId>com.codingfire</groupId><artifactId>cloud-passport</artifactId><version>0.0.1-SNAPSHOT</version><name>cloud-passport</name><description>DemoprojectforSpringBoot</description><dependencies><!--SpringBootWeb:支持SpringMVC--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--SpringBootSecurity:处理认证与授权--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--SpringBootTest:测试--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies></project>

父子关联

xml复制代码

<modules><module>cloud-commons</module><module>cloud-bussiness</module><module>cloud-cart</module><module>cloud-order</module><module>cloud-stock</module><module>gateway</module><module>search</module><module>cloud-passport</module></modules>

启动项目

依赖添加完毕,什么都不需要做,直接运行passport的启动文件,可以在控制台看到如下输出:

sql复制代码

Usinggeneratedsecuritypassword:1060ee9f-a56e-4ff5-bce4-68306b3265b1

这就是SpringSecurity生成的随机密码,它同时还提供了一个URL:http://localhost:8080/login

我们点开URL,在浏览器打开一个登录页面,我们输入用户名:user,密码就用上面的密码,登录成功后跳转回之前访问的URL,由于我们没有做这个页面,会显示404。这就是SpringSecurity默认要求所有的请求都是必须先登录才允许的访问的能力。

Bcrypt算法的工具

SpringSecurity的依赖项中包括了Bcrypt算法的工具类,这是一款非常优秀的密码加密工具,适和对需要存储下来的密码进行加密处理。我们来测试下看看。

打开测试类,添加如下测试代码:

java复制代码packagecom.codingfire.cloud.passport;nnimportorg.junit.jupiter.api.Test;nimportorg.springframework.boot.test.context.SpringBootTest;nimportorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;nn@SpringBootTestnclassCloudPassportApplicationTests{nnprivateBCryptPasswordEncoderpasswordEncoder=newBCryptPasswordEncoder();nn@TestnpublicvoidtestEncode(){n//原文相同的情况,每次加密得到的密文都不同nfor(inti=0;i<10;i++){nStringrawPassword="123456";nStringencodedPassword=passwordEncoder.encode(rawPassword);nSystem.out.println("rawPassword="+rawPassword);nSystem.out.println("encodedPassword="+encodedPassword);n}n}nn@TestnpublicvoidtestMatches(){nStringrawPassword="123456";nStringencodedPassword="$2a$10$4LHozWwptKuabvikrzM1KefYFgI7H4A9xCVv7cvMKsV9ycS4guS5K";nbooleanmatchResult=passwordEncoder.matches(rawPassword,encodedPassword);nSystem.out.println("matchresult:"+matchResult);n}n}n

我们分别运行这两个方法,会看到如下输出:

ini复制代码

rawPassword=123456encodedPassword=$2a$10$4LHozWwptKuabvikrzM1KefYFgI7H4A9xCVv7cvMKsV9ycS4guS5KrawPassword=123456encodedPassword=$2a$10$VA9u7X9rSvuEtPlEixhnSujHdVsK8OwqkVIOqLzNydxa.ypCviVIqrawPassword=123456encodedPassword=$2a$10$d9lWItH5YhEFRns/Yj5U3OUyHM8rLKAE9X.SsbcIOA0WwRqUwFl82rawPassword=123456encodedPassword=$2a$10$W/PLc/Q04.8xfmEQgwSKC.g79FxRPJGFXRuFzISdVrn3cYWk1xkyerawPassword=123456encodedPassword=$2a$10$/9Ya1aqjQBX8342iH5blTOZeHJomKUitInVmLTsANonXriQjxhb5KrawPassword=123456encodedPassword=$2a$10$kX2u5zLrDN/VC8CLVRGmsOIFqA2FHCJRYJKnYmWeu/NyTQEjBCbkirawPassword=123456encodedPassword=$2a$10$igB96QfY9XDwhPz3U8Z7Nui1UQy.wtzSl9uk2n7m.lCdcKwhGqLXurawPassword=123456encodedPassword=$2a$10$ssDypFmm0bN0CvIBqoB4huHIhT7oRwS9KsO1iopyFeSOUWYR96NPCrawPassword=123456encodedPassword=$2a$10$IWBuDVLYjvHCUqOM9qAQuu.kTlW8RH08CbIFlvYTzcdEMLHbVSFtSrawPassword=123456encodedPassword=$2a$10$J/eN5/loO6DTJG7ubgQh4.1ovwI9CS1H0yqnsbYEQFwnvqRq64bU.

sql复制代码

matchresult:true

下面的解密使用上面的第一个加密后的密文进行的解密。大家要用自己的电脑生成的密文进行解密,用博主的可能会出现无法匹配的情况。

此加密工具有个特点,大家应该发现了,此加密得到的密文都不相同。

接着需要和数据库中存储的密文进行对比,此时需要使用SQL去数据库查询该用户的密文进行比对,比对通过,则可进行登录。此时就不能使用默认的user用户名和随机的密码的方式,具体做法我们继续往下看。

创建VO模型类

在commons工程下创建pojo.passport.vo.AdminLoginVO类:

java复制代码packagecom.codingfire.cloud.commons.pojo.passport.vo;nnimportlombok.Data;nnimportjava.io.Serializable;nimportjava.util.List;nn@DatanpublicclassAdminLoginVOimplementsSerializable{nprivateLongid;nprivateStringusername;nprivateStringpassword;nprivateIntegerisLogin;nprivateList<String>permissions;n}n

创建完成后我们发现要使用commons模块,那需要依赖添加此模块:

xml复制代码

<!--all-common依赖--><dependency><groupId>com.codingfire</groupId><artifactId>cloud-commons</artifactId><version>0.0.1-SNAPSHOT</version></dependency>

创建接口文件

在passport下创建mapper.AdminMapper接口:

java复制代码packagecom.codingfire.cloud.passport.mapper;nnimportcom.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;nnpublicinterfaceAdminMapper{nAdminLoginVOgetLoginInfoByUsername(Stringusername);n}n创建XML文件

大家还记得吗?我们在Mybatis框架中有使用XML文件来写SQL。在src/main/resources下创建mapper文件夹,mapper文件夹下可以把前面的xml文件粘贴过来,写入如下SQL:

xml复制代码

<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTDMapper3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.codingfire.cloud.passport.mapper.AdminMapper"><!--AdminLoginVOgetLoginInfoByUsername(Stringusername);--><selectid="getLoginInfoByUsername"resultMap="LoginInfoResultMap">select<includerefid="LoginInfoQueryFields"/>fromadminleftjoinadmin_roleonadmin.id=admin_role.admin_idleftjoinrole_permissiononadmin_role.role_id=role_permission.role_idleftjoinpermissiononrole_permission.permission_id=permission.idwhereusername=#{username}</select><sqlid="LoginInfoQueryFields"><iftest="true">admin.id,admin.username,admin.password,admin.is_login,permission.name</if></sql><resultMapid="LoginInfoResultMap"type="com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO"><idcolumn="id"property="id"/><resultcolumn="username"property="username"/><resultcolumn="password"property="password"/><resultcolumn="is_login"property="isLogin"/><collectionproperty="permissions"ofType="java.lang.String"><constructor><argcolumn="name"/></constructor></collection></resultMap></mapper>

在这里大家要留意几个问题了,我们这里需要连接mybatis的数据库,第一次看博主文章的需要看看Java开发-Mybatis框架初体验_CodingFire的博客-CSDN博客

这篇博客,才知道建的什么数据库,有哪些表,有哪些参数,否则将很难进行下去。

补充配置

由于需要使用数据库,需要补充配置和依赖。

添加依赖

xml复制代码

<!--mybatis整合springboot--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><!--alibaba数据源德鲁伊--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId></dependency><!--mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>

添加配置

这里,我们选择从mybatis复制配置信息到properties文件:

ini复制代码

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=truespring.datasource.driver=com.mysql.cj.jdbc.Driverspring.datasource.username=rootspring.datasource.password=0mybatis.mapper-locations=classpath:mapper/AdminMapper.xml

密码写自己的数据库密码。

创建配置类

需要连接数据库,那么少不了mybatis配置了,创建MybatisConfiguration类,在passport下创建config包,此包下创建配置类:

kotlin复制代码packagecom.codingfire.cloud.passport.config;nnimportorg.springframework.context.annotation.ComponentScan;nimportorg.springframework.context.annotation.Configuration;nn@Configurationn@ComponentScan("com.codingfire.cloud.passport.mapper")npublicclassMybatisConfiguration{n}n

前面也是有创建过的,你可以选择直接贴过来,但要注意扫描的路径改成自己的路径。原本需要在配置文件中配置mybatis.mapper-locations属性,上面已经补充过了。

测试上面的配置

在测试类下,我们添加如下代码:

ini复制代码@AutowirednAdminMapperadminMapper;nn@TestnvoidselectUser(){nAdminLoginVOadminLoginVO=adminMapper.getLoginInfoByUsername("admin04");nSystem.out.println(adminLoginVO);n}n

这是我们原来表中的数据,没有数据的需要预先插入一些数据。运行测试方法,发现报错?

额,一大堆,看了......好一会儿,才发现有两个地方写错了:

一个是AdminMapper内没有添加@Repository注解:

另一个是MybatisConfiguration类上的scan注解写错了,修改一下:

然后再次运行测试方法,可以在控制台看到输出的用户信息如下:

AdminLoginVO(id=1,username=admin04,password=123456,isLogin=0,permissions=[全频道可删除,全频道可筛选,全频道读取,单频道可删除,单频道可筛选,单频道观看])

代表我们的测试成功了。简直累的一逼,真是错一步都不行。

让SpringSecurity通过数据库验证密码

前面提过,要让SpringSecurity通过数据库的数据来验证用户名与密码,我们还需要做出一些修改和配置,我们看到每次控制台都会输出一串新的密码:

Usinggeneratedsecuritypassword:a47b9983-3ea3-45d8-9632-faf701a7925b

下面,让我们看看怎样才能不让它输出。

配置密码加密器

在config包下创建SecurityConfiguration类:

kotlin复制代码packagecom.codingfire.cloud.passport.config;nnimportorg.springframework.context.annotation.Bean;nimportorg.springframework.context.annotation.Configuration;nimportorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;nimportorg.springframework.security.crypto.password.PasswordEncoder;nn@ConfigurationnpublicclassSecurityConfiguration{nn@BeannpublicPasswordEncoderpasswordEncoder(){nreturnnewBCryptPasswordEncoder();n}nn}n重写SpringSecurity下的用户相关抽象方法

在passport下建新包security,包下建类UserDetailsServiceImpl,并实现UserDetailsService接口:

kotlin复制代码packagecom.codingfire.cloud.passport.security;nnimportcom.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;nimportcom.codingfire.cloud.passport.mapper.AdminMapper;nimportorg.springframework.beans.factory.annotation.Autowired;nimportorg.springframework.security.authentication.BadCredentialsException;nimportorg.springframework.security.core.userdetails.User;nimportorg.springframework.security.core.userdetails.UserDetails;nimportorg.springframework.security.core.userdetails.UserDetailsService;nimportorg.springframework.security.core.userdetails.UsernameNotFoundException;nn@ServicenpublicclassUserDetailsServiceImplimplementsUserDetailsService{n@AutowirednprivateAdminMapperadminMapper;nn@OverridenpublicUserDetailsloadUserByUsername(Strings)throwsUsernameNotFoundException{nSystem.out.println("根据用户名查询尝试登录的管理员信息,用户名="+s);nAdminLoginVOadmin=adminMapper.getLoginInfoByUsername(s);nSystem.out.println("通过持久层进行查询,结果="+admin);nnif(admin==null){nSystem.out.println("根据用户名没有查询到有效的管理员数据,将抛出异常");nthrownewBadCredentialsException("登录失败,用户名不存在!");n}nnSystem.out.println("查询到匹配的管理员数据,需要将此数据转换为UserDetails并返回");nUserDetailsuserDetails=User.builder()n.username(admin.getUsername())n.password(admin.getPassword())n.accountExpired(false)n.accountLocked(false)n.disabled(admin.getIsLogin()!=1)n.credentialsExpired(false)n.authorities(admin.getPermissions().toArray(newString[]{}))n.build();nSystem.out.println("转换得到UserDetails="+userDetails);nreturnuserDetails;n}nn}n测试成果

重新启动工程,看看还有没有随机密码生成:

可以看到,随机密码已经不会再自动生成。

JWT什么是JWT

Jsonwebtoken(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

客户端第1次访问服务器端时,是没有携带令牌访问的,当服务器进行响应时,会将JWT响应到客户端,客户端保存后,在第2次访问时就开始携带JWT进行请求,服务器收到请求中的JWT后就可以识别用户身份。

关于JWT的详细介绍,推荐这篇博客:SpringBoot集成JWT实现token验证-简书

为什么使用JWT

SpringSecurity默认使用Session机制存储用户信息,而HTTP协议是无状态协议,它不保存客户端信息,所以,同一个客户端的多次访问,等效于多个不同的客户端各访问一次服务端,为了保存用户信息,使服务器端能够识别客户端身份,我们推荐使用Token或其他技术,比如我们马上要说的JWT。

如何使用JWT添加依赖

JWT只是一个概念,而实现生成JWT、解析JWT的框架却有不少,我们这里要使用的是jjwt,添加依赖如下:

xml复制代码

<!--https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency>

由于版本已经在主项目中控制,此处版本省略。

测试jwt

在测试类下创建JwtTests类,添加如下测试代码:

typescript复制代码//密钥,遵从越长越好,越乱越复杂越好的原则nStringsecretKey="asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";nn@TestnpublicvoidtestGenerateJwt(){n//ClaimsnMap<String,Object>claims=newHashMap<>();nclaims.put("id",01);nclaims.put("name","codingfire");nn//JWT的组成部分:Header(头),Payload(载荷),Signature(签名)nStringjwt=Jwts.builder()n//Header:指定算法与当前数据类型n//格式为:{"alg":算法,"typ":"jwt"}n.setHeaderParam(Header.CONTENT_TYPE,"HS256")n.setHeaderParam(Header.TYPE,Header.JWT_TYPE)n//Payload:通常包含Claims(自定义数据)和过期时间n.setClaims(claims)n.setExpiration(newDate(System.currentTimeMillis()+5*60*1000))n//Signature:由算法和密钥(secretkey)这2部分组成n.signWith(SignatureAlgorithm.HS256,secretKey)n//打包生成n.compact();nSystem.out.println(jwt);n}n

运行测试方法,输出加密后的密文如下:

复制代码

eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As

你能看到里面有两个点,这是JWT加密的固定格式,需要你去看推荐的博文。

接着我们把这串密文用来解密试试看能得到什么:

ini复制代码@TestnpublicvoidtestParseJwt(){nStringjwt="eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As";nClaimsclaims=Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();nObjectid=claims.get("id");nObjectname=claims.get("name");nSystem.out.println("id="+id);nSystem.out.println("name="+name);n}nn

运行测试方法:

看到如图所示结果,你的jwt就已经引入成功。但,这还不够,我们是要在SpringSecurity中使用JWT,所以还有很多工作要做。

在SpringSecurity中使用JWT自动装配AuthenticationManager对象

这是一个认证管理器,我们需要接管这个管理器,在SecurityConfiguration类中做一些操作,来看看最终的SecurityConfiguration类吧:

scala复制代码packagecom.codingfire.cloud.passport.config;nnimportorg.springframework.context.annotation.Bean;nimportorg.springframework.context.annotation.Configuration;nimportorg.springframework.security.authentication.AuthenticationManager;nimportorg.springframework.security.config.annotation.web.builders.HttpSecurity;nimportorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;nimportorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;nimportorg.springframework.security.crypto.password.PasswordEncoder;nn@ConfigurationnpublicclassSecurityConfigurationextendsWebSecurityConfigurerAdapter{nn@BeannpublicPasswordEncoderpasswordEncoder(){nreturnnewBCryptPasswordEncoder();n}nn@Beann@OverridenpublicAuthenticationManagerauthenticationManagerBean()throwsException{nreturnsuper.authenticationManagerBean();n}nn@Overridenprotectedvoidconfigure(HttpSecurityhttp)throwsException{n//禁用防跨域攻击nhttp.csrf().disable();nn//URL白名单nString[]urls={n"/admins/login"n};nn//配置各请求路径的认证与授权nhttp.authorizeRequests()//请求需要授权才可以访问n.antMatchers(urls)//匹配一些路径n.permitAll()//允许直接访问(不需要经过认证和授权)n.anyRequest()//匹配除了以上配置的其它请求n.authenticated();//都需要认证n}n}n创建DTO类

在上面创建AdminLoginVO类的地方创建新的包dto,下面建新类:

创建接口类

在passport下创建service包,其下创建新接口类IAdminService:

java复制代码packagecom.codingfire.cloud.passport.service;nnimportcom.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;nnpublicinterfaceIAdminService{nnStringlogin(AdminLoginDTOadminLoginDTO);nn}n创建实现类

在service包下创建新包impl,其下创建实现类AdminServiceImpl:

typescript复制代码packagecom.codingfire.cloud.passport.service.impl;nnimportcom.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;nimportcom.codingfire.cloud.passport.service.IAdminService;nimportorg.springframework.beans.factory.annotation.Autowired;nimportorg.springframework.security.authentication.AuthenticationManager;nimportorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;nimportorg.springframework.security.core.Authentication;nimportorg.springframework.stereotype.Service;nnn@ServicenpublicclassAdminServiceImplimplementsIAdminService{nn@AutowirednprivateAuthenticationManagerauthenticationManager;nn@OverridenpublicStringlogin(AdminLoginDTOadminLoginDTO){nn//生成此用户数据的JWTnStringjwt="ThisisaJWT.";//临时nreturnjwt;n}n}n创建控制器类

在passport下创建controller包,其下创建AdminController:

kotlin复制代码packagecom.codingfire.cloud.passport.controller;nnimportcom.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;nimportcom.codingfire.cloud.passport.service.IAdminService;nimportorg.springframework.beans.factory.annotation.Autowired;nimportorg.springframework.web.bind.annotation.RequestMapping;nimportorg.springframework.web.bind.annotation.RestController;nn@RestControllern@RequestMapping(value="/admins",produces="application/json;charset=utf-8")npublicclassAdminController{nn@AutowirednprivateIAdminServiceadminService;nn@RequestMapping("/login")npublicStringlogin(AdminLoginDTOadminLoginDTO){nStringjwt=adminService.login(adminLoginDTO);nreturnjwt;n}nn}n测试代码

启动项目,在浏览器输入:http://localhost:8080/admins/login?username=codingfire&password=123456

把用户名和密码改成你自己数据库中的用户名和密码,也可以写错的,然后进行多次尝试,看浏览器会返回什么:

看到此信息,就代表你的jwt接入成功了,但我们需要返回给客户端jwt数据,接下来我们实现这个过程。

返回客户端JWT数据修改AdminServiceImpl实现类

java复制代码packagecom.codingfire.cloud.passport.service.impl;nnimportcom.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;nimportcom.codingfire.cloud.passport.service.IAdminService;nimportio.jsonwebtoken.Header;nimportio.jsonwebtoken.Jwts;nimportio.jsonwebtoken.SignatureAlgorithm;nimportorg.springframework.beans.factory.annotation.Autowired;nimportorg.springframework.security.authentication.AuthenticationManager;nimportorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;nimportorg.springframework.security.core.Authentication;nimportorg.springframework.security.core.userdetails.User;nimportorg.springframework.stereotype.Service;nnimportjava.util.Date;nimportjava.util.HashMap;nimportjava.util.Map;nnn@ServicenpublicclassAdminServiceImplimplementsIAdminService{nn@AutowirednprivateAuthenticationManagerauthenticationManager;nn@OverridenpublicStringlogin(AdminLoginDTOadminLoginDTO){n//密钥,遵从越长越好,越乱越复杂越好的原则nStringsecretKey="asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";nn//准备被认证数据nAuthenticationauthenticationn=newUsernamePasswordAuthenticationToken(nadminLoginDTO.getUsername(),adminLoginDTO.getPassword());n//调用AuthenticationManager验证用户名与密码n//执行认证,如果此过程没有抛出异常,则表示认证通过,如果认证信息有误,将抛出异常nauthenticationManager.authenticate(authentication);nnUseruser=(User)authentication.getPrincipal();nSystem.out.println("从认证结果中获取Principal="+user.getClass().getName());nMap<String,Object>claims=newHashMap<>();nclaims.put("username",user.getUsername());nclaims.put("permissions",user.getAuthorities());nSystem.out.println("即将向JWT中写入数据="+claims);nn//JWT的组成部分:Header(头),Payload(载荷),Signature(签名)nStringjwt=Jwts.builder()n//Header:指定算法与当前数据类型n//格式为:{"alg":算法,"typ":"jwt"}n.setHeaderParam(Header.CONTENT_TYPE,"HS256")n.setHeaderParam(Header.TYPE,Header.JWT_TYPE)n//Payload:通常包含Claims(自定义数据)和过期时间n.setClaims(claims)n.setExpiration(newDate(System.currentTimeMillis()+5*60*1000))n//Signature:由算法和密钥(secretkey)这2部分组成n.signWith(SignatureAlgorithm.HS256,secretKey)n//打包生成n.compact();nn//返回JWT数据nreturnjwt;n}n}n

你会发现,这就是我们在测试类中测试的代码,基本上是直接贴过来的。

修改控制器类

kotlin复制代码packagecom.codingfire.cloud.passport.controller;nnimportcom.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;nimportcom.codingfire.cloud.commons.restful.JsonResult;nimportcom.codingfire.cloud.passport.service.IAdminService;nimportorg.springframework.beans.factory.annotation.Autowired;nimportorg.springframework.web.bind.annotation.RequestMapping;nimportorg.springframework.web.bind.annotation.RestController;nn@RestControllern@RequestMapping(value="/admins",produces="application/json;charset=utf-8")npublicclassAdminController{nn@AutowirednprivateIAdminServiceadminService;nn@RequestMapping("/login")npublicJsonResult<String>login(AdminLoginDTOadminLoginDTO){nStringjwt=adminService.login(adminLoginDTO);nreturnJsonResult.ok(jwt);n}nn}n

修改返回值类型。

测试jwt数据返回

运行项目,在浏览器输入原来的html:http://localhost:8080/admins/login?username=codingfire&password=123456

浏览器将得到如下数据:

json复制代码

{"state":200,"message":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwZXJtaXNzaW9ucyI6Ilt7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-WPr-WIoOmZpFwifSx7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-WPr-etm-mAiVwifSx7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-ivu-WPllwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-WPr-WIoOmZpFwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-WPr-etm-mAiVwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-ingueci1wifV0iLCJleHAiOjE2Nzc3NDkzOTUsInVzZXJuYW1lIjoiY29kaW5nZmlyZSJ9.dw4tk52xTXXQ4-D_qkZNhjL-RkHnzG6QKHe6Tq1j3_Y","data":null}

这里有个坑啊小伙伴们,如果你一直403,且控制台提示你EncodedpassworddoesnotlooklikeBCrypt,这是因为你的数据库存储的是明文密码,必须存储我们在测试类中使用BCryptPasswordEncoder加密后的密码。博主刚刚就犯了这个错,真实太容易忽略了,不知道该说啥,大家可别犯这个错。

使用其他URL被屏蔽怎么办

刚刚由于我们禁止了未登陆时直接进入SpringSecurity的登陆页,所以才需要添加了白名单解决屏蔽所有连接的问题。如果使用Knife4j,该怎么添加白名单呢?我们来看看:

arduino复制代码String[]urls={n"/admins/login",n"/doc.html",//从本行开始,以下是新增n"/**/*.js",n"/**/*.css",n"/swagger-resources",n"/v2/api-docs",n"/favicon.ico"n};n使用请求头

得到JWT之后,在后续的请求中都需要在请求头中带上JWT,放在Authorization属性内,所以应该先判断请求头中是否有Authorization,而不能让请求直达服务器业务模块。这让我想到了前面讲过的过滤器,下面,我们在security包下创建一个过滤器类:

scala复制代码packagecom.codingfire.cloud.passport.security;nnimportorg.springframework.stereotype.Component;nimportorg.springframework.web.filter.OncePerRequestFilter;nnimportjavax.servlet.FilterChain;nimportjavax.servlet.ServletException;nimportjavax.servlet.http.HttpServletRequest;nimportjavax.servlet.http.HttpServletResponse;nimportjava.io.IOException;nn@ComponentnpublicclassJwtAuthenticationFilterextendsOncePerRequestFilter{nn@OverridenprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{nSystem.out.println("JwtAuthenticationFilter.doFilterInternal()");n}nn}n

过滤器类是需要注册后才能工作的,所以下一步对过滤器进行注册。用于验证JWT的过滤器应该运行在SpringSecurity处理登录的过滤器之前才能工作,所以需要在自定义的SecurityConfiguration中的configure()方法中将我们自定义的过滤器注册在SpringSecurity的相关过滤器之前。

同一个项目中允许存在多个过滤器,形成过滤器链,所以我们注册过滤器不需要单独建个类来处理了,而是在SecurityConfiguration类中进行,最终的类如下:

scala复制代码packagecom.codingfire.cloud.passport.config;nnimportcom.codingfire.cloud.passport.security.JwtAuthenticationFilter;nimportorg.springframework.beans.factory.annotation.Autowired;nimportorg.springframework.context.annotation.Bean;nimportorg.springframework.context.annotation.Configuration;nimportorg.springframework.security.authentication.AuthenticationManager;nimportorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;nimportorg.springframework.security.config.annotation.web.builders.HttpSecurity;nimportorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;nimportorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;nimportorg.springframework.security.crypto.password.PasswordEncoder;nimportorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;nn@Configurationn@EnableGlobalMethodSecurity(prePostEnabled=true)npublicclassSecurityConfigurationextendsWebSecurityConfigurerAdapter{nn@AutowirednprivateJwtAuthenticationFilterjwtAuthenticationFilter;nn@BeannpublicPasswordEncoderpasswordEncoder(){nreturnnewBCryptPasswordEncoder();n}nn@Beann@OverridenpublicAuthenticationManagerauthenticationManagerBean()throwsException{nreturnsuper.authenticationManagerBean();n}nn@Overridenprotectedvoidconfigure(HttpSecurityhttp)throwsException{n//禁用防跨域攻击nhttp.csrf().disable();nn//URL白名单nString[]urls={n"/admins/login",n"/doc.html",//从本行开始,以下是新增n"/**/*.js",n"/**/*.css",n"/swagger-resources",n"/v2/api-docs",n"/favicon.ico"n};nnn//配置各请求路径的认证与授权nhttp.authorizeRequests()//请求需要授权才可以访问n.antMatchers(urls)//匹配一些路径n.permitAll()//允许直接访问(不需要经过认证和授权)n.anyRequest()//匹配除了以上配置的其它请求n.authenticated();//都需要认证nn//注册处理JWT的过滤器n//此过滤器必须在SpringSecurity处理登录的过滤器之前nhttp.addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);n}n}n

我们重起项目,输入之前的url,不太对啊,下载了一个空的login文件,控制台看到了如下内容:

JwtAuthenticationFilter.doFilterInternal()

那是因为过滤器的工作还没有结束,他还需要实现以下功能:

尝试从请求头中获取JWT数据,如果无JWT数据,直接放行,SpringSecurity会进行下一步处理,比如,白名单的请求允许访问,其它请求禁止访问如果存在JWT数据,应该尝试解析,解析失败,就是认证失败了,要求客户端重新登录,客户端就可以得到新的、正确的JWT,客户端在下一次提交请求时,使用新的JWT就可以正常访问将解析得到的数据封装到Authentication对象中,SpringSecurity的上下文中存储的数据类型就是Authentication类型为避免存入1次后,SpringSecurity的上下文中始终存在Authentication,在此过滤器执行的第一时间,应该先清除上一次的数据

下面,我们来看看自定义过滤器中还有哪些代码:

kotlin复制代码packagecom.codingfire.cloud.passport.security;nnimportcom.alibaba.fastjson.JSON;nimportcom.codingfire.cloud.commons.restful.JsonResult;nimportcom.codingfire.cloud.commons.restful.ResponseCode;nimportio.jsonwebtoken.*;nimportorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;nimportorg.springframework.security.core.Authentication;nimportorg.springframework.security.core.authority.SimpleGrantedAuthority;nimportorg.springframework.security.core.context.SecurityContextHolder;nimportorg.springframework.stereotype.Component;nimportorg.springframework.util.StringUtils;nimportorg.springframework.web.filter.OncePerRequestFilter;nnimportjavax.servlet.FilterChain;nimportjavax.servlet.ServletException;nimportjavax.servlet.http.HttpServletRequest;nimportjavax.servlet.http.HttpServletResponse;nimportjava.io.IOException;nimportjava.util.List;nn/**n*JWT过滤器:从请求头的Authorization中获取JWT中存入的用户信息n*并添加到SpringSecurity的上下文中n*以致于SpringSecurity后续的组件(包括过滤器等)能从上下文中获取此用户的信息n*从而验证是否已经登录、是否具有权限等n*/n@ComponentnpublicclassJwtAuthenticationFilterextendsOncePerRequestFilter{nn/**n*JWT数据的密钥n*/nprivateStringsecretKey="fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";nn@OverridenprotectedvoiddoFilterInternal(HttpServletRequestrequest,nHttpServletResponseresponse,nFilterChainfilterChain)throwsServletException,IOException{nSystem.out.println("JwtAuthenticationFilter.doFilterInternal()");n//清除SpringSecurity上下文中的数据n//避免此前曾经存入过用户信息,后续即使没有携带JWT,在SpringSecurity仍保存有上下文数据(包括用户信息)nSystem.out.println("清除SpringSecurity上下文中的数据");nSecurityContextHolder.clearContext();n//客户端提交请求时,必须在请求头的Authorization中添加JWT数据,这是当前服务器程序的规定,客户端必须遵守n//尝试获取JWT数据nStringjwt=request.getHeader("Authorization");nSystem.out.println("从请求头中获取到的JWT="+jwt);n//判断是否不存在jwt数据nif(!StringUtils.hasText(jwt)){n//不存在jwt数据,则放行,后续还有其它过滤器及相关组件进行其它的处理,例如未登录则要求登录等n//此处不宜直接阻止运行,因为“登录”、“注册”等请求本应该没有jwt数据nSystem.out.println("请求头中无JWT数据,当前过滤器将放行");nfilterChain.doFilter(request,response);//继续执行过滤器链中后续的过滤器nreturn;//必须n}nn//注意:此时执行时,如果请求头中携带了Authentication,日志中将输出,且不会有任何响应,因为当前过滤器尚未放行n//以下代码有可能抛出异常的n//TODO密钥和各个Key应该统一定义nStringusername=null;nStringpermissionsString=null;ntry{nSystem.out.println("请求头中包含JWT,准备解析此数据……");nClaimsclaims=Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();nusername=claims.get("username").toString();npermissionsString=claims.get("permissions").toString();nSystem.out.println("username="+username);nSystem.out.println("permissionsString="+permissionsString);n}catch(ExpiredJwtExceptione){nSystem.out.println("解析JWT失败,此JWT已过期:"+e.getMessage());nJsonResult<Void>jsonResult=JsonResult.failed(nResponseCode.ERR_JWT_EXPIRED,"您的登录已过期,请重新登录!");nStringjsonString=JSON.toJSONString(jsonResult);nSystem.out.println("响应结果:"+jsonString);nresponse.setContentType("application/json;charset=utf-8");nresponse.getWriter().println(jsonString);nreturn;n}catch(MalformedJwtExceptione){nSystem.out.println("解析JWT失败,此JWT数据错误,无法解析:"+e.getMessage());nJsonResult<Void>jsonResult=JsonResult.failed(nResponseCode.ERR_JWT_MALFORMED,"获取登录信息失败,请重新登录!");nStringjsonString=JSON.toJSONString(jsonResult);nSystem.out.println("响应结果:"+jsonString);nresponse.setContentType("application/json;charset=utf-8");nresponse.getWriter().println(jsonString);nreturn;n}catch(SignatureExceptione){nSystem.out.println("解析JWT失败,此JWT签名错误:"+e.getMessage());nJsonResult<Void>jsonResult=JsonResult.failed(nResponseCode.ERR_JWT_SIGNATURE,"获取登录信息失败,请重新登录!");nStringjsonString=JSON.toJSONString(jsonResult);nSystem.out.println("响应结果:"+jsonString);nresponse.setContentType("application/json;charset=utf-8");nresponse.getWriter().println(jsonString);nreturn;n}catch(Throwablee){nSystem.out.println("解析JWT失败,异常类型:"+e.getClass().getName());ne.printStackTrace();nJsonResult<Void>jsonResult=JsonResult.failed(nResponseCode.ERR_INTERNAL_SERVER_ERROR,"获取登录信息失败,请重新登录!");nStringjsonString=JSON.toJSONString(jsonResult);nSystem.out.println("响应结果:"+jsonString);nresponse.setContentType("application/json;charset=utf-8");nresponse.getWriter().println(jsonString);nreturn;n}nn//将此前从JWT中读取到的permissionsString(JSON字符串)转换成Collection<?extendsGrantedAuthority>nList<SimpleGrantedAuthority>permissionsn=JSON.parseArray(permissionsString,SimpleGrantedAuthority.class);nSystem.out.println("从JWT中获取到的权限转换成SpringSecurity要求的类型:"+permissions);n//将解析得到的用户信息传递给SpringSecurityn//获取SpringSecurity的上下文,并将Authentication放到上下文中n//在Authentication中封装:用户名、null(密码)、权限列表n//因为接下来并不会处理认证,所以Authentication中不需要密码n//后续,SpringSecurity发现上下文中有Authentication时,就会视为已登录,甚至可以获取相关信息nAuthenticationauthenticationn=newUsernamePasswordAuthenticationToken(username,null,permissions);nSecurityContextHolder.getContext().setAuthentication(authentication);nSystem.out.println("将解析得到的用户信息传递给SpringSecurity");n//放行nSystem.out.println("JwtAuthenticationFilter放行");nfilterChain.doFilter(request,response);n}nn}n

你可能在添加了这个类中的代码后会有一些报错,是因为错误码没有提前声明在枚举类,自己手动添加一下。

接着在SecurityConfiguration类上添加一个新的注解

ini复制代码@EnableGlobalMethodSecurity(prePostEnabled=true)//新增n

作用是开启“通过注解配置权限”的功能。

下面,我们来做个测试,在任何你需要设置权限的处理请求的方法上,通过@PreAuthorize注解来实现通过注解配置权限功能,你可以配置你想要的某种权限:

在AdminController类中添加如下方法:

less复制代码@GetMapping("/codingfire")n@PreAuthorize("hasAuthority('单频道观看')")//新增npublicStringcodingfire(){nreturn"codingfire";n}n

重启项目,使用具有“单频道观看”权限的用户可以直接访问,不具有此权限的用户则不能访问,将出现403错误,可通过在线文档功能进行测试。

在线文档添加请求头方式:

请求头内的数据使用正常用的登录后返回的JWT数据,登录的用户权限可自己调整,然后访问codingfire接口查看结果。博主就不再贴后续的内容了。

结语

虽然这篇博客结束了,但登录并没有结束,登录的整体逻辑还有不少,关键的部分本文已经全部列出,剩下的就要大家在实战中慢慢叠加了。3w字才码完,可以说是自己又学习了一遍,你会发现,很多东西都是套路的固定的,只有少部分东西是需要自己去写的,那就是涉及业务的部分。希望大家都能有所收获。

OK,关于Javaparser HD和单点登录初体验的内容到此结束了,希望对大家有所帮助。