JWT介绍和结合springboot项目实践(登录、注销授权认证管理)

发布于:2024-12-07 ⋅ 阅读:(177) ⋅ 点赞:(0)

一、JWT介绍

(一)基本介绍

JSON Web Token 是一种开放标准(RFC 7519),用于在各方之间以 JSON 对象的形式安全地传输信息。它可以在无状态、分布式的应用环境中实现身份验证和授权等功能,通常由三部分组成,分别是头部(Header)、载荷(Payload)和签名(Signature),格式为 Header.Payload.Signature。在应用中配置 JWT 相关内容,主要涉及设置私钥(用于签名生成)等关键信息,以便后续能正确地创建、验证 JWT

(二)jwt有哪些库

1、jjwt(Java JWT)

简介:这是一个非常流行的 Java 库,用于处理 JWT。它提供了简洁的 API 来生成和解析 JWT。它支持多种 JWT 签名算法,如 HS256、RS256 等,并且可以方便地在 Java 项目(特别是 Spring Boot 项目)中进行集成。
需要引入的依赖

<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>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

示例代码:
生成 JWT:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtExample {
    private static final String SECRET_KEY = "your - secret - key";

    public static String generateJwt(String subject, long expirationMillis) {
        Map<String, Object> claims = new HashMap<>();
        Date now = new Date();
        Date expiration = new Date(now.getTime() + expirationMillis);

        return Jwts.builder()
               .setClaims(claims)
               .setSubject(subject)
               .setIssuedAt(now)
               .setExpiration(expiration)
               .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
               .compact();
    }

    public static Claims parseJwt(String jwt) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(jwt).getBody();
    }
}

2、nimbus - jwt - jwt - api 和 nimbus - jwt - jwt - impl

简介:Nimbus JOSE + JWT 是一个功能强大的库,用于处理 JWT 以及其他 JOSE(JSON Object Signing and Encryption)结构。nimbus - jwt - jwt - api提供了接口定义,nimbus - jwt - jwt - impl是其实现部分。它支持高级的 JWT 功能,如加密的 JWT(JWE),并且在安全和标准遵循方面表现出色。
需要引入的依赖

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus - jwt - jwt - api</artifactId>
    <version>9.22</version>
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus - jwt - jwt - impl</artifactId>
    <version>9.22</version>
</dependency>

示例代码:
生成 JWT:

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.text.ParseException;
import java.util.Date;

public class NimbusJwtExample {
    private static final String SECRET_KEY = "your - secret - key";

    public static String generateJwt(String subject, long expirationMillis) throws JOSEException {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + expirationMillis);
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
               .subject(subject)
               .issueTime(now)
               .expirationTime(expiration)
               .build();
        JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
        SignedJWT signedJWT = new SignedJWT(header, claimsSet);
        JWSSigner signer = new MACSigner(SECRET_KEY);
        signedJWT.sign(signer);
        return signedJWT.serialize();
    }

    public static void parseJwt(String jwt) throws ParseException, JOSEException {
        SignedJWT signedJWT = SignedJWT.parse(jwt);
        if (signedJWT.verify(new MACSigner(SECRET_KEY))) {
            JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
            System.out.println("Subject: " + claims.getSubject());
            System.out.println("Expiration Time: " + claims.getExpirationTime());
        }
    }
}

3、spring - security - jwt(已弃用,但在旧项目中有参考价值)

简介:这是 Spring Security 官方之前提供的用于 JWT 集成的库,不过现在已经弃用。它在早期的 Spring Boot 项目与 JWT 集成中发挥了作用,并且提供了基于 Spring Security 框架的 JWT 认证机制。
需要引入的依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring - security - jwt</artifactId>
    <version>1.1.0.RELEASE</version>
</dependency>

示例(简单示意其曾经的使用方式):
在配置类中(部分代码):

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordCipher;
import org.springframework.security.crypto.password.PasswordCipher;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.web.filter.OncePerRequestFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Bean
    public PasswordCipher passwordCipher() {
        return new BCryptPasswordCipher();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeRequests()
               .antMatchers("/api/public/**").permitAll()
               .anyRequest().authenticated()
               .and()
               .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
               .and()
               .addFilterBefore(new JwtAuthenticationFilter(userDetailsService), UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordCipher(passwordCipher());
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

这个库中的JwtAuthenticationFilter等组件(这里只是示意代码)可以帮助实现 JWT 的验证和基于 JWT 的用户认证流程,不过在新项目中一般推荐使用jjwt等更现代的库与 Spring Boot 的安全配置相结合来实现 JWT 认证。

二、结合springboot项目运用

以下示例使用 Java 语言,结合 jjwt 库(一个常用的用于处理 JWT 的库)来展示如何基于配置的私钥进行 JWT 的生成和验证操作。

(一)整合JWT

1、添加依赖

如果使用 Maven 构建项目,需要在pom.xml文件中添加jjwt相关依赖。jjwt是一个用于处理 JWT 的 Java 库,提供了生成和解析 JWT 的功能。

<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>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

2、配置JWT相关属性

可以在application.yml或application.properties文件中配置 JWT 相关的属性,如密钥、过期时间等。例如:

jwt:
  secret: your - secret - key - here
  expiration: 86400000 # 过期时间,单位为毫秒,这里设置为一天

3、创建JWT工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtils {

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

    @Value("${jwt.expiration}")
    private long expiration;

    // 生成JWT
    public String generateToken(String subject) {
        Map<String, Object> claims = new HashMap<>();
        return Jwts.builder()
              .setClaims(claims)
              .setSubject(subject)
              .setIssuedAt(new Date())
              .setExpiration(new Date(System.currentTimeMillis() + expiration))
              .signWith(SignatureAlgorithm.HS256, secret)
              .compact();
    }

    // 从JWT中获取主题(通常是用户信息)
    public String getSubjectFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        return claims.getSubject();
    }

    // 验证JWT是否有效
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
在这个工具类中:
通过@Value注解从配置文件中获取 JWT 的密钥和过期时间。
generateToken方法用于生成 JWT,它创建一个包含声明(Claims)的JWT,设置主题(通常是用户标识等信息)、签发时间和过期时间,并使用配置的密钥和HS256签名算法进行签名。
getSubjectFromToken方法用于从 JWT 中获取主题,通过解析 JWT 并获取Claims对象中的主题信息。
validateToken方法用于验证 JWT 的有效性,尝试解析 JWT,如果没有抛出异常则表示 JWT 有效。

4、创建认证过滤器(用于保护资源)

创建一个过滤器来拦截请求并验证 JWT。例如,在 Spring Boot 的 Web 应用中可以创建一个JwtAuthenticationFilter:

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = getTokenFromRequest(request);
        if (token!= null && jwtUtils.validateToken(token)) {
            String username = jwtUtils.getSubjectFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserDetails(username);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
                    userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken!= null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
在这个过滤器中:
首先通过getTokenFromRequest方法从请求头中获取 JWT(通常在Authorization头中,格式为Bearer <token>)。
如果获取到 JWT 并且通过JwtUtils验证有效,则从 JWT 中获取用户信息(主题),通过UserDetailsService加载用户详细信息,创建一个UsernamePasswordAuthenticationToken用于 Spring Security 的认证,并将其设置到SecurityContextHolder中,这样后续的请求处理就能获取到认证后的用户信息。
最后通过filterChain.doFilter(request, response)让请求继续向下传递。

5. 配置 Spring Security(如果项目使用 Spring Security)

在 Spring Security 的配置类中,需要配置认证管理器(AuthenticationManager)和添加自定义的过滤器。以下是一个简单的示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordCipher;
import org.springframework.security.crypto.password.PasswordCipher;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
              .and()
              .authorizeRequests()
              .antMatchers("/api/public/**").permitAll()
              .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordCipher(passwordCipher());
    }

    @Bean
    public PasswordCipher passwordCipher() {
        return new BCryptPasswordCipher();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
在这个配置类中:
configure(HttpSecurity http)方法:
禁用了 CSRF(跨站请求伪造)保护,因为 JWT 是无状态的,不需要依赖会话。
设置会话管理策略为STATELESS,表示不使用基于会话的认证。
配置了请求的授权规则,例如/api/public/**路径下的请求可以被任何人访问,其他请求需要认证。
添加了自定义的JwtAuthenticationFilter过滤器,并且放在UsernamePasswordAuthenticationFilter之前,这样在进行用户名 - 密码认证之前就会先验证 JWT。
configure(AuthenticationManagerBuilder auth)方法用于配置认证管理器,指定用户详细信息服务(UserDetailsService)和密码加密方式(这里使用BCryptPasswordCipher)。
passwordCipher方法创建了一个BCryptPasswordCipher用于密码加密。
authenticationManagerBean方法用于暴露AuthenticationManager bean,供其他部分(如JwtAuthenticationFilter)使用

(二)结合用户登录、注销操作(完整过程)

1. 用户实体类(示例)

首先,创建一个简单的用户实体类,用于表示系统中的用户信息,假设用户有用户名、密码等基本属性,并且实现 UserDetails 接口(方便与 Spring Security 集成)。

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class User implements UserDetails {

    private String username;
    private String password;
    private List<String> roles; // 假设用户有角色列表,用于权限控制

    public User(String username, String password, List<String> roles) {
        this.username = username;
        this.password = password;
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String role : roles) {
            authorities.add(new SimpleAuthority(role));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

2. 用户服务层(UserService)

import org.springframework.stereotype.Service;

import java.util.List;

public interface UserService {
    User findByUsername(String username);
}

@Service
public class UserServiceImpl implements UserService {

    // 这里模拟从数据库或者其他数据源获取用户信息,实际应用中需要替换为真实的数据查询逻辑
    @Override
    public User findByUsername(String username) {
        // 假设数据库中有如下用户信息(仅为示例,真实场景需从数据库获取)
        if ("admin".equals(username)) {
            List<String> roles = List.of("ROLE_ADMIN");
            return new User("admin", "$2a$10$pY85s8XzG7f2m8Y18T5c7e6c7888g5X889g9W89c8g", roles);
        }
        return null;
    }
}

3. 用户详细信息服务(UserDetailsService)实现

实现 Spring Security 的 UserDetailsService 接口,用于加载用户详细信息供认证使用,它会调用上面的 UserService 来获取用户数据。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}

4. 登录接口

创建一个登录接口,用于用户提交用户名和密码进行登录,成功登录后生成并返回 JWT 给客户端。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtils jwtUtils;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = jwtUtils.generateToken(loginRequest.getUsername());

        Map<String, String> tokenResponse = new HashMap<>();
        tokenResponse.put("token", jwt);

        return new ResponseEntity<>(tokenResponse, HttpStatus.OK);
    }

    // 登录请求的参数封装类
    static class LoginRequest {
        private String username;
        private String password;

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }
    }
}
在登录接口中:
通过 AuthenticationManager 对用户提交的用户名和密码进行认证,如果认证成功,则将认证信息设置到 SecurityContextHolder 中,表示用户已登录。
然后使用 JwtUtils 工具类生成 JWT,并将其封装到响应中返回给客户端,客户端后续的请求就可以携带这个 JWT 来访问受保护的资源。

5. 注销接口

创建注销接口,用于清除用户的认证状态以及使 JWT 失效(这里使 JWT 失效可以通过一些策略实现,比如将 JWT 加入黑名单等,下面是简单示例通过清除 SecurityContextHolder 中的认证信息来模拟注销效果)。

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    // 省略其他代码,只展示注销接口相关部分

    @PostMapping("/logout")
    public ResponseEntity<?> logout() {
        SecurityContextHolder.clearContext();
        return new ResponseEntity<>(HttpStatus.OK);
    }
}
当用户调用注销接口时,通过 SecurityContextHolder.clearContext() 清除了当前用户的认证上下文信息,这样后续的请求就会被视为未认证状态,相当于完成了注销操作。

6. 完善 JwtAuthenticationFilter(处理 JWT 验证及授权)

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    // 假设存在一个 JWT 黑名单列表,用于存储已注销或失效的 JWT(这里简单用 List 模拟,实际可使用 Redis 等存储)
    @Autowired
    private List<String> jwtBlacklist;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = getTokenFromRequest(request);
        if (token!= null && jwtUtils.validateToken(token) &&!jwtBlacklist.contains(token)) {
            String username = jwtUtils.getSubjectFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserDetails(username);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
                    userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken!= null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
在这个过滤器中,添加了对 jwtBlacklist 的检查,如果请求携带的 JWT 在黑名单中,即使它本身验证签名等是有效的,也会被视为无效的 JWT,不会进行后续的授权操作,从而实现了在注销或者其他使 JWT 失效场景下的安全控制。

7. 配置类调整(SecurityConfig)

根据上述新增的登录、注销接口以及完善的 JWT 验证逻辑,对 Spring Security 配置类进行相应调整,例如配置登录、注销相关的请求路径权限等。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordCipher;
import org.springframework.security.crypto.password.PasswordCipher;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
             .and()
             .authorizeRequests()
             .antMatchers("/api/auth/login", "/api/auth/logout").permitAll() // 允许登录、注销接口无需认证访问
             .antMatchers("/api/public/**").permitAll()
             .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordCipher(passwordCipher());
    }

    @Bean
    public PasswordCipher passwordCipher() {
        return new BCryptPasswordCipher();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

通过以上完整的代码示例,在 Spring Boot 项目中实现了基于 JWT 的身份认证机制,并且结合了用户登录、注销操作,提供了更符合实际应用场景的安全认证授权流程。需要注意的是,上述代码中的部分内容(如用户数据查询、JWT 黑名单存储等)只是简单的示例,在实际生产环境中,需要根据具体的架构和业务需求进行更深入、安全的设计和实现。

(三)登录认证过程具体分析

1、配置AuthenticationManager

在SecurityConfig类中,通过重写configure(AuthenticationManagerBuilder auth)方法来配置AuthenticationManager。
代码如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordCipher(passwordCipher());
}

这里将自定义的UserDetailsService(即CustomUserDetailsService)设置给AuthenticationManagerBuilder。UserDetailsService负责根据用户名加载用户详细信息(包括密码等)。同时,设置了密码编码器passwordCipher()(这里是BCryptPasswordCipher),用于验证用户提交的密码与数据库中存储的密码是否匹配。

2、用户认证过程

2.1 当调用登录接口时,AuthController中的login方法被触发。
2.2 首先创建UsernamePasswordAuthenticationToken:
代码为new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())。这一步将用户提交的用户名和密码封装成一个认证令牌,作为即将进行认证的凭据。
2.3 然后将这个令牌传递给AuthenticationManager的authenticate方法:
代码是authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()))。
2.4 AuthenticationManager会根据配置的UserDetailsService来加载用户详细信息。具体来说,CustomUserDetailsService的loadUserByUsername方法会被调用,该方法会根据用户名查找用户信息。如果找不到用户(User对象为null),会抛出UsernameNotFoundException。
2.5 找到用户后,AuthenticationManager会使用配置的密码编码器(BCryptPasswordCipher)来验证用户提交的密码与数据库中存储的密码是否匹配。如果密码不匹配,会抛出BadCredentialsException等认证异常。
2.6 只有当用户名存在且密码匹配时,认证才会成功,AuthenticationManager会返回一个经过认证的Authentication对象。这个对象包含了用户的详细信息(如权限等),并且会被设置到SecurityContextHolder中(通过SecurityContextHolder.getContext().setAuthentication(authentication);),表示用户已经成功认证登录。后续流程(如生成 JWT)就可以基于这个已认证的状态进行操作。

(四)登录成功后,后续请求认证过程具体分析

1、JwtAuthenticationFilter 的作用

在服务端,JwtAuthenticationFilter起着关键的验证作用。这个过滤器会拦截请求,检查请求头中的Authorization字段是否包含有效的 JWT。
它通过getTokenFromRequest方法从请求头中提取 JWT,代码如下:

private String getTokenFromRequest(HttpServletRequest request) {
    String bearerToken = request.getHeader("Authorization");
    if (bearerToken!= null && bearerToken.startsWith("Bearer ")) {
        return bearerToken.substring(7);
    }
    return null;
}

如果提取到 JWT,就会调用jwtUtils.validateToken(token)方法来验证 JWT 的有效性。jwtUtils是之前创建的 JWT 工具类,validateToken方法会根据配置的密钥和签名算法检查 JWT 的签名是否正确,以及检查 JWT 是否过期等。

2、加载用户详细信息和授权

如果 JWT 验证通过,JwtAuthenticationFilter会从 JWT 中获取主题(通常是用户名),通过jwtUtils.getSubjectFromToken(token)方法实现。
然后,它会使用userDetailsService.loadUserDetails(username)加载用户详细信息。userDetailsService是CustomUserDetailsService,会根据用户名查找用户信息,包括用户的权限等信息。
最后,创建一个UsernamePasswordAuthenticationToken实例,将用户详细信息、凭证(这里设为null)和用户权限放入其中,代码如下:

UserDetails userDetails = userDetailsService.loadUserDetails(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
        userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);

并将这个认证令牌设置到SecurityContextHolder中,这样后续的请求处理就可以获取到已认证的用户信息,完成授权过程,允许用户访问受保护的资源。整个过程是无状态的,每次请求都通过 JWT 验证和加载用户信息来确定用户的身份和权限。

(五)首次请求和后续请求的区别

1、首次请求(登录成功后首次携带 JWT 访问)

首次请求在验证 JWT 成功后,会加载用户详细信息和进行授权。
当用户登录成功并获得 JWT 后,第一次使用该 JWT 访问受保护资源时,JwtAuthenticationFilter会拦截请求。它首先验证 JWT 的有效性,这包括检查签名是否正确以及是否过期等。
在 JWT 验证通过后,会从 JWT 中获取主题(通常是用户名),然后通过userDetailsService.loadUserDetails(username)加载用户详细信息。这个过程会从数据源(如数据库)中获取用户的完整信息,包括权限信息等。接着,会创建一个UsernamePasswordAuthenticationToken,将用户详细信息、凭证(一般设为null,因为 JWT 已经验证了身份)和用户权限放入其中,并将这个认证令牌设置到SecurityContextHolder中。这一步完成了用户的授权,使得后续的请求处理可以获取到已认证的用户信息,从而允许用户访问受保护的资源。

2、后续请求(已完成首次加载用户信息后的请求)

后续请求主要是验证 JWT。因为SecurityContextHolder已经保存了用户的认证信息,只要 JWT 验证通过,就可以直接使用已保存的用户授权信息来处理请求。
每次请求依然会经过JwtAuthenticationFilter,它会提取请求头中的 JWT 并验证。如果 JWT 验证通过,就会根据SecurityContextHolder中已有的用户认证信息来处理请求,不需要再次加载用户详细信息(除非SecurityContextHolder中的信息丢失或者过期等异常情况)。这样可以提高性能,因为避免了每次都从数据源重新加载用户信息的开销。不过,如果在请求处理过程中需要更新用户权限等信息,可能需要重新加载用户详细信息并更新SecurityContextHolder中的内容。


网站公告

今日签到

点亮在社区的每一天
去签到