SpringBoot集成JWT 实现token认证

1. 什么是JWT?了解JWT,认知JWT?

首先jwt其实是三个英语单词JSON Web Token的缩写。通过全名你可能就有一个基本的认知了。token一般都是用来认证的,比如我们系统中常用的用户登录token可以用来认证该用户是否登录。jwt也是经常作为一种安全的token使用。

1.1 JWT的定义:

JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

1.2 JWT请求流程

SpringBoot集成JWT 实现token认证_第1张图片

  1. 用户使用账号和密码发出post请求,实现登录操作;
  2. 服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器。

2. JWT构成或者说JWT是什么样的?

2.1 JWT结构

JWT主要包含三个部分之间用英语句号'.'隔开
1.Header 头部
2.Payload 负载
3.Signature 签名
类似以下这种,注意他们顺序是:Header.Payload.Signature
*************.##################.&&&&&&&&&&&&&&&&&&&&

2.2.JWT的头部(Header)

在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

2.3.JWT的负载(Payload)

负载(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中直接获取登录的用户名,但不建议在头部跟载荷中使用私密信息来作为这两部分的组成成分

2.4.Signature(签名)

签名其实是对JWT的头部和负载整合的一个签名验证
首先需要将头部和负载通过.链接起来就像这样:header.Payload,上述的例子链接起来之后就是这样的:

ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9.ew0KICAgICJpc3MiOiAiTGVmdG8uY29tIiwNCiAgICAiaWF0IjogMTUwMDIxODA3NywNCiAgICAiZXhwIjogMTUwMDIxODA3NywNCiAgICAiYXVkIjogInd3dy5sZWZ0c28uY29tIiwNCiAgICAic3ViIjogImxlZnRzb0BxcS5jb20iLA0KICAgICJ1c2VyX2lkIjogImRjMmM0ZWVmZTJkMTQxNDkwYjZjYTYxMmUyNTJmOTJlIiwNCiAgICAidXNlcl90b2tlbiI6ICIwOWY3ZjI1Y2RiMDAzNjk5Y2VlMDU3NTllNzkzNGZiMiINCn0=

由于HMacSHA256加密算法需要一个key,我们这里key是保存在服务端的,不可以泄露
然后将的签名内容进行base64编码得到最终的签名
最后将三部分以点的形式拼接起来,生成最终的token,返回跟前端,以后前端每次请求后台都会在请求头带上token,具体以哪种形式带上token,可以前后端两者商量一下:以下的一种也是常用的带请求头的方式
SpringBoot集成JWT 实现token认证_第2张图片
通过上面的一个简单的说明您是否对JWT有一个简单额认知了呢?接下来我将讲解JWT在Java编程中的使用

3.废话不多说 代码上见

3.1SpringBoot集成JWT

现在基本都是SpringBoot的时代了,我就直接用SpringBoot来演示一下简单的操作

3.1.1引入JWT依赖

<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

3.1.2自定义两个注解

可有可无,这里可以稍微学习一下注解是这么使用的,很多时候我们都不太清楚注解到底是这么使用的,这里可以有个简单的了解,以便后面学习提升用
用来跳过验证的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是怎样生成的,看一下的代码,好像就只是头载荷跟签名:
SpringBoot集成JWT 实现token认证_第3张图片
这里我们可以稍微看一下源码学习一下
SpringBoot集成JWT 实现token认证_第4张图片

/**
 * 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测试接口
SpringBoot集成JWT 实现token认证_第5张图片
SpringBoot集成JWT 实现token认证_第6张图片
SpringBoot集成JWT 实现token认证_第7张图片

4.顺便解释一个校验过程,肯定也有相同疑惑的人

SpringBoot集成JWT 实现token认证_第8张图片

你可能感兴趣的:(java,SpringBoot,JWT,java,token,SpringBoot,JWT)