基础技术
自定义拦截器-HandlerInterceptor
一般情况下,对来自浏览器的请求,在该请求进行业务逻辑处理之前,我们是可以进行拦截处理的,实 现的方式有两种,分别是基于Tomcat的Filter,和基于SpringMVC的HandlerInterceptor。
- 基于Tomcat的Filter实现拦截,可以利用SpringBoot的FilterRegistrationBean实现Filter和Spring 的整合,向Tomcat注册自定义Filter,使Filter可以在Servlet处理请求之前拦截请求。
- 基于SpringMVC中的拦截器HandlerInterceptor实现拦截器,拦截器可以在Controller中相应的 Action方法处理请求前进行拦截
Filter和Interceptor的区别: 1. Filter直接依赖Servlet容器,而Interceptor不直接依赖于Servlet容器 2. Filter对Http请求起作用,而Interceptor只能对action请求起作用。 3. Interceptor可以访问Action的上下文值栈里的对象,即接收的参数,而Filter不能。 4. 在action的生命周期里,Interceptor可以被多次调用,而Filter针对一个请求只会被调用一次。 5. Filter在过滤是只能对request和response进行操作,而interceptor可以对request、response、handler、 modelAndView、exception进行操作
代码实现
@Component public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("执行了preHandle"); return false; } }
@Configuration public class MyWebMvcConfiure implements WebMvcConfigurer { @Autowired MyInterceptor myInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry .addInterceptor(myInterceptor) .addPathPatterns("/**"); } }
请求的拦截流程:
TK-Mybatis
官网:https://github.com/abel533/Mapper/wik
TK-Mybatis是在Mybatis的基础之上进行了简化开发,主要针对单表的查询更方便了
使用步骤:
- 导入依赖
<!--Tk-Mybatis相关--> <!--Mysql连接--> <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> <!--durid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.16</version> </dependency> <!--tk-Mybatis--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency>
- 数据源配置
spring: datasource: url: jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf8&useOldAliasMetadataBehavior=true&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver
这里在配置的时候一定要注意带上时区的信息,否则会报错
- 代码
- DO类
@Data @Table(name = "user") //设置该类对应的表名 public class User { @Id //标记为主键 @KeySql(useGeneratedKeys = true) //开启插入返回主键 public Integer id; @Column(name = "username") //当类中的属性名和表中的字段对不上时,使用Column注解来转换 public String name; public String mobile; }
- 接口
public interface UserMapper extends Mapper<User> { }
- SpringBoot的启动类
@SpringBootApplication @MapperScan(basePackages = "com.fh.tkmybatis.tk") //扫描mapper包配置,注意要使用tk-mybatis的 public class TkMybatisApplication { public static void main(String[] args) { SpringApplication.run(TkMybatisApplication.class, args); } }
- 使用
@Test public void testInsert(){ // User user = new User(); // user.setMobile("1234567"); // user.setName("whwu"); // userMapper.insert(user); User user = new User(); user.setName("zl"); userMapper.insertSelective(user); } @Test public void query(){ // User user = userMapper.selectByPrimaryKey(6); // System.out.println(user); // User user = new User(); // user.setId(7); // User user1 = userMapper.selectByPrimaryKey(user); // System.out.println(user1); // Example example = new Example(User.class); // // example.createCriteria().andEqualTo("name", "zl"); // // List<User> users = userMapper.selectByExample(example); // // System.out.println(users); Example example = new Example(User.class); example.createCriteria().andLike("name","%z%"); List<User> users = userMapper.selectByExample(example); System.out.println(users); } @Test public void testUpdate(){ User user = new User(); user.setName("景天"); Example example = new Example(User.class); example.createCriteria().andEqualTo("mobile","1234"); userMapper.updateByExampleSelective(user,example); } @Test public void testDelete(){ Example example = new Example(User.class); example.createCriteria().andEqualTo("name","景天"); userMapper.deleteByExample(example); }
MapStruct
在一个成熟的工程中,尤其是现在的分布式系统中,应用与应用之间,还有单独的应用细分模块之 后, DO一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO用于对象传输,也即 是 DO对象 对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。
DTO : Data Transport Object (数据传输对象)
VO: View Object (视图解析对象)
- 导入依赖
<!--mapstruct--> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.3.0.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.3.0.Final</version> </dependency>
- 定义转换器接口
@Mapper(componentModel = "spring") public interface UserConverter { @Mappings({ @Mapping(source = "name",target = "username") }) UserDTO usertoUserDTO(User user); List<UserDTO> usersToUserDTOList(List<User> users); }
- 同样的,如果这里出现了DO类和DTO类的属性名称对不上,那么是可以使用@Mappings注解来进行转换的,如果定义了单个对象的转换的方法上加上了注解,那在List的方法上就不需要加上注解了
@Data public class UserDTO { public Integer id; public String username; }
- 使用定义好的转换器
@Test public void testMapStruct(){ Example example = new Example(User.class); example.createCriteria().andLike("name","%h%"); List<User> users = userMapper.selectByExample(example); System.out.println(users); List<UserDTO> userDTOS = userConverter.usersToUserDTOList(users); System.out.println(userDTOS); }
SPI
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用 它来做接口实现类的扩展发现(Dubbo、JDBC等), 简单来说,它就是一种动态替换发现的机制, 举个例子来 说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。
Java SPI的机制的约定
- 当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录中同时 创建一个以服务接口命名的文件。
- 该文件里就是实现该服务接口的具体实现类。
- 而当程序运行时,使用这个接口的实现类的时,就能通过该jar包META-INF/Services/的配置 文件找到具体的实现类名。
- 并加载该实现类(可能有多个)并实例化 基于这样的一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。
具体的代码实现:
- 首先定义接口
public interface Pay { void payAccount(Double money); }
- 定义接口实现类
public class AliPay implements Pay { @Override public void payAccount(Double money) { System.out.println("支付宝支付"+money+"元"); } }
public class WechatPay implements Pay { @Override public void payAccount(Double money) { System.out.println("微信支付"+money+"元"); } }
配置文件的名字需要和接口的全限定类名一致,里面的内容填写接口的实现类的全限定类名即可
com.fh.tkmybatis.spi.WechatPay com.fh.tkmybatis.spi.AliPay
- 使用过程
public class Test { public static void main(String[] args) { ServiceLoader<Pay> load = ServiceLoader.load(Pay.class); Iterator<Pay> iterator = load.iterator(); while (iterator.hasNext()){ Pay next = iterator.next(); next.payAccount(200.0); } } }
Gateway&JWT
API网关
概述
API是Application Programming Interface缩写,翻译成中文就是应用程序接口。在实际微服务中可以 理解一个个功能方法。就比如你一个用户服务的微服务,可以对外提供 API 接口为,查找用户,创建用 户等。
网关:
在计算机网络中,网关(英语:Gateway)是转发其他服务器通信数据的服务器,接收从客户端发送来的请求时,它就像自己拥有资源的源服务器一样对请求进行处理
如果没有网关,难道不行吗?功能上是可以的,我们直接调用提供的接口就可以了。那为什么还需要网 关?
因为网关的作用不仅仅是转发请求而已。我们可以试想一下,如果需要做一个请求认证功能,我们可以 接入到 API 服务中。但是倘若后续又有服务需要接入,我们又需要重复接入。这样我们不仅代码要重复 编写,而且后期也不利于维护。
由于接入网关后,网关将转发请求。所以在这一层做请求认证,天然合适。这样这需要编写一次代码, 在这一层过滤完毕,再转发给下面的 API。 所以 API 网关的通常作用是完成一些通用的功能,如请求认证,请求记录,请求限流,黑白名单判断 等。
API网关是一个服务器,是系统的唯一入口。
API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有 的非业务功能。通常,网关也是提供REST/HTTP的访问API。
架构图
JWT
简介
JWT全称 Json·Web·Token,是一个开放标准(RFC·7519),它定义了一种紧凑的,自包含的方式,用 于作为JSON对象在各方之间安全的传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT是目前最流行的跨域身份解决方案。
使用场景
下列场景中使用JWT是很有用的:
- Information Exchange(信息交换):对于安全的在各方之间传输信息而言,Json·Web·Token 无疑是一种很好的方式。因为JWT可以被签名,我们还可以验证内容有没有被篡改。
- 正因为数据可以基于JWT进行安全的传输,所以基于JWT,我们可以实现单点登录功能
单点登录:Single Sign On,简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在 多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
单点登录问题及解决方案
方案1:Nginx负载均衡做iphash,同一个用户的请求永远只给到同一个后台服务器。
不足之处:
- 一个服务有若干个用户,有的用户活跃,向后端发起请求的次数比较频繁,而有的用户不活跃,那 么基于这种iphash的解决方案的话就势必会造成负载不均衡,也就是说有的tomcat服务服务器压 力大,而有的tomcat服务器压力不大,造成资源分配不均的情况。
- 其次就是假如有一个tomcat服务挂了,那么对于有一些用户来说,这个系统就挂了,失去了集群 的意义。
方案2:基于tomcat广播的session复制
不足之处:
- 每一个tomcat都需要维护一个大的session,会造成内存资源紧张。
- 大量复制session占用服务器带宽
方案3: 将session数据,以能够共享的方式存储,比如存储到Mysql,或者Redis数据库中
- 存储session数据时,还要多访问一次数据库,或涉及到网络,或IO过程
解决方案:使用JWT能够完美的解决上述问题
基于Token的身份认证,大概流程如下:
1.客户端使用用户名、密码登陆
2.服务器接收到请求后去验证用户名和密码
3.验证成功之后服务端会签发一个token,并且将这个token发送给客户端
4.客户端接收到了token之后将他存储起来
5.客户端每次向服务器发起请求的时候都需要带着服务器签发的token
6.服务端接收到请求,然后去验证客户端中的token,如果验证成功,就返回请求数据,否则不返回
Token VS Session
在看JWT如何解决上述问题之前,我们先来看看基于Token的身份认证与基于session的身份认证。
- 基于session的身份认证。
- Sessions:每次用户认证通过以后,服务器需要创建一条记录来保存用户信息,通常是在内 存中。那么随着认证通过的用户越来越多,服务器在这里的开销就会越来越大。
- Scalability:由于session是在内存中的,这就带来一些扩展性的问题。
- CSRF:用户很容易受到CSRF的攻击
CSRF:跨站请求伪造(早期的攻击方式) 假设一个网站用户Bob可能正在浏览聊天论坛,而同时另一个用户Alice也在此论坛中,并且后者 刚发布了一个具有Bob银行链接的图片信息。正常情况下,Bob点击图片链接访问银行,银行会让 Bob登录 假如Bob在此之前刚好登录过了银行,浏览器中还有cookie,那么此时银行可能认为点击这个图片访问银行是Bob发出的,所以会正常给出响应。假如这个链接是一个Alice伪造的转账的请求,那么就会产生经济损失
对比
- 而JWT是把用户信息保存在客户端,基于token的方式将用户状态分散到了各个客户端中,可以减 轻服务端的内存压力。
- session的状态存储在服务端,客户端只有sessionId,而token的状态是存储在客户端,这就使得 服务器端并未存储用户登录状态,是无状态的,因为是无状态的,所以便于集群的扩容。每次在扩容的时候就不需要关心session的复制等问题
- 安全
- Token不是cookie。每次请求的时候token都会被发送,可以作为请求参数发送,可以放在请 求头里面发送,也可以放在cookie里面被发送
- 即使在你的实现中将token存储到客户端的cookie中,这个cookie也只是一种存储机制,而 非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话
JWT token的基本格式
JSON·Web·Token由三部分组成,他们之间用圆点(·)连接,这三部分分别是
- Header
- Payload
- Signature
Header由两部分的信息组成:
- type:声明类型,这里的是jwt
- alg:声明加密的算法 通常直接使用 HMAC SHA256
Payload就是存放有效信息的地方(不强制)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
claim:jwt存放信息的地方
Signature:签名信息
因此,一个典型的JWT看起来是这个样子的: xxxxxxxx·yyyyyyyyyy·zzzzzzzzzzz
例如:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3bGd6cyIsImV4cCI6MTU4Nzk3MzY1NywidXNlciI6Ijk2MkYxODkwNTVFMzRFNzVERjVGMzQ0QTgxODNCODdGIn0.APehq9dxRiilgTOGyuz9qtZxvPDIJ5QIIVUCLYeX1QE
项目中如何使用
项目已经整合好了JWT,并且给我们提供了现有的工具类进行操作
com.mall.user.utils.JwtTokenUtils
如何使用现有的工具创建JWT呢?
String token = JwtTokenUtils.builder().msg("xxx").build().creatJwtToken();
如何解析JWT呢?
String msg = JwtTokenUtils.builder().token(token).build().freeJwt();
JWT的具体的执行流程
- 首先在登陆的时候进行用户名密码验证,如果通过了那就通过加密算法存储信息后将token给客户端
- 客户端之后访问网站的时候只需要携带token即可,然后服务器通过预存的解密算法进行解密,如果能正常的解密出来,那就说明是正确的token,即放行请求,否则不通过
- 即使是多台服务器,只需要进行相同的方法解密就行了
TOGyuz9qtZxvPDIJ5QIIVUCLYeX1QE
### 项目中如何使用 项目已经整合好了JWT,并且给我们提供了现有的工具类进行操作
com.mall.user.utils.JwtTokenUtils
如何使用现有的工具创建JWT呢? ```java String token = JwtTokenUtils.builder().msg("xxx").build().creatJwtToken();
如何解析JWT呢?
String msg = JwtTokenUtils.builder().token(token).build().freeJwt();
JWT的具体的执行流程
- 首先在登陆的时候进行用户名密码验证,如果通过了那就通过加密算法存储信息后将token给客户端
- 客户端之后访问网站的时候只需要携带token即可,然后服务器通过预存的解密算法进行解密,如果能正常的解密出来,那就说明是正确的token,即放行请求,否则不通过
- 即使是多台服务器,只需要进行相同的方法解密就行了