mall整合SpringBoot+MyBatis搭建基本骨架

mall-tiny-01

重点为MyBatis的自动生成代码。

当运行MyBatis的Generator时,配置数据库连接,会读取数据库的表结构,Mybatis generator生成model、mapper接口及mapper.xml的路径。

1
2
3
4
5
6
7
8
9
10
11
<!--指定生成model的路径-->
<javaModelGenerator targetPackage="com.macro.mall.tiny.mbg.model" targetProject="mall-tiny-01\src\main\java"/>
<!--指定生成mapper.xml的路径-->
<sqlMapGenerator targetPackage="com.macro.mall.tiny.mbg.mapper" targetProject="mall-tiny-01\src\main\resources"/>
<!--指定生成mapper接口的的路径-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.macro.mall.tiny.mbg.mapper"
targetProject="mall-tiny-01\src\main\java"/>
<!--生成全部表tableName设为%-->
<table tableName="pms_brand">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>

实现表的基本增删改查。

mall整合Swagger-UI实现在线API文档

地址

mall-tiny-02

新项目使用Swagger UI自动生成接口文档,不需要频繁更新接口文档,保证接口文档与代码的一致

Swagger UI:提供了一个可视化的UI页面展示描述文件。接口的调用方、测试、项目经理等都可>以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入描述文件和本地部署UI项目。

Swagger-UI是HTML, Javascript, CSS的一个集合,可以动态地根据注解生成在线API文档。

常用注解

  • @Api:用于修饰Controller类,生成Controller相关文档信息
  • @ApiOperation:用于修饰Controller类中的方法,生成接口方法相关文档信息
  • @ApiParam:用于修饰接口中的参数,生成接口参数相关文档信息
  • @ApiModelProperty:用于修饰实体类的属性,当实体类是请求参数或返回结果时,直接生成相关文档信息

整合流程

  1. 添加项目依赖
  2. 添加Swagger-UI的Java配置文件

注意:Swagger对生成API文档的范围有三种不同的选择

  • 生成指定包下面的类的API文档
  • 生成有指定注解的类的API文档
  • 生成有指定注解的方法的API文档
  1. 给controller增加注解
  2. 修改MyBatis Generator注释的生成规则

CommentGenerator为MyBatis Generator的自定义注释生成器,修改addFieldComment方法使其生成Swagger的@ApiModelProperty注解来取代原来的方法注释,添加addJavaFileComment方法,使其能在import中导入@ApiModelProperty,否则需要手动导入该类,在需要生成大量实体类时,是一件非常麻烦的事。

  1. 运行项目

接口文档地址:http://localhost:8080/swagger-ui.html

可直接在在线文档上面进行接口测试

mall整合Redis实现缓存功能

mall-tiny-03

下载Redis,下载地址:https://github.com/MicrosoftArchive/redis/releases

  1. 添加项目依赖

    1
    2
    3
    4
    5
    <!--redis依赖配置-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    1. 修改springboot配置文件,增加redis配置
1
2
3
4
5
6
7
8
9
10
11
12
redis:
host: localhost # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: # Redis服务器连接密码(默认为空)
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
timeout: 3000ms # 连接超时时间(毫秒)
  1. 在根节点下添加Redis自定义key的配置
1
2
3
4
5
6
7
# 自定义redis key
redis:
key:
prefix:
authCode: "portal:authCode:"
expire:
authCode: 120 # 验证码超期时间
  1. 添加RedisService接口用于定义一些常用Redis操作

存储数据 获取数据 设置超期时间 删除数据 自增操作

  1. RedisServiceImpl 注入StringRedisTemplate,实现RedisService接口
1
2
3
4
//验证码绑定手机号并存储到redis
redisService.set(REDIS_KEY_PREFIX_AUTH_CODE + telephone, sb.toString());
redisService.expire(REDIS_KEY_PREFIX_AUTH_CODE + telephone, AUTH_CODE_EXPIRE_SECONDS);
return CommonResult.success(sb.toString(), "获取验证码成功");
1
2
3
//对输入的验证码进行校验
String realAuthCode = redisService.get(REDIS_KEY_PREFIX_AUTH_CODE + telephone);
boolean result = authCode.equals(realAuthCode);

mall整合SpringSecurity和JWT实现认证和授权(一)

mall-tiny-04

本文主要讲解mall通过整合SpringSecurity和JWT实现后台用户的登录和授权功能,同时改造Swagger-UI的配置使其可以自动记住登录令牌进行发送。

SpringSecurity

SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。

JWT

JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。

JWT的组成

  • JWT token的格式:header.payload.signature

  • header中用于存放签名的生成算法

    1
    {"alg": "HS512"}Copy to clipboardErrorCopied
  • payload中用于存放用户名、token的生成时间和过期时间

    1
    {"sub":"admin","created":1489079981393,"exp":1489684781}Copy to clipboardErrorCopied
  • signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败

    1
    2
    //secret为加密算法的密钥
    String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

JWT实现认证和授权的原理

  • 用户调用登录接口,登录成功后获取到JWT的token;
  • 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token;
  • 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。

Hutool是一个丰富的Java开源工具包,它帮助我们简化每一行代码,减少每一个方法,mall项目采用了此工具包。

整合SpringSecurity及JWT

  1. 添加项目依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--SpringSecurity依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Hutool Java工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.7</version>
</dependency>
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
  1. 添加JWT token的工具类

用于生成和解析JWT token的工具类

相关方法说明:

  • generateToken(UserDetails userDetails) :用于根据登录用户信息生成token
  • getUserNameFromToken(String token):从token中获取登录用户的信息
  • validateToken(String token, UserDetails userDetails):判断token是否还有效

题外话

在很多post,put,delete等请求之前,会有一次options请求。什么是options请求呢?它是一种探测性的请求,通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。用以检查请求是否是可靠安全的,如果options获得的回应是拒绝性质的,比如404\403\500等http状态,就会停止post、put等请求的发出。

前台跨域post请求,由于CORS(cross origin resource share)规范的存在,浏览器会首先发送一次options嗅探,同时header带上origin,判断是否有跨域请求权限,服务器响应access control allow origin的值,供浏览器与origin匹配,如果匹配则正式发送post请求。

如果有服务器程序权限,设置,比如jsp中,设置header access control allow origin等于*,就可以得到跨域访问的目的。

摘自:https://www.cnblogs.com/ermaoblog/p/8855915.html

在正式跨域的请求前,浏览器会根据需要,发起一个“PreFlight”(也就是Option请求),用来让服务端返回允许的方法(如get、post),被跨域访问的Origin(来源,或者域),还有是否需要Credentials(认证信息)。 三种场景:

  1. 如果跨域的请求是Simple Request(简单请求 ),则不会触发“PreFlight”。Mozilla对于简单请求的要求是: 以下三项必须都成立:
  2. 只能是Get、Head、Post方法
  3. 除了浏览器自己在Http头上加的信息(如Connection、User-Agent),开发者只能加这几个:Accept、Accept-Language、Content-Type
  4. Content-Type只能取这几个值: application/x-www-form-urlencoded 、multipart/form-data text/plain

XHR对象对于HTTP跨域请求有三种:简单请求、Preflighted 请求、Preflighted 认证请求。简单请求不需要发送OPTIONS嗅探请求,但只能按发送简单的GET、HEAD或POST请求,且不能自定义HTTP Headers。Preflighted 请求和认证请求,XHR会首先发送一个OPTIONS嗅探请求,然后XHR会根据OPTIONS请求返回的Access-Control-*等头信息判断是否有对指定站点的访问权限,并最终决定是否发送实际请求信息。 那么我的get请求呢? 原来,产生 OPTIOINS 请求的原因是:自定义 Headers 头信息导致的。 浏览器会去向 Server 端发送一个 OPTIONS 请求,看 Server 返回的 “Access-Control-Allow-Headers” 是否有自定义的 header 字段。因为我之前没有返回自定义的字段,所以,默认是不允许的,造成了客户端没办法拿到数据。

  1. 添加SpringSecurity的配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
.antMatchers("/admin/login", "/admin/register")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
// .antMatchers("/**")//测试时全部运行访问
// .permitAll()
.anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated();

相关依赖及方法说明

  • configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
  • configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
  • RestfulAccessDeniedHandler:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;
  • RestAuthenticationEntryPoint:当未登录或token失效时,返回JSON格式的结果;
  • UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
  • UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;
  • PasswordEncoder:SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;
  • JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录。

添加RestfulAccessDeniedHandler

添加RestAuthenticationEntryPoint

添加AdminUserDetails

添加JwtAuthenticationTokenFilter

在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。

mall整合SpringSecurity和JWT实现认证和授权(二)

mall-tiny-04

接上一篇,controller和service层的代码实现及登录授权流程演示。

  1. 添加UmsAdminController类

实现了后台用户登录、注册及获取权限的接口

  1. 添加UmsAdminService接口

  2. 添加UmsAdminServiceImpl类

  3. 修改Swagger的配置

通过修改配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了。

  1. 给PmsBrandController接口中的方法添加访问权限
  • 给查询接口添加pms:brand:read权限
  • 给修改接口添加pms:brand:update权限
  • 给删除接口添加pms:brand:delete权限
  • 给添加接口添加pms:brand:create权限
1
2
3
4
@PreAuthorize("hasAuthority('pms:brand:read')")
public CommonResult<List<PmsBrand>> getBrandList() {
return CommonResult.success(brandService.listAllBrand());
}

不明白,是不是SpringSecurity的要求?

是设置的权限控制,登录后会查询该用户的权限列表,请求到达controller之前会被SpringSecurity拦截进行返回403错误。返回设置在SpringSecurity的配置类

1
2
3
4
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);

从数据库中查询出当前登录用户的所有权限并交给security管理;注解@PreAuthorize(“hasAuthority(‘xxx’)”)来判断“xxx”是否在当前登录用户的权限集合中,在则200,不在则403。

mall整合SpringTask实现定时任务

mall-tiny-05

SpringTask

SpringTask是Spring自主研发的轻量级定时任务工具,相比于Quartz更加简单方便,且不需要引入其他依赖即可使用。

Cron表达式是一个字符串,包括6~7个时间元素,在SpringTask中可以用于指定任务的执行时间。

cron语法格式:Seconds Minutes Hours DayofMonth Month DayofWeek

Cron格式中每个时间元素的说明:

时间元素 可出现的字符 有效数值范围
Seconds , - * / 0-59
Minutes , - * / 0-59
Hours , - * / 0-23
DayofMonth , - * / ? L W 0-31
Month , - * / 1-12
DayofWeek , - * / ? L # 1-7或SUN-SAT

Cron格式中特殊字符说明

字符 作用 举例
, 列出枚举值 在Minutes域使用5,10,表示在5分和10分各触发一次
- 表示触发范围 在Minutes域使用5-10,表示从5分到10分钟每分钟触发一次
* 匹配任意值 在Minutes域使用*, 表示每分钟都会触发一次
/ 起始时间开始触发,每隔固定时间触发一次 在Minutes域使用5/10,表示5分时触发一次,每10分钟再触发一次
? 在DayofMonth和DayofWeek中,用于匹配任意值 在DayofMonth域使用?,表示每天都触发一次
# 在DayofMonth中,确定第几个星期几 1#3表示第三个星期日
L 表示最后 在DayofWeek中使用5L,表示在最后一个星期四触发
W 表示有效工作日(周一到周五) 在DayofMonth使用5W,如果5日是星期六,则将在最近的工作日4日触发一次

业务场景说明

  • 用户对某商品进行下单操作;
  • 系统需要根据用户购买的商品信息生成订单并锁定商品的库存;
  • 系统设置了60分钟用户不付款就会取消订单;
  • 开启一个定时任务,每隔10分钟检查下,如果有超时还未付款的订单,就取消订单并取消锁定的商品库存。
  1. 整合添加,由于SpringTask已经存在于Spring框架中,所以无需添加依赖

  2. 添加,只需要在配置类中添加一个@EnableScheduling注解即可开启SpringTask的定时任务能力。

1
2
3
4
5
6
7
8
/**
* 定时任务配置
* Created by macro on 2019/4/8.
*/
@Configuration
@EnableScheduling
public class SpringTaskConfig {
}

添加OrderTimeOutCancelTask来执行定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Created by macro on 2018/8/24.
* 订单超时取消并解锁库存的定时器
*/
@Component
public class OrderTimeOutCancelTask {
private Logger LOGGER = LoggerFactory.getLogger(OrderTimeOutCancelTask.class);

/**
* cron表达式:Seconds Minutes Hours DayofMonth Month DayofWeek [Year]
* 每10分钟扫描一次,扫描设定超时时间之前下的订单,如果没支付则取消该订单
*/
@Scheduled(cron = "0 0/10 * ? * ?")
private void cancelTimeOutOrder() {
// TODO: 2019/5/3 此处应调用取消订单的方法,具体查看mall项目源码
LOGGER.info("取消订单,并根据sku编号释放锁定库存");
}
}

后面暂时用不到,放到以后学习。end;