首先jwt其实是三个英语单词JSON Web Token的缩写。通过全名你可能就有一个基本的认知了。token一般都是用来认证的,比如我们系统中常用的用户登录token可以用来认证该用户是否登录。jwt也是经常作为一种安全的token使用。
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT主要包含三个部分之间用英语句号'.'隔开
1.Header 头部
2.Payload 负载
3.Signature 签名
类似以下这种,注意他们顺序是:Header.Payload.Signature
*************.##################.&&&&&&&&&&&&&&&&&&&&
在header中通常包含了两部分:token类型和采用的加密算法。如下:
{
"alg": "HS256",
"typ": "JWT"
}
上面的JSON内容指定了当前采用的加密方式为HS256,token的类型为jwt
将上面的内容进行base64编码,可以得到我们JWT的头部,编码后如下:
ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9
一下是我复制的一个base64在线的编码解码的一个网址工具,同志们可以自行测试学习
[link](http://tools.jb51.net/tools/base64_decode-utf8.php)
http://tools.jb51.net/tools/base64_decode-utf8.php
负载(Payload)为JWT的第二部分。JWT的标准所定义了一下几个基本字段
iss: 该JWT的签发者
sub: 该JWT所面向的用户
aud: 接收该JWT的一方
exp(expires): 什么时候过期,这里是一个Unix时间戳
iat(issued at): 在什么时候签发的
除了标准定义的字段外,我们还要定义一些我们在业务处理中需要用到的字段,例如用户token一般可以包含用户登录的token或者用户的id,一个简单的例子如下:
{
"iss": "Lefto.com",
"iat": 1500218077,
"exp": 1500218077,
"aud": "www.leftso.com",
"sub": "[email protected]",
"user_id": "dc2c4eefe2d141490b6ca612e252f92e",
"user_token": "09f7f25cdb003699cee05759e7934fb2"
}
上面的user_id、user_token都是我们自己定义的字段
现在我们需要将负载这整个部分进行base64编码,编码后结果如下:
ew0KICAgICJpc3MiOiAiTGVmdG8uY29tIiwNCiAgICAiaWF0IjogMTUwMDIxODA3NywNCiAgICAiZXhwIjogMTUwMDIxODA3NywNCiAgICAiYXVkIjogInd3dy5sZWZ0c28uY29tIiwNCiAgICAic3ViIjogImxlZnRzb0BxcS5jb20iLA0KICAgICJ1c2VyX2lkIjogImRjMmM0ZWVmZTJkMTQxNDkwYjZjYTYxMmUyNTJmOTJlIiwNCiAgICAidXNlcl90b2tlbiI6ICIwOWY3ZjI1Y2RiMDAzNjk5Y2VlMDU3NTllNzkzNGZiMiINCn0=
标准定义的字段并不需要全部用到或者可以不用,更多的时候,我们使用自定义的字段来实现我们的载荷这一部分,比如自定义一个登录用户名username,我们可以在token中直接获取登录的用户名,但不建议在头部跟载荷中使用私密信息来作为这两部分的组成成分
签名其实是对JWT的头部和负载整合的一个签名验证
首先需要将头部和负载通过.链接起来就像这样:header.Payload,上述的例子链接起来之后就是这样的:
ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9.ew0KICAgICJpc3MiOiAiTGVmdG8uY29tIiwNCiAgICAiaWF0IjogMTUwMDIxODA3NywNCiAgICAiZXhwIjogMTUwMDIxODA3NywNCiAgICAiYXVkIjogInd3dy5sZWZ0c28uY29tIiwNCiAgICAic3ViIjogImxlZnRzb0BxcS5jb20iLA0KICAgICJ1c2VyX2lkIjogImRjMmM0ZWVmZTJkMTQxNDkwYjZjYTYxMmUyNTJmOTJlIiwNCiAgICAidXNlcl90b2tlbiI6ICIwOWY3ZjI1Y2RiMDAzNjk5Y2VlMDU3NTllNzkzNGZiMiINCn0=
由于HMacSHA256加密算法需要一个key,我们这里key是保存在服务端的,不可以泄露
然后将的签名内容进行base64编码得到最终的签名
最后将三部分以点的形式拼接起来,生成最终的token,返回跟前端,以后前端每次请求后台都会在请求头带上token,具体以哪种形式带上token,可以前后端两者商量一下:以下的一种也是常用的带请求头的方式
通过上面的一个简单的说明您是否对JWT有一个简单额认知了呢?接下来我将讲解JWT在Java编程中的使用
现在基本都是SpringBoot的时代了,我就直接用SpringBoot来演示一下简单的操作
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
为了方便同志们在初始化SpringBoot或者新建Maven工程时弄错依赖,我把完成的依赖复制在下面提供参考学习,主要是方便实用,不要一不小心入坑,浪费时间
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lg</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>commons-compiler</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
SpringBoot中的yml文件基本在这个学习中不用怎么配置,看大家在项目实际中的具体应用
server:
port: 8080
可有可无,这里可以稍微学习一下注解是这么使用的,很多时候我们都不太清楚注解到底是这么使用的,这里可以有个简单的了解,以便后面学习提升用
用来跳过验证的PassToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
需要登录才能进行操作的注解UserLoginToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
以下是注解常用的几个知识点:
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention:注解的保留位置
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解
简单自定义一个实体类User,使用lombok简化实体类的编写:现在项目基本用这个来简化代码,没用过的建议百度一下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
String Id;
String username;
String password;
}
需要写token的工具类
这里需要讲解一下啊,之前我也没怎么了解过token是怎样生成的,看一下的代码,好像就只是头载荷跟签名:
这里我们可以稍微看一下源码学习一下
/**
* Date:2019/11/29
* @author:lg
*/
public class TokenUtils {
public static String getToken(User user) {
String token="";
token= JWT.create().withAudience(user.getId())
.sign(Algorithm.HMAC256(user.getPassword()));
return token;
}
}
Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
withAudience()存入需要保存在token的信息,这里我把用户ID存入token中
先写个UserService,这里不查询数据库,密码不加密,正常情况都是密码加密,然后登录的时候前端传进来的密码采取同种加密方式加密,然后再比对两个密码是否相等
/**
* Date:2019/11/29
* @author:lg
*/
@Service
public class UserServiceImpl implements UserService {
@Override
public User findUserByUserId(String userId) {
User user = new User();
user.setId("1");
user.setPassword("369369");
user.setUsername("ceshi");
return user;
}
@Override
public User findByUsername(String username) {
User user = new User();
user.setId("1");
user.setPassword("369369");
user.setUsername("ceshi");
return user;
}
}
接下来需要写一个拦截器去获取token并验证token,一般前置拦截是最常用的,他能先拦截请求完成一些校验跟功能的加强
/**
* Date:2019/11/29
*拦截器去获取token并验证token
* @author:lg
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//从请求头中取出token
String token = httpServletRequest.getHeader("token");
//如果不是直接映射到方法就直接通过
if(!(o instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) o;
Method method = handlerMethod.getMethod();
//检查方法上是否有自定义的注解,有则放行
if (method.isAnnotationPresent(PassToken.class)){
PassToken annotation = method.getAnnotation(PassToken.class);
if (annotation.required()){
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)){
UserLoginToken loginToken = method.getAnnotation(UserLoginToken.class);
if (loginToken.required()){
if (token == null){
throw new RuntimeException("无token ,请重新登录");
}
String userId = "";
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException e) {
e.printStackTrace();
}
User userByUserId = userService.findUserByUserId(userId);
if (userByUserId == null){
throw new RuntimeException("用户不存在,请重新登录");
}
//验证token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(userByUserId.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
//没有相关注解的全部放行,
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
实现一个拦截器就需要实现HandlerInterceptor接口
HandlerInterceptor接口主要定义了三个方法
1.boolean preHandle ():
预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器(主要是在这里第三个参数的使用),自定义Controller,返回值为 true表示继续流程(如调用下一个拦截器或处理器)或者接着执行
postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。
2.void postHandle():
后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
3.void afterCompletion():
整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
主要流程:
1.从 http 请求头中取出 token,
2.判断是否映射到方法
3.检查是否有passtoken注释,有则跳过认证
4.检查有没有需要用户登录的注解,有则需要取出并验证
5.认证通过则可以访问,不通过会报相关错误信息
配置拦截器
在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内
/**
* Date:2019/11/29
*配置拦截器
* @author:lg
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
//配置拦截器,拦截所有请求
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor()).
addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor(){
return new AuthenticationInterceptor();
}
@Override
public void addFormatters(FormatterRegistry formatterRegistry) {
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> list) {
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> list) {
}
@Override
public Validator getValidator() {
return null;
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer contentNegotiationConfigurer) {
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer asyncSupportConfigurer) {
}
@Override
public void configurePathMatch(PathMatchConfigurer pathMatchConfigurer) {
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> list) {
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
@Override
public void addViewControllers(ViewControllerRegistry viewControllerRegistry) {
}
@Override
public void configureViewResolvers(ViewResolverRegistry viewResolverRegistry) {
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry resourceHandlerRegistry) {
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer defaultServletHandlerConfigurer) {
}
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
}
}
InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。
这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录,没有注解的全部放行
/**
* Date:2019/11/29
* @author:lg
*/
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/**
* 登录上没有注解。拦截统一是放行的
* @param user
* @return
*/
@PostMapping("/login")
public Map login(@RequestBody User user) throws JSONException {
HashMap<String, Object> map = new HashMap<>();
User byUsername = userService.findByUsername(user.getUsername());
if (byUsername ==null){
map.put("message","登录失败,用户不存在");
return map;
}else {
if (!byUsername.getPassword().equals(user.getPassword())){
map.put("message","登录失败,密码错误");
return map;
}else{
String token = TokenUtils.getToken(byUsername);
map.put("token",token);
map.put("user",byUsername);
return map;
}
}
}
//测试拦截
@UserLoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "通过了";
}
}
不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问
下面进行测试,启动项目,使用postman测试接口