JWT令牌技术

目录

什么是令牌技术?

为什么要使用令牌技术?

JWT令牌

JWT令牌的生成和校验

令牌的优缺点


什么是令牌技术?

令牌技术:是一种用于身份验证授权的方法,常用于网络安全和信息技术领域。在计算机系统中,令牌是一种用于证明用户身份或授权访问的凭证,可以是基于硬件的安全令牌,也可以是基于软件的生成代码或数据。令牌技术允许用户获取一个特定的令牌后,在之后的通信中使用该令牌来证明自己的身份或获得授权。

为什么要使用令牌技术?

我们通过一个例子:用户登录 来进一步理解令牌技术的使用

在实现用户登录时,我们可以使用 cookie 和 session 来验证用户身份:

1. 用户提交用户名和密码后,登录页面将用户名、密码提交给服务器

2. 服务器验证用户名密码是否正确

3. 若验证成功,服务器创建一个 Session,并将SessionId 存储在 Cookie中,将用户身份信息存储在Session中

4. 在之后的请求中,用户的游览器都会发送包含SessionId 的 Cookie 到服务器

5. 服务器根据这个SessionId 来检索 Session 数据,并根据其中存储的身份信息来验证用户的身份

 使用 cookie 和 session 实现身份验证时,需要服务器存储用户的身份信息到会话中,

然而,此时就会存在问题:集群环境下无法使用Session

对于开发的项目,当部署在一台机器上时,容易发送单点故障(当该服务器出现故障或需要维护时,所有用户的会话数据都会丢失,导致所有用户需要重新登录),因此,在通常情况下,一个Web应用会部署在多个服务器上,通过Nginx等实现负载均衡,此时来自用户的请求就会被分配到不同的服务器上

若使用Session进行会话跟踪,可能出现一些情况:

1. 用户登录:用户登录请求,经过负载均衡,把请求转给了第一台服务器,第一台服务器进行账号密码验证,验证成功后,将Session存放在了第一台服务器上

2. 查询操作:用户登录成功之后,携带Cookie(里面有SessionId)继续查询操作,此时请求转发到了第二台机器,第二台机器会进行权限验证操作(通过SessionId验证用户是否登录),此时由于第二台机器上未存储该用户的Session,就会出现问题

为了解决上述问题,我们可以将 Session 存储在可靠的持久化存储中,如数据库或缓存服务

但此时就需要存储大量用户数据信息

为了解决上述问题,我们可以使用令牌技术:

1. 当用户提交账号和密码后,服务器进行校验

2. 校验成功,生成令牌(Token),将其返回给客户端

3. 客户端携带 Token 再次进行访问,服务器对 Token 进行校验

4. 服务器校验 Token 的真假,若为真,则提供服务

 

此时,由客户端存储令牌,减轻了服务器的存储压力,且在集群环境下也能进行认证

我们再通过一个生活中的例子进一步理解令牌技术:

令牌就类似于我们的身份证:

用户提供户口本等信息,交由公安局进行校验,公安局校验成功后,发放身份证

用户保存身份证

用户上学、出行、旅游等携带身份证

酒店等通过公安局提供的专用设备核验身份证真假,若为真,则提供服务

JWT令牌

我们可以通过令牌来解决上述问题:

令牌(Token)的本质就是一个字符串,其实现方式很多,如JWT令牌、刷新令牌、Bearer令牌等

在这里,我们学习JWT令牌

JWT令牌:JWT(JSON Web Token)是一种开放的标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 可以通过数字签名或加密来验证其可靠性,通常用于在客户端和服务器之间安全地传递身份验证信息

JWT官网:JSON Web Tokens - jwt.io

JWT由三部分组成,分别是头部(Header)载荷(Payload)签名(Signature),每个部分之间使用 . 分割

头部(Header):包括令牌类型(即JWT)和使用的哈希算法(如 HMAC、SHA256)

载荷(Payload):存放有效信息,其中是一些自定义的内容(如:{"userName": "zhangsan"}),也可以存储JWT提供的默认字段,如 exp(过期时间戳)等

JWT指定了七个默认字段:

iss(Issuer):该JWT的签发者

sub(Subject):令牌的主题,表示该JWT的主体,即该JWT所代表的用户或实体

aud(Audience):JWT的受众,该JWT的使用对象

exp(Expiration Time):JWT的过期时间,表示该JWT的有效期限,超过该时间后JWT将不再可用

nbf(Not Before):JWT的生效时间,在该时间之前,该JWT都是不可用的

iat(Issued At):JWT的签发时间

jti(JWT ID):JWT的唯一标识符,表示该JWT的唯一性,可用于防止重放攻击

签名(Signature):用于验证消息的完整性和可靠性,防止JWT内容被篡改,确保安全性

签名用于验证消息的完整性和可靠性,JWT中任何一个字符被篡改,整个令牌都会校验失败

但其不确保其保密性,即不能防止被解析,也就是任何人都可以看到JWT中的信息(就如身份证,任何人都可以看到身份证上的信息,但不能篡改身份证上的信息)

因此 JWT令牌不适合存放敏感信息(如用户密码等)

当没有签名时,我们仍然能够看到JWT中的信息,即令牌不保证保密性

在了解了JWT令牌后,我们来学习JWT令牌的生成和校验 

JWT令牌的生成和校验

首先,我们需要引入 JWT 令牌的依赖

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.5</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>

接着,我们就可以使用Jar包中提供的API来完成JWT令牌的生成和校验了

我们首先来看生成令牌:

Header

JWT的头部包含两部分信息:

1. 令牌类型(alg):JWT

2. 加密算法(typ):可选择 HS256、HS384、HS512、RS256...

        //头部
        Map<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");
        //生成令牌
        String token = Jwts.builder()
                .setHeader(header) // 可以不设定,此时使用默认的

Payload:

Paylod通常是一个JSON对象,包含了JWT的所有声明(claims)以及其他需要传递的信息

载荷包含两种类型数据:

1. 自定义数据

2. 标准中注册的声明数据

        long JWT_EXPIRATION = 60 * 60 * 1000;
// 自定义信息
        Map<String, Object> claim = new HashMap<>();
        claim.put("id", 1);
        claim.put("userName", "zhangsan");
        //生成令牌
        String token = Jwts.builder()
                .setClaims(claim) // 自定义信息
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) // 过期时间

Signature:

签名内容需要通过计算得出:

base64UrlEncode(header) + "." + base64UrlEncode(payload) +  "." + your-256-bit-secret

将待签名消息进行编码:

通常采用Base64编码将 header 和 payload转换为字符串

选择密钥:

我们以 HS256(HMAC with SHA-256)加密算法为例,我们需要选择一个密钥(密钥是进行签名计算的关键)

使用加密算法计算签名:

然后通过 header 中声明的加密算法 进行签名计算(仍以 HS256为例),将 编码后的消息 和 密钥输入 HMAC 算法中,进行 SHA-256哈希处理,生成签名

将签名添加到JWT中:

将生成的签名添加到JWT的尾部,形成最终的JWT

 我们首先生成随机密钥:

     @Test
    //生成随机密钥
    public void genKey() {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        String secretStr = Encoders.BASE64.encode(secretKey.getEncoded());
        System.out.println(secretStr);
    }

 运行,得到密钥:

以运行结果作为随机密钥:


        String secretStr = "IufNeZtdSwoMADOxF3yofGqrpjAhNx58C/bomQ27NKg=";
        Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));
        
        // 生成令牌
        String token = Jwts.builder()
                .setHeader(header) // 可以不设定,此时使用默认的
                .setClaims(claim) // 自定义信息
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) // 过期时间
                .signWith(key)
                .compact();

 生成令牌:

    private static final long JWT_EXPIRATION = 60 * 60 * 1000;
    @Test
    public void genJwt() {
        //头部
        Map<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");
        // 自定义信息
        Map<String, Object> claim = new HashMap<>();
        claim.put("id", 1);
        claim.put("userName", "zhangsan");
        // 生成密钥
        String secretStr = "IufNeZtdSwoMADOxF3yofGqrpjAhNx58C/bomQ27NKg=";
        Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));
        // 生成令牌
        String token = Jwts.builder()
                .setHeader(header) // 可以不设定,此时使用默认的
                .setClaims(claim) // 自定义信息
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) // 过期时间
                .signWith(key) // 签名
                .compact();
        System.out.println(token);
    }

运行,查看得到的令牌:

我们将得到的令牌复制到官网上解析:

将密钥复制到  your-256-bit-secret 位置

 校验通过

接下来,我们实现令牌的校验:

我们需要使用之前生成的随机密钥来进行解密:

    private static final String secretStr = "IufNeZtdSwoMADOxF3yofGqrpjAhNx58C/bomQ27NKg=";
    private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));

    @Test
    public void parseToken() {
        // 令牌
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlck5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTcxNzgyMDA3MywiZXhwIjoxNzE3ODIzNjczfQ.MKqOIgYTmmJMPktpbjsWq4WDmkuceCQPq2tJQoJVSpE";
        // 解析令牌
        JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
        Claims claims = build.parseClaimsJws(token).getBody();
        System.out.println(claims);
    }

运行,我们可以看到解析的消息

令牌的优缺点

优点:

1. 解决了集群环境下的认证问题

2. 减轻了服务器的存储压力,即无需在服务器端存储

3. 令牌可以通过加密或签名等方式进行安全传输和存储,减少了被盗用的风险,更安全

缺点:

1. 相对于传统的基于会话的认证机制,令牌认证通常需要更多的技术和知识,包括令牌签发、验证、刷新等流程,因此实现和维护的成本较高

2. 如果令牌泄露,可能会导致安全风险,因此需要进行令牌的安全存储和传输

  • 72
    点赞
  • 81
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 46
    评论
好的,这个问题属于技术问题,关于Java结合SpringBoot给登录写个用户名和密码JWT令牌验证,你可以参考以下步骤: 1. 在SpringBoot项目的pom.xml文件中添加JWT依赖: ``` <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> ``` 2. 编写一个jwt工具类,该类用于生成和解析JWT令牌: ``` public class JwtUtils { private static final long EXPIRE_TIME = 30 * 60 * 1000; // 过期时间为30分钟 private static final String SECRET_KEY = "your_secret_key"; // 自定义秘钥,用于签名和解密 public static String generateToken(String username) { Date now = new Date(); Date expireTime = new Date(now.getTime() + EXPIRE_TIME); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expireTime) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public static boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } } ``` 3. 编写一个登录接口,该接口接收用户名和密码参数,并返回生成的JWT令牌: ``` @RestController public class LoginController { @PostMapping("/login") public RespBean login(@RequestParam("username") String username, @RequestParam("password") String password) { // TODO: 在此处校验用户名和密码是否正确 String token = JwtUtils.generateToken(username); return RespBean.success(token); } } ``` 4. 在需要验证访问权限的接口中,使用Spring Security和JWT进行验证: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager(), userDetailsService)); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { String username = request.getParameter("username"); String password = request.getParameter("password"); Authentication authentication = new UsernamePasswordAuthenticationToken( username, password, Collections.emptyList()); return getAuthenticationManager().authenticate(authentication); } catch (Exception e) { throw new RuntimeException(e); } } } public class JwtAuthorizationFilter extends BasicAuthenticationFilter { private final UserDetailsService userDetailsService; public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) { super(authenticationManager); this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } String token = header.replace("Bearer ", ""); if (JwtUtils.validateToken(token)) { String username = JwtUtils.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); } } ``` 我希望上述内容对你有所帮助,如果需要了解更详细的内容,可以自行查询相关资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 46
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楠枬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值