SpringBoot Security JJWT
一、api开发阶段
1、项目构建(多模块开发)
主模块:无代码只有pom.xml文件
<dependencyManagement>
<!--引入两个dependencies管理maven 所有包的version-->
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--依赖管理-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!--使用maven-compiler-plugin插件可以指定项目源码的jdk版本
,编译后的jdk版本,以及编码-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<!--加入子模块,子模块可使用主模块所有dependencies,
子模块只许引入自己所特需的dependencies-->
<modules>
<module>../imooc-security-app</module>
<module>../imooc-security-browser</module>
<module>../imooc-security-core</module>
<module>../imooc-security-demo</module>
</modules>
core:核心业务逻辑
<!--各种核心业务逻辑依赖,可以被其他子模块引用-->
<dependencies>
<dependency>xxx</dependency>
<dependency>xxx</dependency>
....
<dependency>xxx</dependency>
</dependencies>
browser:浏览器安全特定代码
<!--可以直接引用其他子模块的依赖-->
<dependencies>
<dependency>
<groupId>com.imooc.security</groupId>
<artifactId>imooc-security-core</artifactId>
<version>${imooc.security.version}</version>
</dependency>
</dependencies>
app:app相关特定代码
demo:demo程序
<!--打包可执行jar,将其他子项目下的依赖也都引入-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.3.3.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<finalName>demo</finalName>
</build>
2、demo API开发
3-2 增加分页查询 pagehelper
一、添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.2</version>
</dependency>
二、配置(可以不设置使用默认)
#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<User> users = userService.selectAll();
PageInfo<User> 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<User>对象无法返回再次封装对象(未解决)
public List<User> selectAll(int pageNum,int pageSize) {
//PageHelper.startPage(pageNum,pageSize);
List<User> users = userService.selectAll();
//PageInfo<User> 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<MyConstraint,Object> {
//两个泛型。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<String> order() throws Exception {
logger.info("主线程开始");
String orderNumber = RandomStringUtils.randomNumeric(8);
mockQueue.setPlaceOrder(orderNumber);
DeferredResult<String> 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<ContextRefreshedEvent> {
@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<String, DeferredResult<String>> map = new HashMap<String, DeferredResult<String>>();
public Map<String, DeferredResult<String>> getMap() {
return map;
}
public void setMap(Map<String, DeferredResult<String>> map) {
this.map = map;
}
}
3、异步处理配置
3-11 与前端协同开发
使用Swagger自动生成文档
使用WireMock伪造REST服务
swagger
1、添加依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
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<String> 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自定义登陆失败处理