分布式环境认证和授权-基于springboot+JWT+拦截器实现-实操+源码下载

1、功能概述?

1、当用户登录的时候,将用户的信息通过JWT进行加密和签名,并将JWT产生了token信息保存到当前浏览器的localStoragee中,即本地存储中。

2、当用户登录成功后,访问其他资源的时候,程序从localStorage中获取token的信息,并将token的信息存入到请求头中。

3、拦截器拦截当前的请求,并从请求中获取到token的值,使用JWT进行验证,如果验证表示当前用户合法就放行,如果验证不通过表示不合法,不放行。

2、JWT概述

1、JSON WebToken(JWT)是一种紧凑、自包含方式的、遵循RFC 7519开放标准,是一种协议。

2、JWT中的声明(如何用户名/编号等信息)被编译成JSON对象,并且这些信息会经过数字签名,信息可以进行验证和信任。

3、JWT支持以下签名和验证算法: HMAC、RSA 或 ECDSA。

4、JSON WebToken是常用的跨域身份验证方案。

5、JWT生成的token内容可以被解析,因为采用的是base64算法,但是使用了签名所以数据不能被篡改,敏感信息不能放入其中如密码,放置信息泄露。

2.1、JWT优点

1、以json行书传输,数据量小,传输速度快且JWT是跨语言的。

2、适用于分布式和微服务架构,因为信息的保存不依赖于session或者cookie.

3、使用cookie不适合移动端、但是JWT既不依赖session也不依赖cookie,单点登录实现容易。

4、因此无论是单体结构还是分布式都可以使用JWT进行身份认证。

2.2、JWT的组成结构

我们可以使用官网:https://github.com/auth0/java-jwt,学习JWT相关的结构

JWT主要有三部分组成:标头(Header)+有效载荷(Payload)+签名(Signature)

格式通常为:Header.Payload.Signature

【第一部分:Header

由令牌类型和签名算法组成

【第二部分:Payload

有效负载,通常定义用户自定义信息,这部分构成JWT的第二部分数据,是推荐使用的,而非强制。主要包括

iss:发行人或签发者

exp:到期时间

sub:主题

aud:用户/jwt接收方

nbf:在此之前不可用

iat:发布时间/签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

【第三部分:Signature

由于Header和Payload都是使用Base64进行编码,是可逆的,因此信息可以被解析出来,为了放置信息被篡改,加入了签名。同时如果提供的签名不正确,jwt生成的token不能被正确解析。

通过jwt.io 这个网站观看jwt 生成的token 样式

2.3、JWT使用的大体流程

1、在登录验证通过后,给用户生成一个对应的随机token(注意这个token不是指jwt,可以用uuid等算法生成),然后将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间;

将第1步中生成的随机token作为JWT的payload生成JWT字符串返回给前端;

前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串;

后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录。

3、使用JWT实现签发token/校验token/获取token信息

3.1、项目结构

3.2、创建springboot工程映入jwt的包信息

我们使用java-jwt实现

XML 复制代码
<?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>3.1.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.txc</groupId>
    <artifactId>distribute-session-jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>distribute-session-jwt</name>
    <description>distribute-session-jwt</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <builder>paketobuildpacks/builder-jammy-base:latest</builder>
                    </image>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.3、创建login测试类签发token

注意:理论上,签名可以通过穷举法进行破除,所以一般签名不要使用的过于简单或者定期更换签名,增强系统的安全性。

程序中通过jwt.create进行签发,并设置了签名信息,过期时间,及相关的用户信息。

java 复制代码
@RestController
public class JWTController {
    @RequestMapping("/login")
public String login(@RequestParam String username, HttpServletResponse response, HttpServletRequest request){
    //创建map存放用户信息
    Map<String,Object> map=new HashMap<>();
    map.put("userid","1001");
    map.put("username",username);
    String token=null;
    try {
        //!@#$%^&123~:是我们使用的签名
        Algorithm algorithm = Algorithm.HMAC256("!@#$%^&123~");
        token = JWT.create()
                .withIssuer("auth0")//角色权限
                .withClaim("userinfo",map)
                //设置token的过期时间为一个小时
                .withExpiresAt(new Date(System.currentTimeMillis()+3600000))
                .sign(algorithm);
        //将token信息添加到token中
    } catch (JWTCreationException exception){
        System.out.println("=====程序异常=======");
    }
    return token;
}
}

3.4、访问login后结果如下

3.5、解析之后效果如下

将浏览器生成的token的信息,拷贝到JSON Web Tokens - jwt.io地址中解析如下:

3.6、创建getLoginToken实现校验和获取token信息

此处为了测试方便,我们是直接手动的将token的值拷贝过来,放在地址栏中传递到该方法,实际的项目中,会将token的信息存放到请求的header中,用户通过header获取。

由于JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全

请求地址:localhost:8080/getLoginToken?**token=**eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCIsInVzZXJpbmZvIjp7InVzZXJpZCI6MTAwMSwidXNlcm5hbWUiOiJ4aWFvY2h1biJ9LCJleHAiOjE3MDE4ODMwNjd9.ceGcf1Q_TG26HFCBf20E0TIoSXlJYK7gudAvfKOjUlw

说明1 **:**校验的时候签名千万不能写错了,否则会提示校验失败。

说明2 **:**如果抛出异常说明校验失败

说明3 **:**注意获取值的返回值结果

java 复制代码
@RequestMapping("/getLoginToken")
public String getLoginToken(@RequestParam String token){
    //验证一个token
    try{
        Algorithm algorithm=Algorithm.HMAC256("!@#$%^&123~");
        JWTVerifier verifier=JWT.require(algorithm)
                .withIssuer("auth0")
                .build();
        //jwt通过verify进行校验
        DecodedJWT jwt=verifier.verify(token);
        //如果校验成功就获取userinfo的信息
        Map<String,Object> userinfo=jwt.getClaim("userinfo").asMap();
        //获取userinfo中的username值并转化成string
        String username=userinfo.get("username").toString();
        int userid=Integer.valueOf(userinfo.get("userid").toString());
        //获取token信息的有效期
        Date exp=jwt.getExpiresAt();
        return username+"==="+userid+"==="+exp;
    }catch(Exception e){
        e.printStackTrace();
        return "校验失败";
    }
}

返回结果

4、拦截器统一处理token

案例实现当用户登录成功后,将当前token信息保存到浏览器本地存储位置。当再次在项目中发起请求的时候,从本地不去到token,将token信息保存到请求头中,拦截器获取到请求头中的token信息,如果验证成功就允许访问资源,如果验证失败重定向到登录页中。

4.1、在springboot中创建拦截器

1、下面的拦截器主要实现,如果没有从请求头中获取到token就抛出异常,程序不放行。如果获取了token的值,但是验证没有通过不放行。

2、DecodedJWT jwt=verifier.verify(token);就是验证方法,如果验证不通过会抛出异常。
3 、hasText 判断字符串是否为null 用法:

https://blog.csdn.net/tangshiyilang/article/details/134926299

java 复制代码
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(request.getMethod()+"============拦截器执行===============");

       String token=request.getHeader("token");
       if(!StringUtils.hasText(token)){
           throw new TokenIsNullException();
       }
        try{
            //验证一个token
            Algorithm algorithm=Algorithm.HMAC256("!@#$%^&123~");
            JWTVerifier verifier= JWT.require(algorithm)
                    .withIssuer("auth0")
                    .build();
            DecodedJWT jwt=verifier.verify(token);
            String username=jwt.getClaim("username").asString();
            request.setAttribute("username",username);
        }catch(Exception e){
            return false;
        }
        return true;
    }
}

4.2、在工程中加载拦截器

实现WebMvcConfigurer接口,重写addInterceptors方法。

addInterceptor:表示需要加载的拦截器

addPathPatterns:需要拦截的地址

excludePathPatterns:不需要拦截的地址,如果拦截login地址会出现死循环的情况。

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test.do")
                .excludePathPatterns("/login");
    }

}

4.3、创建自定义异常类

java 复制代码
public class TokenIsNullException extends RuntimeException{

    public TokenIsNullException(){
        super("token为空!");
    }
}

4.4、创建登录使用的接口login1

说明1:为了便于测试,之前的login方法我们保留了下来,单独又写了一个login1用于当前的测试。

java 复制代码
@RequestMapping("/login1")
@ResponseBody
public String login1(@RequestParam String username, HttpServletResponse response, HttpServletRequest request){
    System.out.println("=======login==========="+username);
    //创建map存放用户信息
    Map<String,Object> map=new HashMap<>();
    map.put("userid",1001);
    map.put("username",username);
    String token=null;
    try {
        //!@#$%^&123~:是我们使用的签名
        Algorithm algorithm = Algorithm.HMAC256("!@#$%^&123~");
        token = JWT.create()
                .withIssuer("auth0")//角色权限
                .withClaim("userinfo",map)
                //设置token的过期时间为一个小时
                .withExpiresAt(new Date(System.currentTimeMillis()+3600000))
                .sign(algorithm);
    } catch (JWTCreationException exception){
        System.out.println("=====程序异常=======");
        return "0";//如果返回值为0,表示失败。
    }
    return "1";//如果返回值为1,表示成功。
}

4.5、创建test.do

这个方法主要用于模拟登录成功后,我们需要访问的资源,拦截器会获取请求中的token,如果验证成功允许访问,否则不允许访问。

java 复制代码
@RequestMapping("/test.do")
@ResponseBody
public String test(){
    System.out.println("=====test.do========");
    return "恭喜你资源访问成功。";
}

4.6、创建登录页实现登录

html 复制代码
说明1:localStorage.setItem("token",msg2);使用jquery向本地存储数据
说明2:window.location.href="show.html";//登录成功后,重定向到show.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery-2.1.1.js"></script>
    <script type="text/javascript">
        function login(){
            $.ajax({
                type:"get",
                url:"/login",
                cache:false,
                dataType:"text",
                data: {"username":"xiaochun"},
                success:function(msg2,request){
                    alert("==msg2==="+msg2);
                    //存储字段
                    localStorage.setItem("token",msg2);
                    //重定向到主页
                    window.location.href="show.html";
                }
            });
        }
    </script>
</head>
<body>
      <input type="text" name="username"> <br>
      <button onclick="login()">登录</button>
</body>
</html>

登录页样式:

4.7、创建show.html页面

如果登录成功后,页面会重定向到show.html中。这个时候点击a标签,触发getUserToken 函数访问test.do 资源,在访问资源的同时会从本地获取token 信息,放入到请求头中。这个时候请求会被拦截器拦截,拦截器获取请求头中的token 信息,如果验证成功允许访问test.do, 如果验证失败拒绝访问。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
  <script src="js/jquery-2.1.1.js"></script>
  <script type="text/javascript">
    function getUserToken(){
      //获取保存在localStore中的数据
      var mytoken=localStorage.getItem("token");
      alert("===token==="+token);
      $.ajax({
        type:"post",
        url:"/test.do",
        headers: {'token':mytoken},
        success:function(msg2){
          //存储字段
          localStorage.setItem("token",msg2);
          //重定向到主页
          window.location.href="show.html";
        }
      });
    }
  </script>
</head>
<body>
   我是主页 <br>
   <a href=" javaScript:void(0)"  onclick="getUserToken()">发起请求访问test.do资源</a>
</body>
</html>

4.8、登录成功后的信息

登录成功后重定向到show.html.

4.9、通过show.html访问test.do资源

当我们点击"发起请求访问test.do资源",经过拦截器校验token成功,返回成功访问,且从请求头中可以看到token的信息。

4.10、手动清除token信息,再次访问

从下面的地址可以看出,test.do没有成功访问,拦截器没有放行。

相关推荐
程序员 小柴2 小时前
RabbitMQ的工作模式
分布式·rabbitmq·ruby
蒋星熠2 小时前
在VMware下Hadoop分布式集群环境的配置--基于Yarn模式的一个Master节点、两个Slaver(Worker)节点的配置
大数据·linux·hadoop·分布式·ubuntu·docker
小样vvv3 小时前
【分布式】微服务系统中基于 Hystrix 的熔断实现方案
分布式·hystrix·微服务
清风19816 小时前
kafka消息可靠性传输语义
数据库·分布式·kafka
小诸葛的博客6 小时前
Kafka、RocketMQ、Pulsar对比
分布式·kafka·rocketmq
数据智能老司机8 小时前
CockroachDB权威指南——SQL调优
数据库·分布式·架构
数据智能老司机8 小时前
CockroachDB权威指南——应用设计与实现
数据库·分布式·架构
数据智能老司机9 小时前
CockroachDB权威指南——CockroachDB 模式设计
数据库·分布式·架构
数据智能老司机1 天前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构