前言
随着大环境的影响,互联网寒冬降临,程序员的日子越来越难,搞不好哪天就被噶了,多学点东西也没啥坏处,国内市场java如日中天,出门在外不会写两行java代码,都不好意思说自己是程序员,伪装成一个萌新运维,混迹于各大java群,偷师学艺,略有所获,水一篇博客以记之
本博客仅仅代表作者个人看法,以.Net视角来对比,不存在语言好坏之分,不足之处,欢迎拍砖,以免误人子弟,java大佬有兴趣可以带我一jio,探讨学习,懂的都懂
特殊用语 --> 懂的都懂 形容一些心照不宣的事情,可自行百度谷歌....
环境准备
JDK 1.8 IDEA Maven 需配置阿里的源
工程结构
.net里工程结构大致如下: |---解决方案 |---项目A |---项目B |---项目C java里工程结构 |---项目 |---模块A |---模块B |---模块C
- 我们先创建一个空模板项目 文件->新建项目-->Empty Project 指定项目名称以及项目路径即可
- 在该项目路径下,创建对应的模块,比较常用的是Spring,Spring Initializr Maven,这个跟.net类似
starter 中间件
starter 是springboot里提出的一个概念,场景启动器,把一些常用的依赖聚合打包,方便使用者直接在项目中使用,简化了开发,在.net里就是中间件了,一个意思
官方解释
Starters are a set of convenient dependency descriptors that you can include in your application. You get a one-stop shop for all the Spring and related technologies that you need without having to hunt through sample code and copy-paste loads of dependency descriptors. For example, if you want to get started using Spring and JPA for database access, include the spring-boot-starter-data-jpa dependency in your project.
spring生态是毋庸置疑的,开发常用的中间件,spring都整理好了,可以去官网直接查阅,懂的都懂
创建一个自定义的starter
-
新建一个模块,选择Maven模块,给模块取个名字 hello-spring-boot-starter, 取名字要遵循starter的规范,望文知意
-
再创建一个模块,选择Spring Initializr模块,给模块取个名字 hello-spring-boot-starter-autoconfigure,用于给starter编写装配信息,这样spring就能根据约定,自动装配,hello-spring-boot-starter 依赖于 hello-spring-boot-starter-autoconfigure,当然了如果嫌麻烦,直接在 hello-spring-boot-starter 里写装配信息也可以,这个跟.net里类似,懂的都懂
-
java项目起手式,在src/main/java,创建包路径,通常为公司域名,com.xxx.xxx,我这里定义为com.liang.hello
1.在com.liang.hello下,定义三个包
autoConfig 用于编写装配信息,生成对象,spring将这些对象添加到IOC容器 bean 用于映射配置文件,将application.yaml里的配置映射为实体类(javabean) service 用于编写中间件的业务代码,需要使用到配置信息的实体类
2.在bean包下,创建HelloProperties 文件,我定义了两个属性,和一个Student对象
package com.liang.hello.bean; import org.springframework.boot.context.properties.ConfigurationProperties; /** * @ConfigurationProperties("hello")是springboot提供读取配置文件的一个注解 * 1)让当前类的属性和配置文件中以 hello开头的配置进行绑定 * 2)以 hello为前缀在配置文件中读取/修改当前类中的属性值 */ @ConfigurationProperties("hello") public class HelloProperties { private String prefix; private String suffix; private Student student; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } }
package com.liang.hello.bean; public class Student { private String name; private String age; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } }
3.在service包下,定义一个接口,跟一个实现类,简单的输出配置文件的信息
package com.liang.hello.service; public interface BaseService { String sayMsg(String msg); }
package com.liang.hello.service; import com.liang.hello.bean.HelloProperties; import org.springframework.beans.factory.annotation.Autowired; public class HelloService implements BaseService{ @Autowired private HelloProperties helloProperties; public String sayMsg(String msg) { return helloProperties.getPrefix()+": "+msg+">> "+helloProperties.getSuffix() + helloProperties.getStudent().getName() + helloProperties.getStudent().getAge(); } }
4.在autoConfig包下,产生一个bean对象,丢给spring ioc,要标记这个类为一个配置类
package com.liang.hello.autoConfig; import com.liang.hello.bean.HelloProperties; import com.liang.hello.service.BaseService; import com.liang.hello.service.HelloService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration //标识配置类 @EnableConfigurationProperties(HelloProperties.class)//开启属性绑定功能+默认将HelloProperties放在容器中 public class HelloAutoConfiguration { /** * @Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。 * 产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中; * * @ConditionalOnMissingBean(HelloService.class) * 条件装配:容器中没有HelloService这个类时标注的方法才生效 / 创建一个HelloService类 */ @Bean @ConditionalOnMissingBean(HelloService.class) public BaseService helloService() { BaseService helloService = new HelloService(); return helloService; } }
- 前面都非常简单,就是自己生成了一个对象,然后交给spring ioc管理,接下来就是告诉spring 如何寻找到HelloAutoConfiguration
5.在resources/META-INF 下,创建spring.factories 文件,告诉你配置类的位置,srping会扫描包里这个文件,然后执行装配
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.liang.hello.autoConfig.HelloAutoConfiguration
这样一个简单的starter就写好了,使用maven构建一下,并推送到本地仓库,maven类似于nuget,懂的都懂,现在去创建一个测试项目,来测试一下
6.新建模块,选择Spring Initializr模块,给模块取个名字 hello-spring-boot-starter-test,在pom.xml里添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.liang</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
7.java项目起手式,在src/main/java,创建包路径,定义为com.liang.hello
在resources下,定义application.yaml配置文件
hello: prefix: 你好! suffix: 666 and 888 student: name: 雪佬 age: 18 server: port: 8080 servlet: context-path: /
8.在com.liang.hello下,定义controller包,用于webapi的控制器
定义一个HelloController类,编写一个简单的webapi,来测试自定义的starter,DataResponse是我自定义的一个统一返回类
@Autowired BaseService helloService; @ResponseBody @GetMapping("/hello") //处理get请求方式的/hello请求路径 public DataResponse sayHello() //处理方法 { String s = helloService.sayMsg("test666777888"); return DataResponse.Success(s,""); }
- java语法懂的都懂,由于函数没有可选参数,所以需要写很多重载方法
package com.liang.hello.common; import lombok.Builder; import lombok.ToString; @Builder @ToString public class DataResponse { /** * 响应码 */ public String Code; /** * 返回的数据 */ public Object Data; /** * 消息 */ public String Message; public DataResponse(String code,Object data,String message){ Code = code; Data = data; Message = message; } public static DataResponse Error() { return DataResponse.builder().Code("-1").build(); } public static DataResponse Error(Object data) { return DataResponse.builder().Code("-1").Data(data).build(); } public static DataResponse Error(String message) { return DataResponse.builder().Code("-1").Message(message).build(); } public static DataResponse Error(Object data,String message) { return DataResponse.builder().Code("-1").Data(data).Message(message).build(); } public static DataResponse Success() { return DataResponse.builder().Code("0").build(); } public static DataResponse Success(Object data) { return DataResponse.builder().Code("0").Data(data).build(); } public static DataResponse Success(String message) { return DataResponse.builder().Code("0").Message(message).build(); } public static DataResponse Success(Object data,String message) { return DataResponse.builder().Code("0").Data(data).Message(message).build(); } }
弄到这里starter就结束了嘛,显然事情没有这么简单,既然用到了spring的自动装配,那我们不妨往深处再挖一挖,没准有意外收获哦
前面我们已经创建了HelloService,那再创建一个TestService,同样继承BaseService,然后HelloAutoConfiguration类下,在写一个testService的bean,测试一下一个接口多个实现,如何获取指定的实例
非常神奇,spring会自动匹配,根据变量名称,自动匹配bean,点击左侧spring的绿色小图标(类似于断点图标),还能自动跳转到bean的实现,不要问,问就是牛逼,懂的都懂
现在我们已经在starter里创建了2个bean,如果有N个bean,每个bean都要去HelloAutoConfiguration类下写装配,真是太麻烦了,这个时候,就可以使用到spring的自动装配注解,只用在testService类上,加一个@Service的注解,就搞定了,简单方便,连spring.factories都不用写,在.net里的DI框架目前还没有统一,有内置的,用的比较多的是autofac,还有自研的DI框架,都大同小异
项目结构
springboot
springboot现在已经是java web开发的主流了,通常我们用.net core来之对标,他们诞生的初衷完全不一样,springboot是整合自身的生态,化繁为简,starter就是非常具有代表性的特性之一,.net core是一套跨平台方案,诞生之初就是为了跨平台,本身就非常简洁,易用性也非常高,开发者往里面添加所需的中间件即可,它的关注点始终围绕框架的简洁与性能 选择springboot脚手架项目,会自动创建一个启动文件HelloSpringBootStarterTestApplication 里面有一个@SpringBootApplication的组合注解,想了解的可以去翻阅java八股文,这里我加了一个@EntityScan("com.liang.hello")注解,用于自动扫描该包下的bean,并完成装配 控制器类上,要加@RestController 注解,这也是一个组合注解,然后在方法上加@ResponseBody注解,用于返回json类型,指定方法映射的路由,就可以了,如果想做mvc项目,还需要下载模板引擎的依赖,修改返回类型,指向一个视图,略微麻烦些
aop
- 创建完springboot webapi模块,我们需要添加一个切面,用于记录请求的信息
java里分为过滤器与拦截器,过滤器依赖与servlet容器,拦截器是Spring容器的功能,本质上都是aop思想的实现
.net core里内置了各种过滤器,方便我们直接使用,拦截器则使用的比较少
1.老步骤,添加maven依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.9.1</version> </dependency>
- 定义一个SpringBootAspect的类,用于AOP拦截,先定义一个切入点,再定义切面处理逻辑,这里主要定义一个控制器全局异常处理
@Aspect @Component public class SpringBootAspect { /** * 定义一个切入点 */ @Pointcut(value="execution(* com.liang.hello.controller.*.*(..))") public void aop(){} @Around("aop()") public Object around(ProceedingJoinPoint invocation) throws Throwable{ Object res = null; System.out.println("SpringBootAspect..环绕通知 Before"); try { res = invocation.proceed(); }catch (Throwable throwable){ //修改内容 System.out.println("页面执行错误,懂的都懂"); res = new DataResponse("500",null,"页面执行错误"); } System.out.println("SpringBootAspect..环绕通知 After"); return res; } }
ide提示异常,java规定,结束语句后面,不允许有代码,他们认为编译器不执行的代码是垃圾代码,呔,java语法懂的都懂,略施小计,成功的骗过了ide
- 执行结果
定时任务
- 定时任务是工作中使用非常频繁的部分,也有很多框架,但是一些简单的内置任务,使用框架就有点杀鸡用牛刀了,.net里我们通常用HostedService来实现,springboot内置了定时任务
1.创建一个ScheduledTasks类,使用注解开启异步,HelloSpringBootStarterTestApplication类也要开启哦,代码如下
@EnableAsync @Component public class ScheduledTasks { private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); /** * 任务调度,每隔1秒执行一次 */ @Async @Scheduled(fixedRate = 1000) public void reportCurrentTime() { runThreadTest(1); } public void runThreadTest(int i) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程"+Thread.currentThread().getName()+"执行异步任务"+i + "现在时间:" + dateFormat.format(new Date())); } }
- runThreadTest方法,堵塞3秒,模拟业务执行耗时,发现定开启了异步,但是它依旧是同步执行,需要等上一个任务执行完毕,才会再执行下一个任务,网上翻了下答案,需要配置线程池,代码如下
package com.liang.hello.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration public class ExecutorConfig implements AsyncConfigurer { // ThredPoolTaskExcutor的处理流程 // 当池子大小小于corePoolSize,就新建线程,并处理请求 // 当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去workQueue中取任务并处理 // 当workQueue放不下任务时,就新建线程入池,并处理请求, // 如果池子大小撑到了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理 // 当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁 //getAsyncExecutor:自定义线程池,若不重写会使用默认的线程池。 @Override @Bean public Executor getAsyncExecutor() { ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor(); //设置核心线程数 threadPool.setCorePoolSize(10); //设置最大线程数 threadPool.setMaxPoolSize(20); //线程池所使用的缓冲队列 threadPool.setQueueCapacity(10); //等待任务在关机时完成--表明等待所有线程执行完 threadPool.setWaitForTasksToCompleteOnShutdown(true); // 等待时间 (默认为0,此时立即停止),并没等待xx秒后强制停止 threadPool.setAwaitTerminationSeconds(60); // 线程名称前缀 threadPool.setThreadNamePrefix("ThreadPoolTaskExecutor-"); // 初始化线程 threadPool.initialize(); return threadPool; } }
执行结果
mybatis plus
- 目前java主流的ORM框架,应该是mybatis了,我是不怎么喜欢在xml里组织sql的,麻烦的一批,但是也避免了萌新为图方便,sql写的到处都是,维护起来懂的都懂,网上随便翻个答案,直接往项目里整合
1.老样子先添加依赖
<!--mybatis-plus的springboot支持--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.1</version> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-core</artifactId> <version>3.4.3.1</version> </dependency>
2.然后在yaml文件里添加mysql与mybatis plus的配置
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://************:3306/test_mybatis?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: root password: ****** jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 serialization: write-dates-as-timestamps: false mybatis-plus: configuration: map-underscore-to-camel-case: false auto-mapping-behavior: full log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml global-config: # 逻辑删除配置 db-config: # 删除前 logic-not-delete-value: 1 # 删除后 logic-delete-value: 0
3.再整一个mybatis plus的配置类,添加mybatis plus的拦截器,反正也是网上抄的,我猜测大致是这个意思
package com.liang.hello.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
现在开始添加创建mybatis的相关目录,网上都有,直接跟着抄就可以了,也非常好理解
4.定义一个mapper的包,在包下编写mybatis的接口,我这里用的是mybatis plus,已经默认实现了CRUD,我们简单的写几个接口,用来测试
package com.liang.hello.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.liang.hello.dto.OrderInfoResponse; import com.liang.hello.entity.UserInfo; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper //表明这是一个Mapper,也可以在启动类上加上包扫描 //Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能 public interface UserInfoMapper extends BaseMapper<UserInfo> { @Select("select u.*,o.id as orderId,o.price from user_info u left join order_info o on u.id = o.userId ${ew.customSqlSegment}") List<OrderInfoResponse> getAll(@Param(Constants.WRAPPER) Wrapper wrapper); List<UserInfo> selectByName(@Param("UserName") String userName); void updateUserInfo(@Param("UserName") String userName,@Param("Age") int age); }
-
简单的sql,mybatis plus也支持直接使用注解的方式来执行,简单方便,参数是通过queryWrapper条件构造器来完成的,喜欢的同学可以重点了解一下,.net里有linq,用过的同学懂的都懂
另外一个方式,就是通过制定mapper.xml来编写sql,xml文件路径在配置文件里制定,我们按照约定即可,在resources/mapper下,创建UserInfo.xml,名称空间指向接口路径,id对应接口的名称,返回类型指向对应的实体
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "<http://mybatis.org/dtd/mybatis-3-mapper.dtd>"> <mapper namespace="com.com.liang.hello.mapper.UserInfoMapper"> <select id="selectByName" resultType="com.liang.hello.entity.UserInfo"> select * from user_info <where> <if test="UserName != null and UserName != ''"> UserName like CONCAT('%',#{UserName},'%'); </if> </where> </select> <select id="updateUserInfo" resultType="com.liang.hello.entity.UserInfo"> update user_info set Age=#{Age} <where> <if test="UserName != null and UserName != ''"> UserName = #{UserName}; </if> </where> </select> </mapper>
mybatis xml语法可以去学习下,也不困难,简单的看一遍就差不多了,复杂的部分用到的时候再去翻阅
5.定义一个service的包,在包下创建UserInfoServiceImpl,很熟悉的味道,经典的三层架构,在service层编写业务逻辑,调用mapper接口的增删改查方法,这里重点说下事务
spring提供了事务的注解@Transactional,使用起来也非常方便,原理应该是借助AOP来实现,使用这个注解前需要事先了解事务失效的场景,老八股文了,懂的都懂,在.net里使用手动提交事务比较多,特意去了解搜了下手动提交事务,感觉差不多
//修改年龄 @Transactional public void update(UserInfo entity){ // TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); // // try { // userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()); // if(true) { // throw new Exception("xx"); // } // userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()+1); // // } catch (Exception e) { // transactionManager.rollback(txStatus); // e.printStackTrace(); // }finally { // transactionManager.commit(txStatus); // } //执行第一条sql userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()); if(true) throw new RuntimeException(); //执行第二条sql userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()+1); }
jwt
- 现在前后端分离已经成为主流,jwt是首选方案,话不多说,直接往里面怼
1.老规矩,先添加jwt的依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
2.先定义一个工具类JwtUtils,用于jwt的一些常规操作,当看到verifyToken这个方法的时候,我就发现事情没有那么简单
package com.liang.hello.common; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; public class JwtUtils { // 过期时间 24 小时 60 * 24 * 60 * 1000 private static final long EXPIRE_TIME = 60 * 60 * 1000;//60分钟 // 密钥 private static final String SECRET = "uxzc5ADbRigUDaY6pZFfWus2jZWLPH1"; private static String json=""; /** * 生成 token */ public static String createToken(String userId) { try { // 设置过期时间 Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); // 私钥和加密算法 Algorithm algorithm = Algorithm.HMAC256(SECRET); // 设置头部信息 Map<String, Object> header = new HashMap<>(2); header.put("Type", "Jwt"); header.put("alg", "HS256"); // 返回token字符串 附带userId信息 return JWT.create() .withHeader(header) .withClaim("userId", userId) //到期时间 .withExpiresAt(date) //创建一个新的JWT,并使用给定的算法进行标记 .sign(algorithm); } catch (Exception e) { return null; } } /** * 校验 token 是否正确 */ public static Map<String, Claim> verifyToken(String token){ token = token.replace("Bearer ",""); DecodedJWT jwt = null; try { JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build(); jwt = verifier.verify(token); } catch (TokenExpiredException e) { //效验失败 //这里抛出的异常是我自定义的一个异常,你也可以写成别的 throw new TokenExpiredException("token校验失败"); } return jwt.getClaims(); } /** * 获得token中的信息 */ public static String getUserId(String token) { Map<String, Claim> claims = verifyToken(token); Claim user_id_claim = claims.get("userId"); if (null == user_id_claim || StringUtils.isEmpty(user_id_claim.asString())) { return null; } return user_id_claim.asString(); } }
- 校验token正确,从token中获取信息,在.net里框架帮忙做了,使用起来非常简单,emmmmm.....我觉得spring提供一个spring-boot-starter-jwt 很有必要
.net里实现如下
//认证参数 services.AddAuthentication("Bearer") .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true,//是否验证签名,不验证的话可以篡改数据,不安全 IssuerSigningKey = signingKey,//解密的密钥 ValidateIssuer = true,//是否验证发行人,就是验证载荷中的Iss是否对应ValidIssuer参数 ValidIssuer = jwtOptions.Iss,//发行人 ValidateAudience = true,//是否验证订阅人,就是验证载荷中的Aud是否对应ValidAudience参数 ValidAudience = jwtOptions.Aud,//订阅人 ValidateLifetime = true,//是否验证过期时间,过期了就拒绝访问 ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0 RequireExpirationTime = true, }; o.Events = new JwtBearerEvents { //权限验证失败后执行 OnChallenge = context => { //终止默认的返回结果(必须有) context.HandleResponse(); var result = JsonConvert.SerializeObject(new { code = "401", message = "验证失败" }); context.Response.ContentType = "application/json"; //验证失败返回401 context.Response.StatusCode = StatusCodes.Status200OK; context.Response.WriteAsync(result); return Task.FromResult(0); } }; });
- 思路应该比较简单,弄个拦截器,校验一波jwt,完成认证,再通过jwt里的userId校验用户是否拥有访问权限,开干
3.先整一个自定义的注解AllowAnonymousAttribute,允许匿名访问,标识这个注解可以作用于类和方法上
package com.liang.hello.attribute; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AllowAnonymousAttribute { boolean required() default true; }
- 编写自定义拦截器,用于jwt的校验,校验通过,获取用户信息并授权,这里主要是获取类跟方法有没有使用自定义注解,HandlerInterceptorAdapter也提示已过期,不知道有没有替代方案
package com.liang.hello.filters; import com.auth0.jwt.interfaces.Claim; import com.liang.hello.attribute.AllowAnonymousAttribute; import com.liang.hello.common.JwtUtils; import com.liang.hello.entity.UserInfo; import com.liang.hello.service.UserInfoServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.security.SignatureException; import java.util.Map; @Component public class JwtFilter extends HandlerInterceptorAdapter { @Autowired UserInfoServiceImpl userInfoService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws SignatureException { // 如果不是映射到方法直接通过 if (!(object instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) object; AllowAnonymousAttribute actionAttribute= handlerMethod.getMethod().getDeclaredAnnotation(AllowAnonymousAttribute.class); AllowAnonymousAttribute controllerAttribute = handlerMethod.getBeanType().getDeclaredAnnotation(AllowAnonymousAttribute.class); if (actionAttribute!=null || controllerAttribute!=null) return true; //默认全部检查 System.out.println("被jwt拦截需要验证"); // 从请求头中取出 token 这里需要和前端约定好把jwt放到请求头一个叫Authorization的地方,**<font color=red size=3>懂的都懂</font>** String token = httpServletRequest.getHeader("Authorization"); // 执行认证 if (token == null) { //这里其实是登录失效,没token了 这个错误也是我自定义的,读者需要自己修改 throw new SignatureException("自定义错误"); } // 获取 token 中的 user Id String userId = JwtUtils.getUserId(token); //找找看是否有这个user 因为我们需要检查用户是否存在,读者可以自行修改逻辑 UserInfo user = userInfoService.getUserInfoById(userId); if (user == null) { //这个错误也是我自定义的 throw new SignatureException("自定义错误"); } //放入attribute以便后面调用 httpServletRequest.setAttribute("userName", user.getUserName()); httpServletRequest.setAttribute("id", user.getId()); httpServletRequest.setAttribute("age", user.getAge()); return true; } }
4.注册自定义拦截器,让spring调用这个拦截器
package com.liang.hello.config; import com.liang.hello.filters.JwtFilter; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private JwtFilter jwtFilter ; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtFilter).addPathPatterns("/**"); } }
异常处理
前面我们有使用过框架自定义的一些异常,TokenExpiredException,SignatureException,我们可以在SpringBootAspect里处理这些异常,并给出友好提示
@ExceptionHandler(value = {TokenExpiredException.class}) public DataResponse tokenExpiredException(TokenExpiredException e){ return new DataResponse("401",null,"权限不足token失效"); } @ExceptionHandler(value = {SignatureException.class}) public DataResponse authorizationException(SignatureException e){ return new DataResponse("401",null,"权限不足"); } //全局异常,兜底方案 @ExceptionHandler(value = {Exception.class}) public DataResponse exception(Exception e){ return new DataResponse("500",null,"系统错误"); }
-
未登录访问需要授权接口
-
登录,使用错误的用户名
-
登录,使用正确的用户名
-
使用token,访问需要授权接口
主动抛出异常
正常执行
-
token过期,访问需要授权接口
-
使用错误token,访问需要授权接口,因为没有主动捕获该异常,被全局异常统一处理