SpringSecurity结合JwtToken验证(后端部分)

简介: SpringSecurity结合JwtToken验证(后端部分)

SpringSecurity结合JwtToken验证

简介:本文在SpringSecurity基础公共之上,整合JwtToken功能,本文是后端部分。

对于SpringSecurity基本功能,可以看这篇文章:SpringSecurity入门案例——基本功能讲解

项目代码地址:https://gitee.com/geek-li-hua/code-in-blog.git

项目准备

添加依赖

在 pom.xml 中添加下列依赖:

  • jjwt-api
  • jjwt-impl
  • jjwt-jackson
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
  • 完整的pom.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>2.7.15</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springsecurity-back-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springsecurity-back-demo</name>
    <description>springsecurity-back-demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot Starter JDBC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- Project Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- MySQL Connector/J -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>
        <!-- MyBatis Plus Boot Starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- MyBatis Plus Generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!-- Spring Boot Starter Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!-- JJWT API -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <!-- JJWT Implementation -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <!-- JJWT Jackson -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

实现 JwtUtil 类

JwtUtil 类为jwt 工具类,用来创建、解析 jwt token

@Component
public class JwtUtil {
    // 设置JWT的有效期为14天
    public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14;
    // 设置JWT的密钥
    public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
    // 生成UUID
    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
    // 创建JWT
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }
    // 构建JWT的Builder
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        // 使用HS256算法进行签名
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 生成密钥
        SecretKey secretKey = generalKey();
        // 获取当前时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        // 如果未指定有效期,则使用默认的14天
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        // 计算过期时间
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        // 构建JWT
        return Jwts.builder()
                .setId(uuid)  // 设置JWT的唯一标识
                .setSubject(subject)  // 设置JWT的主题
                .setIssuer("sg")  // 设置JWT的签发者
                .setIssuedAt(now)  // 设置JWT的签发时间
                .signWith(signatureAlgorithm, secretKey)  // 使用密钥进行签名
                .setExpiration(expDate);  // 设置JWT的过期时间
    }
    // 生成密钥
    public static SecretKey generalKey() {
        byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
    }
    // 解析JWT
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)  // 使用密钥进行验证
                .build()
                .parseClaimsJws(jwt)  // 解析JWT
                .getBody();  // 获取JWT的内容
    }
}

实现 JwtAuthenticationTokenFilter 类

实现 JwtAuthenticationTokenFilter 类,用来验证 jwt token ,如果验证成功,则将 User 信息注入上下文中。

/**
 * JwtAuthenticationTokenFilter 是用于JWT身份验证的过滤器。
 * 它继承了 OncePerRequestFilter 类,确保过滤器每个请求只应用一次。
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private IUserService userService;
    /**
     * 对每个传入的请求调用此方法,执行JWT身份验证过程。
     *
     * @param request       表示传入请求的 HttpServletRequest 对象。
     * @param response      表示要发送的 HttpServletResponse 对象。
     * @param filterChain   用于调用链中的下一个过滤器的 FilterChain 对象。
     * @throws ServletException     处理请求时发生异常。
     * @throws IOException          发生I/O异常。
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        // 从请求头中获取JWT令牌
        String token = request.getHeader("Authorization");
        // 如果令牌为空或不以"Bearer "开头,则跳过身份验证过程
        if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        // 移除令牌中的"Bearer "前缀
        token = token.substring(7);
        String userid;
        try {
            // 解析JWT令牌以获取声明,包括用户ID
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            // 如果在令牌解析过程中发生异常,则抛出运行时异常
            throw new RuntimeException(e);
        }
        // 使用用户ID从用户服务中检索用户
        User user = userService.getById(Integer.parseInt(userid));
        // 如果用户不存在,则抛出运行时异常
        if (user == null) {
            throw new RuntimeException("用户名未登录");
        }
        // 创建表示已认证用户的 UserDetailsImpl 对象
        UserDetailsImpl loginUser = new UserDetailsImpl(user);
        // 创建包含已认证用户的身份验证令牌,并将其设置在安全上下文中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 调用链中的下一个过滤器
        filterChain.doFilter(request, response);
    }
}

配置config.SecurityConfig类

放行登录、注册等接口。

/**
 * SecurityConfig 是用于配置 Spring Security 的类。
 * 它使用 @Configuration 注解将其标记为配置类,并使用 @EnableWebSecurity 注解启用 Web 安全功能。
 * 它继承了 WebSecurityConfigurerAdapter 类,用于自定义 Spring Security 的配置。
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    /**
     * 配置密码编码器的 Bean。
     *
     * @return 用于密码编码的 PasswordEncoder 对象。
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 配置 AuthenticationManager 的 Bean。
     *
     * @return AuthenticationManager 对象。
     * @throws Exception 如果配置 AuthenticationManager 失败。
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    /**
     * 配置 Spring Security 的 HTTP 安全性。
     *
     * @param http HttpSecurity 对象,用于配置 HTTP 安全性。
     * @throws Exception 如果配置 HttpSecurity 失败。
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/account/token/", "/user/account/register/").permitAll()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
        // 将 JwtAuthenticationTokenFilter 添加到 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

实现接口API

实现API需要三个相关类或接口:

  • 在 service 下创建一个接口。
  • 在 service 下创建一个类实现这个接口。
  • 在 controller 下创建一个类进行操作。
1. 实现 LoginService

验证用户名密码,验证成功后返回 jwt token(令牌)

创建接口:在 service下 创建 user 创建 account 新建一个接口 LoginService

import java.util.Map;
public interface LoginService {
    public Map<String, String> getToken(String username, String password);
}

创建实现类:在 service下 impl 下实现一个LoginServiceImpl

@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    /**
     * 通过用户名和密码获取令牌。
     *
     * @param username 用户名。
     * @param password 密码。
     * @return 包含令牌信息的 Map 对象。
     */
    @Override
    public Map<String, String> getToken(String username, String password) {
        // 创建一个 UsernamePasswordAuthenticationToken 对象,用于进行身份验证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        // 使用 AuthenticationManager 对象进行身份验证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 从身份验证结果中获取认证的用户信息
        UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal();
        User user = loginUser.getUser();
        // 创建 JWT 令牌
        String jwt = JwtUtil.createJWT(user.getId().toString());
        // 创建一个 Map 对象,用于存储令牌信息
        Map<String, String> map = new HashMap<>();
        map.put("error_message", "success");
        map.put("token", jwt);
        return map;
    }
}

创建一个:LoginController

@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;
    @PostMapping("/user/account/token/")
    public Map<String, String> getToken(@RequestParam Map<String, String> map) {
        // 从请求参数中获取用户名和密码
        String username = map.get("username");
        String password = map.get("password");
        // 打印用户名和密码
        System.out.println(username + ' ' + password);
        // 调用登录服务的方法获取令牌
        return loginService.getToken(username, password);
    }
}

2.配置InforService类

根据令牌返回用户信息

创建接口:在 service下 新建一个接口 InfoService

public interface InfoService {
    /*
    * 根据token返回用户信息
    * */
    public Map<String, String> getInfo();
}

创建实现类:在 service下 impl 下新建一个实现类InfoServiceImpl

/**
 * 这个类是InfoService接口的实现类。
 * 它提供了根据token获取用户信息的方法。
 */
@Service
public class InfoServiceImpl implements InfoService {
    /**
     * 根据token返回用户信息。
     * @return 包含用户信息的map
     */
    @Override
    public Map<String, String> getInfo() {
        // 从安全上下文中获取认证token
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        // 从认证token中获取已登录的用户详情
        UserDetailsImpl loginUser = (UserDetailsImpl) authentication.getPrincipal();
        User user = loginUser.getUser();
        // 创建一个map来存储用户信息
        Map<String, String> map = new HashMap<>();
        map.put("error_message", "success");
        map.put("id", user.getId().toString());
        map.put("username", user.getUsername());
        return map;
    }
}

创建controller:在 controller 创建一个InfoController

/**
 * 这个类是一个REST控制器,用于处理与用户信息相关的请求。
 */
@RestController
public class InfoController {
    @Autowired
    private InfoService infoService;
    /**
     * 获取用户信息。
     * @return 包含用户信息的map
     */
    @GetMapping("/user/account/info/")
    public Map<String, String> getInfo() {
        // 调用infoService的getInfo()方法来获取用户信息
        return infoService.getInfo();
    }
}

3. 配置RegisterService类

注册账号

创建接口:在 service下 新建一个接口 RegisterService

public interface RegisterService {
    /*
    * 注册账号
    *
    * @param username 用户名
    * @param password 密码
    * */
    public Map<String, String> register(String username, String password, String confirmedPassword);
}

创建实现类:在 service下 新建一个实现类RegisterServiceImpl

/**
 * 这个类是RegisterService接口的实现类。
 * 它提供了注册用户的方法。
 */
@Service
public class RegisterServiceImpl implements RegisterService {
    @Autowired
    private UserMapper userMapper; // 用户数据访问对象
    @Autowired
    private PasswordEncoder passwordEncoder; // 密码加密器
    /**
     * 注册新用户。
     * @param username 用户名
     * @param password 密码
     * @param confirmedPassword 确认密码
     * @return 包含注册结果的map
     */
    @Override
    public Map<String, String> register(String username, String password, String confirmedPassword) {
        Map<String, String> map = new HashMap<>(); // 创建一个map对象用于存储注册结果
        // 检查用户名是否为空
        if (username == null) {
            map.put("error_message", "用户名不能为空");
            return map;
        }
        // 检查密码是否为空
        if (password == null || confirmedPassword == null) {
            map.put("error_message", "密码不能为空");
            return map;
        }
        // 删除用户名首尾的空白字符
        username = username.trim();
        // 检查用户名是否为空
        if (username.length() == 0) {
            map.put("error_message", "用户名不能为空");
            return map;
        }
        // 检查密码是否为空
        if (password.length() == 0 || confirmedPassword.length() == 0) {
            map.put("error_message", "密码不能为空");
            return map;
        }
        // 检查用户名长度是否超过限制
        if (username.length() > 100) {
            map.put("error_message", "用户名长度不能大于100");
            return map;
        }
        // 检查密码长度是否超过限制
        if (password.length() > 100 || confirmedPassword.length() > 100) {
            map.put("error_message", "密码不能大于100");
            return map;
        }
        // 检查两次输入的密码是否一致
        if (!password.equals(confirmedPassword)) {
            map.put("error_message", "两次输入的密码不一致");
            return map;
        }
        // 查询用户名是否已存在
        QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
        queryWrapper.eq("username", username);
        List<User> users = userMapper.selectList(queryWrapper);
        if (!users.isEmpty()) {
            map.put("error_message", "用户名已存在");
            return map;
        }
        // 添加一个新用户
        String encodedPassword = passwordEncoder.encode(password); // 对密码进行加密处理
        User user = new User(null, username, encodedPassword); // 创建一个新的用户对象
        userMapper.insert(user); // 将用户对象插入数据库
        map.put("error_message", "success"); // 注册成功
        return map;
    }
}

相关文章
|
3月前
|
前端开发 安全 Java
SpringBoot 实现登录验证码(附集成SpringSecurity)
SpringBoot 实现登录验证码(附集成SpringSecurity)
|
9月前
|
安全 Java 数据库
SpringSecurity-4-认证流程源码解析
SpringSecurity-4-认证流程源码解析
47 0
|
5月前
|
JSON 安全 Java
Springboot整合之Shiro和JWT技术实现无感刷新8
Springboot整合之Shiro和JWT技术实现无感刷新8
|
5月前
|
存储 前端开发 NoSQL
Springboot整合之Shiro和JWT技术实现无感刷新7
Springboot整合之Shiro和JWT技术实现无感刷新7
|
5月前
|
缓存 NoSQL Java
Springboot整合之Shiro和JWT技术实现无感刷新6
Springboot整合之Shiro和JWT技术实现无感刷新6
|
5月前
|
Java 数据安全/隐私保护
Springboot整合之Shiro和JWT技术实现无感刷新3
Springboot整合之Shiro和JWT技术实现无感刷新3
|
5月前
|
Java
Springboot整合之Shiro和JWT技术实现无感刷新9
Springboot整合之Shiro和JWT技术实现无感刷新9
|
5月前
|
JSON Java 数据安全/隐私保护
Springboot整合之Shiro和JWT技术实现无感刷新2
Springboot整合之Shiro和JWT技术实现无感刷新2
|
5月前
|
安全 Java 数据安全/隐私保护
Springboot整合之Shiro和JWT技术实现无感刷新
Springboot整合之Shiro和JWT技术实现无感刷新
|
5月前
|
Java
Springboot整合之Shiro和JWT技术实现无感刷新4
Springboot整合之Shiro和JWT技术实现无感刷新4