权限系统别自己瞎写,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,就能看到接口文档了,还能直接在页面上测试接口 —— 前端同事再也不用追着你要文档了!
六、总结:这系统能干嘛?
咱花了这么多时间搭的这个系统,到底能满足企业级需求吗?必须能!
- 安全可靠:密码 BCrypt 加密,JWT 签名防篡改,权限控制细到方法级别,防 CSRF、XSS;
- 分布式友好:无状态认证,支持多服务器部署,不用考虑 Session 共享问题;
- 易扩展:可以轻松加 OAuth2.0、RBAC 细粒度权限(比如用户 - 角色 - 菜单)、验证码登录;
- 拿来就用:代码完整,配置清晰,你只要改改数据库连接和密钥,就能放到项目里用。
权限系统别自己瞎写,Spring Security 已经帮你把大部分坑填了,你只要照着我这个教程搭,半天就能搞定一个企业级的权限系统 —— 省下的时间,喝杯咖啡不香吗?
AI大模型学习福利
作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量
SpringBoot3+JWT打造企业级权限系统
6038

被折叠的 条评论
为什么被折叠?



