如何基于Spring Boot、JPA、Hibernate、MySQL实现登录认证并返回JWT给Angular客户端
嘿,我来一步步帮你搞定这个Spring Boot+JWT的登录认证实现,刚好我对这套技术栈熟得很,结合你现有的UserController,咱们一步步来:
一、先把依赖补全
首先得在pom.xml里加上必要的依赖,包括Spring Security、JWT工具库、JPA和MySQL驱动:
<dependencies> <!-- Spring Boot Web --> <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 (JJWT) --> <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> <!-- Spring Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>
二、确保User实体与Repository配置正确
你的User实体需要包含username(唯一约束)、password字段,还要注意密码必须存储加密后的哈希值,不能明文存。示例实体:
import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @Entity @Table(name = "users") @Getter @Setter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; // 可以加其他字段,比如角色、邮箱等 }
然后你的UserRepository需要添加根据用户名查询的方法:
import org.sambasoft.entities.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); }
三、Spring Security核心配置
这部分是认证的核心,分三个小模块:
3.1 密码编码器Bean
配置BCrypt密码编码器,用来加密密码和验证密码:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
3.2 自定义UserDetailsService
实现Spring Security的UserDetailsService,用来从数据库加载用户信息:
import org.sambasoft.entities.User; import org.sambasoft.repos.UserRepository; 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 { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username)); return org.springframework.security.core.userdetails.User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles("USER") // 这里可以根据你的用户角色动态设置,比如从user.getRoles()获取 .build(); } }
3.3 安全FilterChain配置
用最新的Spring Security 6+配置方式,定义认证规则、JWT过滤器等:
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.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { private final CustomUserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(CustomUserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) { this.userDetailsService = userDetailsService; this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) // REST API不需要CSRF保护 .authorizeHttpRequests(auth -> auth .requestMatchers("/api/users/login").permitAll() // 登录接口允许匿名访问 .anyRequest().authenticated() // 其他接口需要认证 ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,用JWT维护会话 ); // 把JWT过滤器放在用户名密码认证过滤器前面 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } }
四、JWT工具类
写一个工具类来生成、解析、验证JWT:
import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; 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 JwtUtil { // 建议把密钥移到配置文件中,这里仅作示例 @Value("${jwt.secret}") private String SECRET_KEY; @Value("${jwt.expiration}") private long JWT_EXPIRATION; // 单位毫秒,比如3600000=1小时 private Key getSigningKey() { return Keys.hmacShaKeyFor(SECRET_KEY.getBytes()); } // 生成JWT public String generateToken(Authentication authentication) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + JWT_EXPIRATION); return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } // 从JWT获取用户名 public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } // 验证JWT public boolean validateToken(String token, UserDetails userDetails) { String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { Date expiration = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .getExpiration(); return expiration.before(new Date()); } }
五、JWT请求过滤器
这个过滤器会在每个请求到来时验证JWT,把用户信息存入SecurityContext:
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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 { private final JwtUtil jwtUtil; private final CustomUserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) { this.jwtUtil = jwtUtil; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头获取JWT String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt)) { try { String username = jwtUtil.getUsernameFromToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 验证token有效 if (jwtUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 把认证信息存入SecurityContext SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { // 这里可以记录日志,比如token无效、过期等 } } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }
六、改造UserController实现登录接口
现在你可以在UserController里添加登录接口,接收用户名密码,验证后返回JWT:
首先定义两个DTO类(用来接收请求和返回响应):
// LoginRequest.java public class LoginRequest { private String username; private String password; // getters and setters 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; } } // JwtResponse.java public class JwtResponse { private String token; public JwtResponse(String token) { this.token = token; } public String getToken() { return token; } }
然后改造你的UserController:
import org.sambasoft.entities.User; import org.sambasoft.repos.UserRepository; 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.*; import java.util.List; @RestController @RequestMapping("/api/users") public class UserController { private final UserRepository userRepository; private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; // 构造函数注入依赖 public UserController(UserRepository userRepository, AuthenticationManager authenticationManager, JwtUtil jwtUtil) { this.userRepository = userRepository; this.authenticationManager = authenticationManager; this.jwtUtil = jwtUtil; } // 你的原有接口,比如获取用户列表 @GetMapping public List<User> getAllUsers() { return userRepository.findAll(); } // 新增登录接口 @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { // 用AuthenticationManager验证用户名密码 Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); // 把认证信息存入SecurityContext SecurityContextHolder.getContext().setAuthentication(authentication); // 生成JWT String jwt = jwtUtil.generateToken(authentication); // 返回JWT return ResponseEntity.ok(new JwtResponse(jwt)); } }
几个关键注意点
- 密码加密:注册用户时,一定要用
passwordEncoder.encode(rawPassword)把明文密码加密后再存入数据库,绝对不能存明文! - 签名密钥保密:在
application.properties里配置JWT参数,不要硬编码:jwt.secret=your-very-long-and-secret-key-here jwt.expiration=3600000 - Angular对接:Angular端发送POST请求到
/api/users/login,携带JSON格式的{username: "xxx", password: "xxx"},拿到返回的token后,后续所有需要认证的请求都要在请求头里加上Authorization: Bearer <你的token>。
内容的提问来源于stack exchange,提问作者Veda
相关产品推荐
相关产品推荐

