在这篇博客中,我们将详细讲解如何在前后端分离的架构中使用 Spring Security,并结合 JWT 实现用户认证。为了让代码更具可读性,我们将在代码中添加详细的注释,帮助理解其工作原理。
1. 项目准备
在后端的 pom.xml 文件中,引入 Spring Security、JWT 以及其他必要的依赖:
<dependencies>
<!-- Spring Boot Web 依赖,构建 REST API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security 依赖,用于用户认证和授权 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依赖,用于生成和解析 JSON Web Token -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- MySQL 依赖,用于数据库连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Spring Data JPA 依赖,用于数据库操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
2. 数据库表设计
首先,我们需要创建用户和角色表,用于存储用户的基本信息以及权限信息:
-- 用户表,用于存储用户信息
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (id)
);
-- 角色表,用于存储用户角色
CREATE TABLE roles (
id BIGINT NOT NULL AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
);
-- 用户与角色的关联表
CREATE TABLE user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
3. 编写用户服务
在 Spring Security 中,用户认证是基于 UserDetails 进行的。我们需要实现 UserDetailsService 接口,从数据库加载用户信息。
1. User 实体类
@Entity
@Table(name = "users") // 映射数据库中的users表
public class User implements UserDetails { // 实现 UserDetails 接口
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER) // 定义多对多关系,立即加载角色信息
@JoinTable(
name = "user_roles", // 关联表名称
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
// 获取用户的角色权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleName())) // 将角色名转换为 GrantedAuthority
.collect(Collectors.toSet());
}
// 实现 UserDetails 接口的其他必要方法
@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 enabled; // 用户是否启用
}
}
Role实体类
@Entity
@Table(name = "roles") // 映射数据库中的roles表
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roleName; // 角色名称
// getter/setter 方法
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
3. 编写 UserDetailsServiceImpl
我们通过 UserDetailsServiceImpl 从数据库中加载用户信息,并实现用户认证逻辑。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
// 根据用户名加载用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found"); // 如果找不到用户,抛出异常
}
return user; // 返回用户信息
}
}
4. 使用 JWT 进行认证
1. JWT 工具类
JWT 用于在用户登录后生成一个 Token,并在后续请求中通过该 Token 验证用户身份。
@Component
public class JwtUtils {
private static final String SECRET_KEY = "mySecretKey"; // 用于签名的秘钥
// 生成 JWT Token
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername()) // 将用户名作为 Token 的主体
.setIssuedAt(new Date()) // 设置 Token 签发时间
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 设置 Token 10小时有效期
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用 HS256 算法进行签名
.compact();
}
// 从 Token 中提取用户名
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY) // 使用签名秘钥进行验证
.parseClaimsJws(token) // 解析 Token
.getBody()
.getSubject(); // 返回 Token 中的主体信息(即用户名)
}
// 验证 Token 是否有效
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token); // 提取 Token 中的用户名
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); // 验证用户名和 Token 是否过期
}
// 检查 Token 是否过期
private boolean isTokenExpired(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getExpiration()
.before(new Date()); // 判断 Token 过期时间是否在当前时间之前
}
}
2. 编写 JwtAuthenticationFilter
JwtAuthenticationFilter 是一个过滤器,用于拦截请求并验证 JWT Token。
@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 {
String authorizationHeader = request.getHeader("Authorization"); // 从请求头获取 Authorization 字段
String username = null;
String token = null;
// 检查 Authorization 头部是否以 "Bearer " 开头
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7); // 提取 Token
username = jwtUtils.extractUsername(token); // 从 Token 中提取用户名
}
// 如果用户名不为空,并且用户没有被认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 加载用户信息
// 验证 Token 是否有效
if (jwtUtils.validateToken(token, userDetails)) {
// 创建认证对象,并将其存储到 SecurityContext 中
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
// 继续执行过滤器链
filterChain.doFilter(request, response);
}
}
5. 配置 Spring Security
最后,我们需要配置 Spring Security,禁用默认的表单登录,并配置 JWT 过滤器。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
// 配置 Spring Security
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用 CSRF
.authorizeRequests()
.antMatchers("/login").permitAll() // 登录接口允许匿名访问
.anyRequest().authenticated(); // 其他所有请求都需要认证
// 在用户名密码认证过滤器之前添加 JWT 过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
// 配置认证管理器,使用自定义的 UserDetailsService 和密码编码器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
// 配置密码编码器,使用 BCrypt 算法
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6. 用户登录接口
创建一个登录接口,用户提交用户名和密码后,后端验证成功后返回 JWT Token。
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
// 用户登录接口
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody AuthRequest authRequest) {
try {
// 使用 Spring Security 的 AuthenticationManager 进行用户认证
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); // 认证失败时返回 401
}
// 认证成功后,生成 JWT Token
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
final String jwt = jwtUtils.generateToken(userDetails);
// 返回 JWT Token 给客户端
return ResponseEntity.ok(new AuthResponse(jwt));
}
}
7. 前端实现
在前端项目中,当用户登录成功后,后端返回的 JWT Token 需要存储在前端的 localStorage 或 sessionStorage 中。每次调用后端 API 时,前端需将该 Token 放在请求头的 Authorization 字段中。
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token'); // 从 localStorage 中获取 JWT Token
if (token) {
config.headers['Authorization'] = 'Bearer ' + token; // 将 Token 添加到请求头中
}
return config;
}, error => {
return Promise.reject(error);
});