前言
随着大环境的影响,互联网寒冬降临,程序员的日子越来越难,搞不好哪天就被噶了,多学点东西也没啥坏处,国内市场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.
懂的都懂
创建一个自定义的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 >
select * from user_info
<where>
<if test="UserName != null and UserName != ''">
UserName like CONCAT('%',#{UserName},'%');
</if>
</where>
</select>
<select >
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,访问需要授权接口,因为没有主动捕获该异常,被全局异常统一处理