JavaWeb前后端分离网站从0开发(四)用户登录功能(3)JWT验证

本项目所将主要用到技术点:
前端:Vue2、ElementUI。
后端:Spring Boot、Spring Security、MySQL、MyBatis-Plus、Docker

为了更一步完善登录功能,这里再加入JWT验证功能,即JSON Web Token,以token令牌的形式来进行用户的验证、管理用户会话。并且使用redis来进行缓存,以提高性能和安全性。

添加JWT功能

1、在pom.xml中添加jjwt依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<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 you prefer -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

2、添加JWT工具类

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration; // token 过期时间,单位秒

    // 生成 JWT
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // 验证 JWT
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    // 获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        return claims.getSubject();
    }
}

接下来配置这个工具类中的 ${jwt.secret} 和 ${jwt.expiration} 。
${jwt.secret}是用于生成token的密钥。
${jwt.expiration}是用于生成token时为它设置有效时长。

两种秘钥管理方式:

1、固定写在配置文件中。

	这种方式使用方便,便于管理。
	但缺点是有安全风险,因为是固定写死在文件中的,有很大的泄露风险。

2、动态生成。

	即在项目启动或者运行时,动态地生成秘钥。每次重新生成后,以前的所有token都将失效,这样安全性大大提高。
	但带来的缺点是:持久化和复杂化问题。
	因为这样频繁地改动秘钥,导致token频繁失效,势必会影响系统的使用体验(持久化问题)。
	而为了解决这个问题,需要额外的一些机制来优化这种问题,而这势必导致系统变得更加复杂。

本项目是练习项目,仅使用第一种,在配置文件固定秘钥的方式。
直接在测试类中临时生成一个可用的秘钥:
在这里插入图片描述
生成后,将其配置到配置类中:
在这里插入图片描述

3、创建JWT过滤器类

用于在请求中解析、验证JWT

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");

        if(token != null){
            // 去除自动添加的 "Bearer " 前缀,并去除首尾空格
            if (token.startsWith("Bearer ")) {
                token = token.substring(7).trim();
            }

            if (jwtUtil.validateToken(token)) {
                String username = jwtUtil.getUsernameFromToken(token);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }
    
}

4、修改spring security配置类

在securityFilterChain方法中添加:

        http.sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置无状态 Session
        );

        // 在 UsernamePasswordAuthenticationFilter 之前添加 JWT 过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

5、取消默认登录接口调用,重写登录功能

首先,注释掉原本的formLogin部分配置:
然后,重写一个自定义的登录接口:

@RestController
@RequestMapping("")
public class LoginController {
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 用于生成Token的jwt工具类
     */
    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/myLogin")
    public String myLogin(@RequestParam String username, @RequestParam String password, HttpServletResponse response) {
        try {
            // 尝试认证
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password));

            // 登录成功,生成 JWT Token
            String token = jwtUtil.generateToken(username);
            
            // 返回 Token
            return "登录成功, Token: " + token;

        } catch (AuthenticationException ex) {
            // 登录失败,返回错误信息
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return "登录失败: " + ex.getMessage();
        }
    }
}

6、测试JWT功能

此时进行一下测试,看是否通过了自定义的登录接口。

6.1、调用登录接口

在这里插入图片描述
登录成功,得到用于验证的token。

6.2、调用其他接口

在这里插入图片描述

从上图可以看到,此时虽然登录成功,但访问其他接口还是会提示无权限,“Full authentication is required to access this resource”。这是因为:在spring security的配置中,加的这一段:

http.sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置无状态 Session
);

这设置了无状态 (STATELESS) 的 Session 管理方式,禁用了服务端的会话存储。它要求任何请求都必须携带JWT Token或其他认证信息来验证用户的身份,否则服务器会无法识别身份。
而在加上token之后,就能通过验证,成功调用接口:
那么接下来继续完善,将每次请求都加上token验证。

8、添加redis功能

现在已经可以顺利使用JWT进行登录和请求验证了。接下来引入redis功能,我们把登录时获取的token放入redis中进行管理。
其实在引入redis之前,系统已可以完成完整的登录流程。
那么,既然JWT的token本身已经具有时效性和验证功能了,那为什么还要使用redis功能呢?
这主要还是因为仅仅使用JWT不够灵活、安全。
在结合了redis这类存储工具后,就可以自定义更多灵活、完善的功能:如即时控制Token的有效性、Token续期/刷新、多客户端同步控制登录状态等,此时它才是足够强大的、能够满足用户需求的。

8.1、添加Redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

8.2、本地安装并配置Redis

安装并启动Redis后(请自行搜索方法),配置application-dev.yml文件

spring:  
  data:
    #redis配置
    redis:
      host: localhost
      port: 6379
      password:
      database: 0

8.3、增加Redis验证

创建Redis配置类
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 添加序列化设置(可选)
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}
创建Redis工具类
@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 写入缓存
     *
     * @param key   键
     * @param value 值
     */
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 写入缓存并设置过期时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public void set(String key, Object value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    /**
     * 读取缓存
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 删除缓存
     *
     * @param key 键
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 判断缓存中是否有对应的key
     *
     * @param key 键
     * @return true 存在 false 不存在
     */
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
}
修改JwtUtil类

在JwtUtil类中引入Redis工具类:

    /**
     * Redis工具类
     */
    @Autowired
    private RedisUtil redisUtil;

生成Token时存储到Redis:

        // 存储到 Redis 中,设置过期时间
        redisUtil.set("token:"+ username,token,expiration);

修改验证Token的方法:

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);

            // 检查 Redis 中是否存在该 Token
            return redisUtil.hasKey("token:" + getUsernameFromToken(token));
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

修改后的JwtUtil类为:

/**
 * jwt工具类
 */
@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    /**
     * token 过期时间,单位秒
     */
    @Value("${jwt.expiration}")
    private long expiration;

    /**
     * Redis工具类
     */
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 生成 token,并存入redis
     */
    public String generateToken(String username) {
        String token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();

        // 存储到 Redis 中,设置过期时间
        redisUtil.set("token:"+ username,token,expiration);
        return token;
    }

    /**
     * 验证 token
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);

            // 检查 Redis 中是否存在该 Token
            return redisUtil.hasKey("token:" + getUsernameFromToken(token));
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    /**
     * 获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        return claims.getSubject();
    }

}

9、最终测试

以上工作完成后,现在开始测试:

登录

在这里插入图片描述

成功登录,产生token。

查看redis

在这里插入图片描述

生成的token正常地存入了redis

调用其他接口

不带token时,调用失败:
在这里插入图片描述
带上token后,调用成功:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值