SpringBoot 3 + Spring Security 6 + JWT 打造企业级权限系统,拿走即用!!

SpringBoot3+JWT打造企业级权限系统

权限系统别自己瞎写,Spring Security 已经帮你把大部分坑填了,你只要照着我这个教程搭,半天就能搞定一个企业级的权限系统 —— 省下的时间,喝杯咖啡不香吗?

兄弟们,大家是不是都遇到过 “权限系统” 这个老大难?要么是自己瞎写的权限逻辑漏洞百出,老板查数据时差点看到员工的工资条;要么是用了老掉牙的 Session 机制,一搞分布式部署就崩,运维小哥追着你骂;再不然就是集成第三方权限框架,文档看得头皮发麻,配置到半夜还报 “403 Forbidden”—— 别提多闹心了!

今天咱就来搞票大的:用 SpringBoot 3、Spring Security 6 和 JWT,搭一个 “拿来就能用、安全又好扩展” 的企业级权限系统。全程大白话,不整那些 “茴香豆的四种写法” 式的废话,遇到复杂概念我给你掰成 “吃火锅” 的例子,保证你看得爽、学得会,看完还想转发给你那正在踩坑的同事。

一、先搞懂:这仨技术到底是干啥的?

在动手之前,咱得先明白这 “铁三角” 各自的分工 —— 不然就像炒菜时分不清盐和糖,最后肯定翻车。

1. SpringBoot 3:“装修好的毛坯房”

你想啊,要是从零搭个 Java Web 项目,得配 Tomcat、写 web.xml、调依赖冲突… 比装修毛坯房还累。SpringBoot 3 就是 “精装房”:你拎包入住(写业务代码)就行,它帮你搞定了自动配置、依赖管理、内嵌服务器这些破事儿。比如你要连数据库,加个spring-boot-starter-jdbc依赖,配几行配置,直接就能用,不用再像以前那样写一堆工厂类。

2. Spring Security 6:“小区里的保安大哥”

保安大哥的职责很简单:谁能进小区(访问系统)、谁能进单元楼(访问接口)、谁能进顶楼(访问管理员功能),都得他说了算。Spring Security 6 就是干这个的 —— 它能帮你做用户认证(“你是谁”)、权限授权(“你能干嘛”),还能防 CSRF、XSS 这些攻击,比你自己写的 “if (role.equals ("admin"))” 靠谱 100 倍。

不过要注意:Spring Security 6 把以前的WebSecurityConfigurerAdapter给删了(官方说这玩意儿不灵活),现在得用SecurityFilterChain来配置 —— 这坑我先替你踩了,后面代码里会详细说。

3. JWT:“电子身份证”

以前咱用 Session 做认证,就像你去酒店住,前台给你张实体房卡(Session),你每次进电梯都得刷一下(带着 SessionId)。但要是酒店连锁(分布式系统),你在 A 店办的房卡,到 B 店就用不了了 —— 这就是 Session 的痛点。

JWT 就不一样了,它是 “电子身份证”:你登录成功后,服务器给你发一个加密的字符串(JWT),你每次请求接口时把它带在请求头里,服务器只要解密验证一下,就知道你是谁、有啥权限,不用再查数据库存 Session 了。不管你访问哪个服务器(分布式),只要能解密 JWT,就能认证 —— 这就叫 “无状态认证”,香得很!

二、动手搭:环境准备 + 项目结构

咱不搞虚的,直接上干货。先把环境搭好,后面写代码才顺风顺水。

1. 环境要求(别下错版本!)

  • JDK:17 及以上(SpringBoot 3 最低要求 JDK17,别再用 JDK8 了,不然跑不起来,别问我怎么知道的…)
  • Maven:3.6+(没啥好说的,版本太旧可能依赖下不全)
  • 数据库:MySQL 8.0(用 5.7 也能行,但建议跟我统一,减少版本冲突)
  • 开发工具:IDEA(没别的,就是好用,你用 Eclipse 也成,别杠)

2. 创建 SpringBoot 项目(IDEA 一步到位)

打开 IDEA,选 “New Project”,然后:

  • 选 “Spring Initializr”,默认下一步;
  • Group 填你自己的包名(比如 com.xxx.security),Artifact 填 jwt-security-demo,Java 版本选 17;
  • 选依赖的时候,直接搜这几个,勾上:

Spring Web(基础 web 功能,不然写不了接口)

Spring Security(核心安全框架)

Spring Data JPA(操作数据库,比 MyBatis 省代码,新手友好)

MySQL Driver(连 MySQL 数据库)

Lombok(省掉 getter/setter,谁用谁知道香)

  • 选个路径,点 Finish—— 搞定,项目骨架就有了。

3. 项目目录结构(别瞎放文件!)

我给你列个清晰的目录,你照着放,后面找文件不迷路:

复制

com.xxx.security
├── config/          # 配置类(Security配置、JWT配置都在这)
├── controller/      # 接口层(登录、注册、测试接口)
├── entity/          # 实体类(用户、角色)
├── repository/      # 数据访问层(JPA的Repository接口)
├── service/         # 业务逻辑层(用户服务、权限服务)
│   └── impl/        # 服务实现类
├── utils/           # 工具类(JWT工具、全局结果封装)
├── exception/       # 异常处理(认证失败、权限不足的异常)
└── JwtSecurityDemoApplication.java  # 启动类

三、核心代码:从数据库到认证,一步都别落

咱按 “数据层→业务层→安全配置→接口层” 的顺序写,这样逻辑清晰,不容易乱。

1. 第一步:数据库设计(用户 + 角色,多对多)

权限系统最基础的就是 “用户” 和 “角色”—— 比如 “张三” 是 “普通用户”,“李四” 是 “管理员”。用户和角色是多对多关系(一个用户可以有多个角色,一个角色可以给多个用户),所以得三张表:

  • users:用户表(存用户名、密码、状态)
  • roles:角色表(存角色名,比如 ROLE_ADMIN、ROLE_USER)
  • user_roles:中间表(用户 ID + 角色 ID,关联两者)
1.1 实体类编写(用 JPA 映射)

先写Role实体类(角色),用 Lombok 省代码:

package com.xxx.security.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "roles")
@Data  // 自动生成getter/setter
@NoArgsConstructor  // 无参构造
@AllArgsConstructor // 全参构造
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增主键
    private Long id;
    // 角色名,比如ROLE_ADMIN,注意SpringSecurity要求角色名必须以ROLE_开头!
    @Column(nullable = false, unique = true)
    private String name;
}

再写User实体类(用户),注意密码要加密存储:

package com.xxx.security.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails { // 实现UserDetails,让SpringSecurity认识用户
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // 用户名,唯一
    @Column(nullable = false, unique = true)
    private String username;
    // 密码,要加密存储,别存明文!
    @Column(nullable = false)
    private String password;
    // 用户状态:true可用,false禁用
    private boolean enabled = true;
    // 多对多关联角色,fetch = FetchType.EAGER表示查询用户时一起查角色
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_roles", // 中间表名
            joinColumns = @JoinColumn(name = "user_id"), // 关联用户表的字段
            inverseJoinColumns = @JoinColumn(name = "role_id") // 关联角色表的字段
    )
    private Set<Role> roles = new HashSet<>();
    // ------------------- 下面是UserDetails接口的方法,必须实现 -------------------
    // 获取用户的权限(把Role转换成SpringSecurity认识的GrantedAuthority)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());
    }
    // 账号是否未过期(咱简化处理,直接返回true)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 账号是否未锁定(简化处理,返回true)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 密码是否未过期(简化处理,返回true)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}

这里要重点说下:User实现了UserDetails接口,这是因为 SpringSecurity 只认UserDetails类型的用户 —— 就像酒店前台只认身份证,你拿社保卡不行,得把用户信息转换成 “身份证”(UserDetails)才行。

1.2 Repository 接口(JPA 操作数据库)

写两个 Repository 接口,继承JpaRepository,JPA 会自动帮你实现 CRUD 方法,不用写 SQL:

复制

// UserRepository.java
package com.xxx.security.repository;
import com.xxx.security.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 根据用户名查用户(登录时要用)
    Optional<User> findByUsername(String username);
    // 判断用户名是否存在(注册时要用)
    boolean existsByUsername(String username);
}
// RoleRepository.java
package com.xxx.security.repository;
import com.xxx.security.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    // 根据角色名查角色
    Optional<Role> findByName(String name);
}

2. 第二步:JWT 工具类(生成 + 解析 + 验证 token)

JWT 是核心,咱得写个工具类,负责生成 token、解析 token 里的信息、验证 token 是否有效。先在application.yml里配 JWT 的参数(别硬编码!):

# application.yml
server:
  port: 8080 # 服务端口
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/jwt_security_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root # 你的MySQL用户名
    password: 123456 # 你的MySQL密码
    driver-class-name: com.mysql.cj.jdbc.Driver
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update # 自动建表(第一次启动用create,之后用update,避免数据丢失)
    show-sql: true # 打印SQL语句,方便调试
    properties:
      hibernate:
        format_sql: true # 格式化SQL,看着舒服
# 自定义JWT配置
jwt:
  secret: your-secret-key-1234567890abcdefg # 密钥,至少32位!别用我这个,自己改复杂点
  expiration: 7200000 # token过期时间,7200000毫秒=2小时
  refresh-expiration: 604800000 # 刷新token的过期时间,604800000毫秒=7天

然后写 JWT 工具类JwtUtils.java,用jjwt这个库(SpringBoot 3 对应的版本是 0.11.5):

package com.xxx.security.utils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtils {
    // 从配置文件读取密钥
    @Value("${jwt.secret}")
    private String jwtSecret;
    // 从配置文件读取token过期时间
    @Value("${jwt.expiration}")
    private int jwtExpirationMs;
    // 生成JWT Token(根据认证信息)
    public String generateJwtToken(Authentication authentication) {
        // 获取当前登录的用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        // 生成token:包含用户名、过期时间、签名
        return Jwts.builder()
                // 把用户名放进token(叫subject)
                .setSubject(userDetails.getUsername())
                // 设置签发时间(现在)
                .setIssuedAt(new Date())
                // 设置过期时间(现在+2小时)
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                // 签名:用密钥加密,防止token被篡改
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                // 压缩成字符串
                .compact();
    }
    // 从token里获取用户名
    public String getUserNameFromJwtToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
    // 验证token是否有效(没过期、签名正确)
    public boolean validateJwtToken(String authToken) {
        try {
            // 解析token,如果解析成功,说明有效
            Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(authToken);
            return true;
        } catch (SecurityException e) {
            // 签名错误(比如token被篡改)
            System.err.println("Invalid JWT signature: " + e.getMessage());
        } catch (MalformedJwtException e) {
            // token格式错误(比如不是JWT格式)
            System.err.println("Invalid JWT token: " + e.getMessage());
        } catch (ExpiredJwtException e) {
            // token过期
            System.err.println("Expired JWT token: " + e.getMessage());
        } catch (UnsupportedJwtException e) {
            // 不支持的token类型
            System.err.println("Unsupported JWT token: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            // token的claims为空
            System.err.println("JWT claims string is empty: " + e.getMessage());
        }
        return false;
    }
    // 获取签名密钥(把字符串密钥转换成Key对象)
    private Key getSigningKey() {
        // 注意:密钥必须至少32位,不然会报错!
        byte[] keyBytes = jwtSecret.getBytes();
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

这里有个坑要注意:jjwt库在 0.10.x 之后,签名方式变了,必须用Keys.hmacShaKeyFor()生成 Key,而且密钥长度不能低于 32 位 —— 别用 “123456” 这种破密钥,不然黑客分分钟破解你的 token,到时候老板扣你工资可别找我。

3. 第三步:Spring Security 配置(核心中的核心)

Spring Security 6 的配置跟以前不一样了,不能继承WebSecurityConfigurerAdapter,得用@Configuration+@EnableMethodSecurity,然后定义SecurityFilterChain和PasswordEncoder这两个 Bean。

先写SecurityConfig.java:

package com.xxx.security.config;
import com.xxx.security.service.impl.UserDetailsServiceImpl;
import com.xxx.security.utils.JwtUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
// 开启方法级别的权限控制(比如@PreAuthorize("hasRole('ADMIN')"))
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    // 注入用户详情服务(后面会写)
    private final UserDetailsServiceImpl userDetailsService;
    // 注入JWT工具类
    private final JwtUtils jwtUtils;
    // 构造器注入(SpringBoot推荐)
    public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtUtils jwtUtils) {
        this.userDetailsService = userDetailsService;
        this.jwtUtils = jwtUtils;
    }
    // 定义JWT过滤器(后面会写)
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter(jwtUtils, userDetailsService);
    }
    // 密码加密器:用BCrypt加密,安全又方便
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    // 认证提供者:负责从数据库查用户,并用PasswordEncoder验证密码
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // 设置用户详情服务(从哪查用户)
        authProvider.setUserDetailsService(userDetailsService);
        // 设置密码加密器(怎么验证密码)
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    // 认证管理器:负责执行认证逻辑(比如登录时验证用户名密码)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    // 核心过滤器链:配置哪些接口要认证、哪些不用,以及Session策略
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF防护:JWT是无状态的,CSRF没用,关了省事儿
                .csrf(csrf -> csrf.disable())
                // 设置Session策略:无状态(不创建Session,适合分布式)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置接口的访问权限
                .authorizeHttpRequests(auth -> auth
                        // 以下接口不用认证:登录、注册、刷新token
                        .requestMatchers("/api/auth/**").permitAll()
                        // 静态资源不用认证(比如swagger文档)
                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                        // 其他所有接口都需要认证
                        .anyRequest().authenticated()
                );
        // 把JWT过滤器加到UsernamePasswordAuthenticationFilter前面
        // 因为要先验证JWT,再执行后续的认证逻辑
        http.authenticationProvider(authenticationProvider())
                .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

这里要解释几个关键配置:

  • @EnableMethodSecurity(prePostEnabled = true):开启方法级权限控制,后面可以在 Controller 方法上用@PreAuthorize("hasRole('ADMIN')")来限制角色访问。
  • SessionCreationPolicy.STATELESS:无状态 Session,意思是服务器不存 Session,所有认证信息都在 JWT 里 —— 这是分布式系统的必备配置,不然你在 A 服务器登录,到 B 服务器又要重新登录。
  • addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class):把 JWT 过滤器放在默认的用户名密码过滤器前面,因为要先验证 JWT 是否有效,再判断用户是否有权限访问接口。

4. 第四步:JWT 过滤器(自动认证 JWT)

写一个JwtAuthenticationFilter,继承OncePerRequestFilter,作用是:每次请求接口时,从请求头里获取 JWT,验证 JWT 是否有效,如果有效,就把用户信息放到SecurityContext里 —— 这样 SpringSecurity 就知道 “当前登录的是谁” 了。

package com.xxx.security.config;
import com.xxx.security.service.impl.UserDetailsServiceImpl;
import com.xxx.security.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    // 每次请求都会执行这个方法
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            // 1. 从请求头里获取JWT
            String jwt = parseJwt(request);
            // 2. 验证JWT是否有效,并且SecurityContext里还没有认证信息
            if (jwt != null && jwtUtils.validateJwtToken(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 3. 从JWT里获取用户名
                String username = jwtUtils.getUserNameFromJwtToken(jwt);
                // 4. 根据用户名从数据库查用户信息(UserDetails)
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                // 5. 创建认证令牌(把用户信息和权限放进去)
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null, // 密码不用放,因为JWT已经验证过了
                                userDetails.getAuthorities());
                // 6. 设置认证的详细信息(比如请求IP、会话ID)
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 7. 把认证令牌放到SecurityContext里
                // 这样后面的过滤器就知道“当前用户已经认证过了”
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            // 认证失败,打印日志,不把异常抛出去,避免影响后续过滤器
            logger.error("Cannot set user authentication: {}", e);
        }
        // 8. 继续执行后续的过滤器(比如UsernamePasswordAuthenticationFilter)
        filterChain.doFilter(request, response);
    }
    // 从请求头里解析JWT:请求头格式是“Authorization: Bearer <token>”
    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");
        // 判断请求头是否存在,并且以“Bearer ”开头
        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            // 截取“Bearer ”后面的部分,就是JWT
            return headerAuth.substring(7);
        }
        return null;
    }
}

这个过滤器的逻辑很简单:就像小区保安在门口检查你的电子身份证(JWT),如果身份证有效,就给你登记一下(放到SecurityContext),让你进小区;如果无效,就不让你进。

5. 第五步:用户详情服务(查用户信息)

写UserDetailsServiceImpl,实现UserDetailsService接口,重写loadUserByUsername方法 —— 这个方法是 SpringSecurity 用来根据用户名查询用户信息的,必须实现。

package com.xxx.security.service.impl;

import com.xxx.security.entity.User;
import com.xxx.security.repository.UserRepository;
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;
import org.springframework.transaction.annotation.Transactional;

@Service
publicclass UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    // 根据用户名查询用户信息
    @Override
    @Transactional// 开启事务,避免懒加载问题
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查用户,如果没查到,抛异常
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

        // 返回User对象(因为User实现了UserDetails)
        return user;
    }
}

这里用了@Transactional注解,是因为User和Role是多对多关联,查询用户时会懒加载角色,如果没有事务,会报 “懒加载异常”—— 这是 JPA 的老坑了,记住就行。

6. 第六步:登录 & 注册接口(用户交互)

写AuthController,提供登录(/api/auth/login)和注册(/api/auth/register)接口,让用户能登录获取 JWT,能注册新用户。

先写个全局结果封装类ResponseResult.java,统一接口返回格式(前端看着舒服):

package com.xxx.security.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass ResponseResult<T> {
    // 状态码:200成功,400失败,401未认证,403无权限
    privateint code;
    // 消息:成功/失败的描述
    private String message;
    // 数据:返回的业务数据
    private T data;

    // 成功的静态方法(不带数据)
    publicstatic <T> ResponseResult<T> success() {
        returnnew ResponseResult<>(200, "Success", null);
    }

    // 成功的静态方法(带数据)
    publicstatic <T> ResponseResult<T> success(T data) {
        returnnew ResponseResult<>(200, "Success", data);
    }

    // 失败的静态方法
    publicstatic <T> ResponseResult<T> error(int code, String message) {
        returnnew ResponseResult<>(code, message, null);
    }
}

然后写AuthController:

package com.xxx.security.controller;

import com.xxx.security.entity.Role;
import com.xxx.security.entity.User;
import com.xxx.security.repository.RoleRepository;
import com.xxx.security.repository.UserRepository;
import com.xxx.security.utils.JwtUtils;
import com.xxx.security.utils.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
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.security.crypto.password.PasswordEncoder;
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 java.util.HashSet;
import java.util.Set;

@RestController
@RequestMapping("/api/auth")
publicclass AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtUtils jwtUtils;

    // 登录接口:接收用户名和密码,返回JWT
    @PostMapping("/login")
    public ResponseResult<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        // 1. 创建认证令牌(用户名+密码)
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );

        // 2. 把认证信息放到SecurityContext里
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 3. 生成JWT
        String jwt = jwtUtils.generateJwtToken(authentication);

        // 4. 返回JWT
        return ResponseResult.success(new JwtResponse(jwt));
    }

    // 注册接口:接收用户名、密码、角色,创建新用户
    @PostMapping("/register")
    public ResponseResult<?> registerUser(@RequestBody SignupRequest signUpRequest) {
        // 1. 检查用户名是否已存在
        if (userRepository.existsByUsername(signUpRequest.getUsername())) {
            return ResponseResult.error(400, "Error: Username is already taken!");
        }

        // 2. 创建新用户
        User user = new User();
        user.setUsername(signUpRequest.getUsername());
        // 密码加密存储(重要!别存明文!)
        user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));

        // 3. 设置用户角色(默认给ROLE_USER,也可以让前端传角色)
        Set<String> strRoles = signUpRequest.getRoles();
        Set<Role> roles = new HashSet<>();

        if (strRoles == null) {
            // 如果前端没传角色,默认给ROLE_USER
            Role userRole = roleRepository.findByName("ROLE_USER")
                    .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
            roles.add(userRole);
        } else {
            // 如果前端传了角色,转换成Role对象
            strRoles.forEach(role -> {
                switch (role) {
                    case"admin":
                        Role adminRole = roleRepository.findByName("ROLE_ADMIN")
                                .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
                        roles.add(adminRole);
                        break;
                    default:
                        Role userRole = roleRepository.findByName("ROLE_USER")
                                .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
                        roles.add(userRole);
                }
            });
        }

        user.setRoles(roles);
        // 4. 保存用户到数据库
        userRepository.save(user);

        return ResponseResult.success("User registered successfully!");
    }

    // ------------------- 内部静态类:接收前端请求参数 -------------------
    // 登录请求参数(用户名+密码)
    publicstaticclass LoginRequest {
        privateString username;
        privateString password;

        // getter/setter
        publicString getUsername() { return username; }
        publicvoid setUsername(String username) { this.username = username; }
        publicString getPassword() { return password; }
        publicvoid setPassword(String password) { this.password = password; }
    }

    // 注册请求参数(用户名+密码+角色)
    publicstaticclass SignupRequest {
        privateString username;
        privateString password;
        private Set<String> roles;

        // getter/setter
        publicString getUsername() { return username; }
        publicvoid setUsername(String username) { this.username = username; }
        publicString getPassword() { return password; }
        publicvoid setPassword(String password) { this.password = password; }
        public Set<String> getRoles() { return roles; }
        publicvoid setRoles(Set<String> roles) { this.roles = roles; }
    }

    // JWT返回结果(token)
    publicstaticclass JwtResponse {
        privateString token;

        public JwtResponse(String token) {
            this.token = token;
        }

        // getter
        publicString getToken() { return token; }
    }
}

这里要注意:注册的时候一定要给密码加密!用passwordEncoder.encode()方法,不然密码明文存到数据库,黑客拿到数据库就能直接登录 —— 这是低级错误,但很多新手会犯,记住了!

7. 第七步:测试接口(验证权限)

写两个测试 Controller,分别对应普通用户(ROLE_USER)和管理员(ROLE_ADMIN)的接口,用@PreAuthorize注解限制角色访问。

// UserController.java(普通用户接口)
package com.xxx.security.controller;

import com.xxx.security.utils.ResponseResult;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/user")
publicclass UserController {

    // 只有ROLE_USER角色能访问
    @GetMapping("/profile")
    @PreAuthorize("hasRole('USER')")
    public ResponseResult<?> getUserProfile() {
        return ResponseResult.success("This is your user profile. Only users can see this.");
    }
}

// AdminController.java(管理员接口)
package com.xxx.security.controller;

import com.xxx.security.utils.ResponseResult;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/admin")
publicclass AdminController {

    // 只有ROLE_ADMIN角色能访问
    @GetMapping("/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseResult<?> getAdminDashboard() {
        return ResponseResult.success("This is admin dashboard. Only admins can see this.");
    }
}

@PreAuthorize("hasRole('USER')")这个注解的意思是:只有拥有 ROLE_USER 角色的用户才能访问这个接口 —— 这就是方法级别的权限控制,非常灵活。

四、测试运行:用 Postman 验证效果

代码写完了,咱得跑起来测试一下,看看好不好使。

1. 初始化角色数据

因为 JPA 的ddl-auto: update只会建表,不会插入数据,所以得先手动往roles表插两条角色数据:

INSERT INTO roles (name) VALUES ('ROLE_USER');
INSERT INTO roles (name) VALUES ('ROLE_ADMIN');

2. 启动项目

运行JwtSecurityDemoApplication.java,看控制台有没有报错,只要没报 “Could not connect to database” 或者 “Invalid key” 之类的错,就说明启动成功了。

3. 测试注册接口

打开 Postman,发送 POST 请求到http://localhost:8080/api/auth/register,请求体是 JSON:

{
  "username": "user1",
  "password": "123456",
  "roles": ["user"]
}

再注册一个管理员用户:

{
  "username": "admin1",
  "password": "123456",
  "roles": ["admin"]
}

如果返回{"code":200,"message":"Success","data":"User registered successfully!"},说明注册成功。

4. 测试登录接口

发送 POST 请求到http://localhost:8080/api/auth/login,请求体是 JSON(用 admin1 登录):

{
  "username": "admin1",
  "password": "123456"
}

返回结果里会有token,比如:

{
  "code":200,
  "message":"Success",
  "data":{
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}

把这个 token 复制下来,后面要用。

5. 测试权限接口

5.1 测试管理员接口

发送 GET 请求到http://localhost:8080/api/admin/dashboard,在请求头里加Authorization: Bearer <你的token>(把<你的token>换成刚才复制的 token)。

如果返回{"code":200,"message":"Success","data":"This is admin dashboard. Only admins can see this."},说明管理员能正常访问。

5.2 测试普通用户接口

用 user1 登录获取 token,然后发送 GET 请求到http://localhost:8080/api/user/profile,同样加Authorization头。

如果返回成功,说明普通用户能访问自己的接口。

5.3 测试权限不足的情况

用 user1 的 token 访问http://localhost:8080/api/admin/dashboard,会返回 403 错误,说明权限控制生效了 —— 这就对了,普通用户不能访问管理员接口!

五、进阶优化:让系统更企业级

上面的系统已经能用了,但作为 “企业级” 系统,还得优化几个地方。

1. 实现刷新 token 功能

token 过期后,用户得重新登录,体验不好。咱加个刷新 token 的接口,用户用快过期的 token 换一个新 token,不用重新输入用户名密码。

在AuthController里加个/refresh-token接口:

@PostMapping("/refresh-token")
public ResponseResult<?> refreshToken(@RequestBody RefreshTokenRequest request) {
    String oldToken = request.getToken();

    // 1. 验证旧token是否有效
    if (!jwtUtils.validateJwtToken(oldToken)) {
        return ResponseResult.error(400, "Invalid or expired token!");
    }

    // 2. 从旧token里获取用户名
    String username = jwtUtils.getUserNameFromJwtToken(oldToken);

    // 3. 根据用户名查用户信息,生成新token
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    String newToken = jwtUtils.generateJwtToken(authentication);

    return ResponseResult.success(new JwtResponse(newToken));
}

// 刷新token的请求参数
publicstaticclass RefreshTokenRequest {
    privateString token;

    // getter/setter
    publicString getToken() { return token; }
    publicvoid setToken(String token) { this.token = token; }
}

2. 全局异常处理

以前认证失败(比如用户名密码错误)会返回默认的 401 页面,很不友好。咱写个全局异常处理器,统一返回 JSON 格式的错误信息。

package com.xxx.security.exception;

import com.xxx.security.utils.ResponseResult;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice// 全局异常处理,只处理Controller的异常
publicclass GlobalExceptionHandler {

    // 处理用户名密码错误的异常
    @ExceptionHandler(BadCredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseResult<?> handleBadCredentialsException(BadCredentialsException e) {
        return ResponseResult.error(401, "Username or password is incorrect!");
    }

    // 处理权限不足的异常
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ResponseResult<?> handleAccessDeniedException(AccessDeniedException e) {
        return ResponseResult.error(403, "You don't have permission to access this resource!");
    }

    // 处理其他所有异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseResult<?> handleGeneralException(Exception e) {
        return ResponseResult.error(500, "Internal server error: " + e.getMessage());
    }
}

这样一来,不管是用户名密码错,还是权限不足,都会返回清晰的 JSON 错误信息,前端好处理。

3. 集成 Swagger 文档

企业级系统肯定要有接口文档,不然前端同事跟你吵架。咱集成 Swagger 3(OpenAPI),自动生成接口文档。

先加依赖(pom.xml):

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>

然后写个 Swagger 配置类:

package com.xxx.security.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
publicclass SwaggerConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        returnnew OpenAPI()
                // 接口文档的基本信息(标题、描述、版本)
                .info(new Info()
                        .title("JWT Security API")
                        .description("SpringBoot 3 + Spring Security 6 + JWT 企业级权限系统接口文档")
                        .version("v1.0.0"))
                // 配置JWT认证(让Swagger支持在请求头加Authorization)
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
                .components(new Components()
                        .addSecuritySchemes("bearerAuth", new SecurityScheme()
                                .type(SecurityScheme.Type.HTTP)
                                .scheme("bearer")
                                .bearerFormat("JWT")));
    }
}

启动项目后,访问http://localhost:8080/swagger-ui/index.html,就能看到接口文档了,还能直接在页面上测试接口 —— 前端同事再也不用追着你要文档了!

六、总结:这系统能干嘛?

咱花了这么多时间搭的这个系统,到底能满足企业级需求吗?必须能!

  1. 安全可靠:密码 BCrypt 加密,JWT 签名防篡改,权限控制细到方法级别,防 CSRF、XSS;
  2. 分布式友好:无状态认证,支持多服务器部署,不用考虑 Session 共享问题;
  3. 易扩展:可以轻松加 OAuth2.0、RBAC 细粒度权限(比如用户 - 角色 - 菜单)、验证码登录;
  4. 拿来就用:代码完整,配置清晰,你只要改改数据库连接和密钥,就能放到项目里用。

权限系统别自己瞎写,Spring Security 已经帮你把大部分坑填了,你只要照着我这个教程搭,半天就能搞定一个企业级的权限系统 —— 省下的时间,喝杯咖啡不香吗?

AI大模型学习福利

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值