SpringBoot Security JJWT
一、api开发阶段
1、项目构建(多模块开发)
主模块:无代码只有pom.xml文件
io.spring.platform
platform-bom
Brussels-SR4
pom
import
org.springframework.cloud
spring-cloud-dependencies
Dalston.SR2
pom
import
org.apache.maven.plugins
maven-compiler-plugin
2.3.2
1.8
UTF-8
../imooc-security-app
../imooc-security-browser
../imooc-security-core
../imooc-security-demo
core:核心业务逻辑
xxx
xxx
....
xxx
browser:浏览器安全特定代码
com.imooc.security
imooc-security-core
${imooc.security.version}
app:app相关特定代码
demo:demo程序
org.springframework.boot
spring-boot-maven-plugin
1.3.3.RELEASE
repackage
demo
2、demo API开发
3-2 增加分页查询 pagehelper
一、添加依赖
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.2
二、配置(可以不设置使用默认)
#pagehelper 分页插件
pagehelper:
helper-dialect: mysql # 分页插件会自动检测当前的数据库链接,自动选择合适的分页方式(可以不设置)
reasonable: true #合法性,即纠错机制,true,这时如果 pageNum <= 0 会查询第一页,如果 pageNum > pages 会查询最后一页。
# support-methods-arguments: true
# params: countSql
三、使用
@GetMapping("/getAll")
public ResultJson selectAll(@RequestParam(name = "pageNum",required = true,defaultValue = "1")int pageNum,@RequestParam(name = "pageSize",required = true,defaultValue = "5")int pageSize) {
PageHelper.startPage(pageNum,pageSize); //重点
List users = userService.selectAll();
PageInfo pageInfo = new PageInfo<>(users);//重点
return ResultJson.ok(pageInfo);
}
3-3 编写用户详情服务
1、@PathVariable映射url片段到java方法的参数
2、在url申明中使用正则表达式
@DeleteMapping("user/{id:\\d+}")//必须为数字
3、@JsonView控制json输出内容
需求:不同的服务展示的字段不同,限制某些字段不显示
实现步骤:
1. 使用接口来声明多个视图
@Component
public class User {
public interface UserSimpleView {};
public interface UserDetailView extends UserSimpleView {};
private String id;
private String name;
.......
}
2. 在值对象的get方法上指定视图
//因为UserDetailView继承了UserSimpleView,所以也拥有UserSimpleView的值
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
@JsonView(UserSimpleView.class)
public String getLoginname() {
return loginname;
}
3. 在Controller方法上指定视图
@GetMapping("/getAll")
@JsonView(User.UserDetailView.class)
//只能返回List对象无法返回再次封装对象(未解决)
public List selectAll(int pageNum,int pageSize) {
//PageHelper.startPage(pageNum,pageSize);
List users = userService.selectAll();
//PageInfo pageInfo = new PageInfo<>(users);
return users;
}
3-4 处理创建请求
@RequestBody映射请求体到java方法的参数
日期类型参数的处理(前后台传递:时间戳)
@valid注解和BindingResult验证请求参数的合法性并处理校验结果
@PostMapping("/register")
@ApiOperation(value = "注册")
public ResultJson register(@Valid @RequestBody User user,BindingResult errors) {
//使用@Valid BindingResult配合,处理校验结果
if (errors.hasErrors()){
errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
return ResultJson.failure(ResultCode.BAD_REQUEST);
}
return userService.register(user);
}
3-5开发用户信息修改和删除服务
1、常用的验证注解
注解 | 含义 |
---|---|
@Null |
限制只能为null |
@NotNull |
限制必须不为null |
@AssertFalse |
限制必须为false |
@AssertTrue |
限制必须为true |
@DecimalMax(value) |
限制必须为一个不大于指定值的数字 |
@DecimalMin(value) |
限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) |
限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future |
限制必须是一个将来的日期 |
@Max(value) |
限制必须为一个不大于指定值的数字 |
@Min(value) |
限制必须为一个不小于指定值的数字 |
@Past |
限制必须是一个过去的日期 |
@Pattern(value) |
限制必须符合指定的正则表达式 |
@Size(max,min) |
限制字符长度必须在min到max之间 |
@Past |
验证注解的元素值(日期类型)比当前时间早 |
@NotEmpty |
验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank |
验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty ,@NotBlank 只应用于字符串且在比较时会去除字符串的空格 |
@Email |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
2、自定义消息
//友好提示
@NotBlank(message = "密码不能为空")
private String password;
3、自定义校验注解
/**
1、新建一个注解类
*/
//标注的位置:方法、字段
@Target({ElementType.METHOD,ElementType.FIELD})
//运行时的注解
@Retention(RetentionPolicy.RUNTIME)
//当前注解用哪个类去校验(执行逻辑)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
//设置校验失败信息
String message() ;
//可不用必须有
Class>[] groups() default { };
//可不用必须有
Class extends Payload>[] payload() default { };
}
/**
2、校验执行类
*/
public class MyConstraintValidator implements ConstraintValidator {
//两个泛型。1、是实现哪个注解类,2、是可以放在什么类型的字段上
@Override
public void initialize(MyConstraint constraintAnnotation) {
System.out.println("注解初始化");
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
System.out.println("打印值:"+value);
return true;//返回true or false 是否校验成功
}
}
3-6 RESTful API错误处理
Spring Boot中默认的错误处理机制
自定义异常处理(默认基本够用)
3-7 RESTful API的拦截
过滤器 Filter(无法获取所调用方法的名称、参数,可以获取request,response)
//1 编写一个过滤器实现(implements)Filter接口
//2 分别实现init、doFilter(chain.doFilter(request,response))、destroy方法
//3 注册:将Filter配置到项目。方法一:在Filter类上添加@Component.方法二:添加WebConfig配置类
拦截器 Interceptor(无法获取所调用方法的参数,可以获取request,response,所调用方法名称)
// 1 编写一个拦截器实现(implements)HandlerInterceptor接口
// 2 分别实现
preHandle (通过request.setAttribute传递信息给postHandle)
postHandle(看preHandle是否报错决定是否执行)
afterCompletion(始终执行)
// 3 声明@Component注解
// 4 注册:写配置类继承WebMvcConfigurerAdapter
@Configuration
public class AddInterceptor extends WebMvcConfigurerAdapter{
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login")
.excludePathPatterns("/loginPost");
}
}
切片Aspect(无法获取request,response,可以获取所调用方法参数)
//编写切片,定义切片,定义切入点
@Aspect
@Component
public class MyAspect {//这就是一个切片
/**
* 定义切入点,切入点为com.example.demo.aop.AopController中的所有函数
* 通过@Pointcut注解声明频繁使用的切点表达式
**/
@Around("execution(* cn.com.controll.UserController.*(..)))")//这是一个切入点,下面的方法就是增强
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
long start =new Date().getTime();
//打印参数
System.out.println("method before execute,aspect start.now time is:"+start);
Object[] args = pjp.getArgs();
for (Object arg:args) {
System.out.println("arg is :"+arg.toString());
}
Object proceed = pjp.proceed();//执行方法
System.out.println("method after execute,aspect end.spend time:"+(new Date().getTime()-start));
return proceed;
}
}
3-8 Spring AOP
3-10 使用多线程提高REST服务性能
1、使用Runnable异步处理Rest服务
只能由主线程调用副线程,无法满足多服务器复杂要求
2、使用DeferredResult异步处理Rest服务
对象:接收请求的应用1、处理逻辑的应用2
过程:1.2.3-> 应用1(线程1)接收请求发送至消息队列,应用2(线程3)监听消息队列的变化进行处理。
4.5.6-> 处理完后发送至消息队列,应用1(线程2)监听消息队列,获取结果,进行响应。
实现过程(模拟,未实现应用2):
//1. 主线程
@RestController
public class AsyncController {
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping("/order")
public DeferredResult order() throws Exception {
logger.info("主线程开始");
String orderNumber = RandomStringUtils.randomNumeric(8);
mockQueue.setPlaceOrder(orderNumber);
DeferredResult result = new DeferredResult<>();
deferredResultHolder.getMap().put(orderNumber, result);
logger.info("主线程结束");
return result;
}
}
//2. 消息队列
@Component
public class MockQueue {
private String placeOrder;
private String completeOrder;
private Logger logger = LoggerFactory.getLogger(getClass());
public String getPlaceOrder() {
return placeOrder;
}
public void setPlaceOrder(String placeOrder) throws Exception {
new Thread(() -> {
logger.info("接到下单请求, " + placeOrder);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
this.completeOrder = placeOrder;
logger.info("下单请求处理完毕," + placeOrder);
}).start();
}
public String getCompleteOrder() {
return completeOrder;
}
public void setCompleteOrder(String completeOrder) {
this.completeOrder = completeOrder;
}
}
//3. 监听器
@Component
public class QueueListener implements ApplicationListener {
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
new Thread(() -> {
while (true) {
if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())) {
String orderNumber = mockQueue.getCompleteOrder();
logger.info("返回订单处理结果:"+orderNumber);
deferredResultHolder.getMap().get(orderNumber).setResult("place order success");
mockQueue.setCompleteOrder(null);
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
//4. DeferredResultHolder(应用1中线程1、2通信)
@Component
public class DeferredResultHolder {
private Map> map = new HashMap>();
public Map> getMap() {
return map;
}
public void setMap(Map> map) {
this.map = map;
}
}
3、异步处理配置
3-11 与前端协同开发
使用Swagger自动生成文档
使用WireMock伪造REST服务
swagger
1、添加依赖
io.springfox
springfox-swagger2
2.7.0
io.springfox
springfox-swagger-ui
2.7.0
2、新增配置类
@Configuration
public class ApplicationConfig extends WebMvcConfigurationSupport {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
}
3、在启动类上添加注释
@EnableSwagger2
4、在controller和domain上添加注释
@ApiOperation(value = "注册")//方法
@ApiParam("用户ID")//路径参数
@ApiModelProperty(value = "登陆密码")//domain对象
WireMock
1、下载 wiremock-standalone-2.27.0.jar 包
2、开启wiremock服务
java -jar wiremock-standalone-2.27.0.jar --port 9999
3、创建假数据文件,并发送到服务
public class MockServer {
public static void main(String[] args) throws IOException {
WireMock.configureFor(9999);
WireMock.removeAllMappings();
mock("/mock/response/01.txt","/order/1");
mock("/mock/response/02.txt","/order/2");
}
public static void mock(String ResourceUrl, String ApiUrl) throws IOException {
ClassPathResource classPathResource = new ClassPathResource(ResourceUrl);
File file = classPathResource.getFile();
List strings = FileUtils.readLines(file, "UTF-8");
String content = StringUtils.join(strings.toArray(),"\n");
WireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo(ApiUrl))
.willReturn(WireMock.aResponse()
.withBody(content)
.withStatus(200)));
}
}
二、SpringSecurity认证、授权阶段
1、SpringSecurity基本原理
1.1 ExceptionTranslationFilter:FilterSecurityInterceptor未通过时的异常处理
1.2 FilterSecurityInterceptor:根据配置类中的配置来判断是否通过
1.3 自定义用户认证逻辑
//1、处理用户信息获取逻辑(UserDetailsService:是否有用户)
重点(UserDetailsService接口):有唯一根据用户名查询用户方法。根据username返回用户信息,信息被封装于(UserDetails接口)的实现类中,再根据UserDetails用户信息去校验。
过程:1.实现 UserDetailsService,根据用户名查询用户信息;
//2、处理用户校验逻辑(UserDetails:是否过期)
普通domain类实现UserDetails。
//3、处理密码加密解密(BCryptPasswordEncoder)
加密:String encode = new BCryptPasswordEncoder().encode("123456");
密码匹配:new BCryptPasswordEncoder().matches();
1.4 个性化用户认证流程
1.4 .1自定义登陆页面
//1 编写配置文件
http.formLogin()
.loginPage("/imooc-signIn.html")//配置登陆页面
.and()
.authorizeRequests()
//登陆页面无需身份认证
.antMatchers("/imooc-signIn.html").permitAll()
.anyRequest().authenticated();
//2 在resources下增加登陆页面
1.4 .2自定义登陆成功处理
.and()
.formLogin().loginPage("/login_p").loginProcessingUrl("/login").permitAll()
1.4 .3自定义登陆失败处理