文章
spring-boot用户注册登录案例
技术栈:spring-boot + data-jpa + jwt + security
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 http://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.18</version>
</parent>
<groupId>com.learn</groupId>
<artifactId>yt_pos</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<dynamic-ds.version>4.2.0</dynamic-ds.version>
<postgresql.version>42.7.3</postgresql.version>
<mysql.version>8.0.33</mysql.version>
<commons-codec.version>1.15</commons-codec.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Apache commons codec 用于MD5 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<!-- DevTools 开发时热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>application.yml
server:
port: 5000
spring:
application:
name: yt_pos
jpa:
hibernate:
ddl-auto: update
show-sql: true
datasource:
url: jdbc:mysql://localhost:3306/yt_pos
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driverconfiguration目录
package com.learn.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
@Configuration // 告诉 Spring 这是一个配置类,里面定义了 Bean
public class SecurityConfig {
/**
* SecurityFilterChain 安全过滤链,简单理解为“请求进来时,Spring Security 要做哪些检查
* HttpSecurity http 用来配置 HTTP 层的安全规则(比如哪些路径要登录、是否启用 CSRF 等)
*/
@Bean // 把这个方法返回的对象交给 Spring 管理
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(management ->
// 无状态会话(Stateless Session
// 意思:你的应用 不使用 Session(传统服务器保存用户登录状态的方式
// 因为你用的是 JWT(Token)认证, 每次请求都带 Token,服务器不保存登录状态
management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(Authorize ->
// 访问控制规则,按顺序匹配(从上到下)
Authorize
// /api/** → 需要登录(认证)
.requestMatchers(new AntPathRequestMatcher("/api/**")).authenticated()
// /api/super-admin/** → 必须是 ADMIN 角色
.requestMatchers(new AntPathRequestMatcher("/api/super-admin/**"))
// 用户的角色必须是 ADMIN, Spring Security 会自动给角色加前缀 ROLE_
// 数据库里存的角色应该是 ADMIN,框架会当成 ROLE_ADMIN 来比对
.hasRole("ADMIN")
// 其他所有请求 → 允许任何人访问
// 顺序很重要!如果把 .anyRequest().permitAll() 放在最前面,那后面两条就永远不会生效!
.anyRequest()
.permitAll() // 不需要登录,谁都能访问
)
// 在 Spring Security 的 BasicAuthenticationFilter 之前,插入你自己的 JwtValidator 过滤器
// 作用:每次请求进来,先用 JwtValidator 检查请求头里的 Token 是否合法。
// 如果合法 → 把用户信息存到 Spring Security 上下文(后续可用)
// 如果不合法 → 拒绝请求
.addFilterBefore(new JwtValidator(), BasicAuthenticationFilter.class)
// 禁用 CSRF(跨站请求伪造保护)
// 防止别人伪造你的请求(比如在你登录状态下,偷偷提交表单删数据)
// 为什么禁用? 因为你的应用是 无状态 API(JWT),不是传统的表单提交网站。
.csrf(AbstractHttpConfigurer::disable)
// 启用 CORS(跨域资源共享)
// CORS 是什么? 浏览器默认不允许前端(如 localhost:5173)调用不同端口的后端(如 localhost:5000),这就是“跨域”
.cors(
cors -> cors.configurationSource(corsConfigurationSource())
).build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private CorsConfigurationSource corsConfigurationSource() {
return new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration cfg = new CorsConfiguration();
// 这里配置了 允许哪些前端域名访问你的 API
cfg.setAllowedOrigins(
Arrays.asList(
"http://localhost:5173",
"http://localhost:3000"
)
);
// 允许所有 HTTP 方法(GET/POST/PUT/DELETE 等)
cfg.setAllowedMethods(Collections.singletonList("*"));
// 允许前端携带 Cookie(但你用 JWT 一般不需要)
cfg.setAllowCredentials(true);
// 允许所有请求头(比如 Authorization: Bearer xxx )
cfg.setAllowedHeaders(Collections.singletonList("*"));
// 哪些响应头可以暴露给前端(比如返回的 Authorization)
cfg.setExposedHeaders(Arrays.asList("Authorization"));
// 浏览器缓存 CORS 预检结果 1 小时(减少 OPTIONS 请求
cfg.setMaxAge(3600L);
return cfg;
}
};
}
}
package com.learn.configuration;
public class JwtConstant {
public static final String JWT_SECRET = "secret123asdsadsadasd,asdasdasd;k;laskd;lasksdsa;kdosadsadskadlksakdlskalmdasdsakdj";
public static final String JWT_HEADER = "Authorization";
}
package com.learn.configuration;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Service
public class JwtProvider {
// 将字符串密钥转换为符合 HMAC-SHA 算法要求的 SecretKey 对象
static SecretKey key = Keys.hmacShaKeyFor(JwtConstant.JWT_SECRET.getBytes());
// 接收 Spring Security 的 Authentication 对象(通常来自登录成功的用户上下文)
public String generateToken(Authentication authentication) {
// authentication.getAuthorities() 获取用户拥有的权限(通常是 SimpleGrantedAuthority 集合,如 ROLE_ADMIN, ROLE_USER)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
String roles = populateAuthorities(authorities);
return Jwts.builder()
.issuedAt(new Date()) // 签发时间
.expiration(new Date(new Date().getTime() + 86400000)) // 过期时间:当前时间 + 24 小时(86400000 毫秒)
// claim(声明) 是指 Token 中携带的、描述主体(通常是用户)的某项信息或属性
.claim("email", authentication.getName()) // 自定义声明:用户名(通常为邮箱)
.claim("authorities", roles) // 自定义声明:用户角色字符串
.signWith(key) // 使用 HMAC-SHA 算法和密钥签名
.compact(); // 生成紧凑型 JWT 字符串
}
// 解析token中的用户email信息
public String getEmailFromToken(String jwt) {
jwt = jwt.substring(7);
Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jwt)
.getPayload();
return String.valueOf(claims.get("email"));
}
// 角色列表转字符串使用逗号间隔
private String populateAuthorities(Collection<? extends GrantedAuthority> authorities) {
Set<String> auths = new HashSet<>();
for (GrantedAuthority authority : authorities) {
auths.add(authority.getAuthority());
}
return String.join(",", auths);
}
}
package com.learn.configuration;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* extends OncePerRequestFilter 这是 Spring 提供的一个过滤器基类,它确保 每个请求只被过滤一次(避免在转发、包含等情况下重复执行)
* 请求到来
* ↓
* 进入 JwtValidator 过滤器
* ↓
* 从 Header 取出 Token(如 "Bearer xxx")
* ↓
* 去掉 "Bearer ",得到纯 Token
* ↓
* 用密钥验证并解析 Token
* ↓
* 成功 → 提取 email 和 authorities
* → 创建 Authentication 对象
* → 放入 SecurityContext
* ↓
* 继续执行后续过滤器(如权限检查、Controller)
*/
public class JwtValidator extends OncePerRequestFilter {
/**
* 这是每个请求进来时都会执行的方法。
* @param request HTTP 请求(可以从中获取 Header、参数等)
* @param response HTTP 响应(可以设置状态码、返回错误等)
* @param filterChain 继续执行下一个过滤器(必须调用,否则请求就卡住了)
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中提取 JWT
String jwt = request.getHeader(JwtConstant.JWT_HEADER);
if (jwt != null) {
//Bearer jwt 去掉 "Bearer " 前缀
jwt = jwt.substring(7);
try {
// 验证 JWT 并解析payload
SecretKey key = Keys.hmacShaKeyFor(JwtConstant.JWT_SECRET.getBytes());
Claims claims = Jwts.parser() // 解析 JWT
.verifyWith(key) // 用密钥验证签名是否合法(防篡改)
.build()
.parseSignedClaims(jwt) // 解析并验证 Token
.getPayload(); // 获取 JWT 的Payload,里面包含你存的数据,比如用户邮箱、角色等
// 从 Payload 中提取用户信息
String email = String.valueOf(claims.get("email"));
String authorities = String.valueOf(claims.get("authorities"));
// 把角色字符串转成 Spring Security 的 GrantedAuthority 对象
// GrantedAuthority:Spring Security 中表示“权限”或“角色”的接口
// AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER")
// 会自动转成两个 SimpleGrantedAuthority("ROLE_ADMIN") 和 SimpleGrantedAuthority("ROLE_USER")
// 这样 .hasRole("ADMIN") 才能识别(它会自动加 ROLE_ 前缀去匹配)。
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
// 创建认证对象,并放入 Security 上下文
// 虽然名字有 "Password",但这里密码传 null(因为用 Token 认证,不需要密码)
// 三个参数:principal(用户名)、credentials(密码,这里是 null)、authorities(角色列表)
Authentication auth = new UsernamePasswordAuthenticationToken(email, null, auths);
// 关键一步! 把用户身份信息存到 Spring Security 的上下文中。
// 后续的 .authenticated()、.hasRole("ADMIN") 都是靠这个上下文判断的。
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
// 如果 JWT 解析失败(比如过期、签名错误),就抛出 BadCredentialsException。
// Spring Security 会捕获这个异常,并返回 401 Unauthorized(未认证)
throw new BadCredentialsException("Invalid JWT...");
}
}
// 必须调用!
// 表示“JWT 验证完成,继续处理请求”(比如进入 Controller)。
filterChain.doFilter(request, response);
}
}
说明:
### 整体流程:请求如何被验证?
当你访问 /api/user/profile 时,Spring Security 的处理流程如下:
- 请求进入 → 经过 Spring Security 过滤器链
- 到达 JwtValidator(因为你用 addFilterBefore 插入了它)
- JwtValidator 执行以下操作:
- 从 Authorization 请求头中提取 JWT Token
- 调用 JwtProvider 验证 Token 是否合法(签名、过期等)
- 如果合法 → 从 Token 中解析出用户名(如 email)
- 查询用户(或构造 UserDetails)
- !!! 创建 Authentication 对象并放入 SecurityContext
- 后续过滤器(如 FilterSecurityInterceptor)检查权限:
- 发现 /api/** 需要 .authenticated()
- !!! 检查 SecurityContext 中是否有认证信息
- 有 → 放行;无 → 返回 401 未认证
✅ 所以:JwtValidator 是“认证执行者”,HttpSecurity 配置是“认证规则声明者”。model目录
package com.learn.model;
import com.learn.domain.UserRole;
import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.Email;
import java.time.LocalDate;
import java.time.LocalDateTime;
// JPA 实体通常使用数据库主键(如 id)作为唯一标识。但 @Data 自动生成的 equals() 和 hashCode() 默认基于所有字段
// 最佳实践:JPA 实体应显式自定义 equals() 和 hashCode(),通常仅基于主键(且需考虑未持久化状态)。因此避免 @Data 自动生成
@Entity // 标记一个 Java 类为 JPA 实体(Entity),表示这个类对应数据库中的一张表,它的实例对应表中的一行记录
@Getter
@Setter
@NoArgsConstructor // Entity 必须有无参构造函数
@AllArgsConstructor
@EqualsAndHashCode // 对于 JPA @Entity 类,推荐显式使用 @Getter + @Setter,并按需、谨慎地添加 @ToString、@EqualsAndHashCode(通常只基于 id 字段),而不是直接使用 @Data
public class User {
@Id // 注解标记主键字段
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String fullName;
@Column(nullable = false, unique = true)
@Email(message = "Email should be valid")
private String email;
private String phone;
@Column(nullable = false)
private UserRole role;
@Column(nullable = false)
private String password;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime lastLogin;
}
repository目录
package com.learn.repository;
import com.learn.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
// 这段代码是 Spring Data JPA 中定义数据访问层(Repository)的标准方式。
// 它的作用是:为 User 实体提供开箱即用的数据库操作能力,而无需手动编写 SQL 或实现类
// User 该 Repository 管理的实体类(对应数据库表)即带 @Entity 的那个类
// Long 实体主键(@Id 字段)的数据类型
public interface UserRepository extends JpaRepository<User, Long> {
// 在 Spring Data JPA 中,你只要在 Repository 接口中“按规则写下方法名”,Spring 就会自动帮你实现数据库查询逻辑,无需写 SQL、无需写实现类
// 字段名必须和实体类中的属性名一致(不是数据库列名!),比如实体是 fullName,不是 full_name
// 方法名命名规则: find + [Distinct] + [字段名] + [By] + [条件字段] + [And/Or] + [其他条件]
// findByEmailAndFullName(String email, String name)
// findByFullNameContaining(String name)
// findByCreatedAtAfter(LocalDateTime time)
// findByFullNameLike(String pattern)
// findByEmailIn(Collection<String> emails)
// findByRoleIsNull()
User findByEmail(String email);
}
service目录
package com.learn.service;
import com.learn.exceptions.UserException;
import com.learn.model.User;
import java.util.List;
public interface UserService {
User getUserFromJwtToken(String token) throws UserException;
User getCurrentUser() throws UserException;
User getUserByEmail(String email) throws UserException;
User getUserById(Long id) throws Exception;
List<User> getAllUsers();
}
package com.learn.service;
import com.learn.exceptions.UserException;
import com.learn.payload.dto.UserDto;
import com.learn.payload.response.AuthResponse;
public interface AuthService {
AuthResponse signup(UserDto userDto) throws UserException;
AuthResponse login(UserDto userDto) throws UserException;
}
service/impl目录
package com.learn.service.impl;
import com.learn.configuration.JwtProvider;
import com.learn.domain.UserRole;
import com.learn.exceptions.UserException;
import com.learn.mapper.UserMapper;
import com.learn.model.User;
import com.learn.payload.dto.UserDto;
import com.learn.payload.response.AuthResponse;
import com.learn.repository.UserRepository;
import com.learn.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collection;
@Service
@RequiredArgsConstructor // 自动生成一个构造函数,参数包含所有被 final 修饰的字段,以及被 @NonNull 标记的非 final 字段
public class AuthServiceImpl implements AuthService {
// 构造函数 , 字段注入(带 @Autowired)
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final CustomUserImplementation customUserImplementation;
/**
* 实现用户注册功能,包括:
* 1.校验邮箱是否已存在
* 2.禁止注册管理员角色(ROLE_ADMIN)
* 3.加密密码并保存用户
* 4.自动登录(生成 JWT Token)
* 5.返回包含 Token 和用户信息的响应
*/
@Override
public AuthResponse signup(UserDto userDto) throws UserException {
// 检查邮箱是否已注册
User user = userRepository.findByEmail(userDto.getEmail());
if (user != null) {
throw new UserException("email id already registered!");
}
// 禁止注册管理员角色
if (userDto.getRole().equals(UserRole.ROLE_ADMIN)) {
throw new UserException("role admin is not allowed!");
}
// 创建并填充新用户对象
User newUser = new User();
newUser.setEmail(userDto.getEmail());
newUser.setPassword(passwordEncoder.encode(userDto.getPassword()));
newUser.setRole(userDto.getRole());
newUser.setFullName(userDto.getFullName());
newUser.setPhone(userDto.getPhone());
newUser.setLastLogin(LocalDateTime.now());
newUser.setCreatedAt(LocalDateTime.now());
newUser.setUpdatedAt(LocalDateTime.now());
User savedUser = userRepository.save(newUser);
// 自动登录:设置 Spring Security 上下文
// 虽然用户刚注册,但系统希望自动登录(无需再调用 /login)。
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDto.getEmail(), userDto.getPassword());
// 手动创建一个 Authentication 对象,并放入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成 JWT Token
String jwt = jwtProvider.generateToken(authentication);
// 构造并返回响应
AuthResponse authResponse = new AuthResponse();
authResponse.setJwt(jwt);
authResponse.setMessage("Registered Successfully!");
// 入参的 userDto 很可能包含数据库自动生成的字段: id, createdAt, updatedAt 某些默认值字段(如 status = "ACTIVE")
// 而 userDto 是前端传来的,不包含这些字段。如果你直接返回它:前端将无法获取用户完整信息(比如跳转到 /user/{id} 时没有 id
// 使用 UserMapper.toDTO(...) 将实体 User 转为 DTO(避免暴露密码等敏感字段)
authResponse.setUser(UserMapper.toDTO(savedUser));
return authResponse;
}
@Override
public AuthResponse login(UserDto userDto) throws UserException {
// 获取登录凭证 从 DTO 中提取邮箱和密码(明文)。
String email = userDto.getEmail();
String password = userDto.getPassword();
// 调用自定义认证方法:查询用户,比对密码,返回认证通过的 Authentication 对象
Authentication authentication = authenticate(email, password);
// 提取用户角色(用于调试或业务逻辑)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
String role = authorities.iterator().next().getAuthority();
// 生成 JWT Token
String jwt = jwtProvider.generateToken(authentication);
User user = userRepository.findByEmail(email);
// 更新用户最后登录时间
user.setLastLogin(LocalDateTime.now());
userRepository.save(user);
AuthResponse authResponse = new AuthResponse();
authResponse.setJwt(jwt);
authResponse.setMessage("Login Successfully!");
authResponse.setUser(UserMapper.toDTO(user));
return authResponse;
}
private Authentication authenticate(String email, String password) throws UserException {
UserDetails userDetails = customUserImplementation.loadUserByUsername(email);
if (userDetails == null) {
throw new UsernameNotFoundException("email id doesn't exist! " + email);
}
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new UserException("password doesn't match");
}
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
package com.learn.service.impl;
import com.learn.model.User;
import com.learn.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.Collection;
import java.util.Collections;
/**
* Spring Security 中实现自定义用户认证的核心组件
* 用于从数据库加载用户信息并交给 Spring Security 进行身份验证
* 当用户登录时,根据用户名(如邮箱)从数据库查询用户,并返回 Spring Security 能识别的 UserDetails 对象,用于后续密码校验和权限控制。
*/
@Service // 表示这是一个 Spring 的业务组件,会被自动扫描并注册为 Bean
public class CustomUserImplementation implements UserDetailsService {
@Autowired
private UserRepository userRepository;
// UserDetailsService 的核心方法
// 意!虽然叫 username,但实际传入的是用户登录时输入的“标识”,在你的系统中是 邮箱(email)。
// 返回值 UserDetails:Spring Security 内部使用的用户信息接口,包含用户名、密码、权限等。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("user not found");
}
// 构建用户权限(GrantedAuthority)
// GrantedAuthority:代表一个权限(如 ROLE_ADMIN, ROLE_USER)。
// SimpleGrantedAuthority:Spring Security 提供的简单实现,把字符串包装成权限对象。
GrantedAuthority authority = new SimpleGrantedAuthority(
// 通常权限字符串应以 ROLE_ 开头(如 ROLE_ADMIN),否则在使用 hasRole('ADMIN') 时会失败(因为 Spring 会自动加前缀)。
user.getRole().toString()
);
Collection<GrantedAuthority> authorities = Collections.singleton(authority);
// 返回 Spring Security 的内置 User 对象
// 这个 User 对象会被 Spring Security 缓存到 SecurityContext
// 后续可通过 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 获取。
return new org.springframework.security.core.userdetails.User(
// username:用户唯一标识(这里是邮箱)
// password:必须是加密后的密码(如 BCrypt 格式),Spring Security 会自动与用户输入的密码比对
// authorities:权限集合
user.getEmail(), user.getPassword(), authorities
);
}
}
package com.learn.service.impl;
import com.learn.configuration.JwtProvider;
import com.learn.exceptions.UserException;
import com.learn.model.User;
import com.learn.repository.UserRepository;
import com.learn.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final JwtProvider jwtProvider;
@Override
public User getUserFromJwtToken(String token) throws UserException {
String email = jwtProvider.getEmailFromToken(token);
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UserException("User not found");
}
return user;
}
@Override
public User getCurrentUser() throws UserException {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UserException("user not found");
}
return user;
}
@Override
public User getUserByEmail(String email) throws UserException {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UserException("user not found");
}
return user;
}
@Override
public User getUserById(Long id) throws Exception {
return userRepository.findById(id).orElseThrow(
() -> new Exception("user not found")
);
}
@Override
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
mapper目录
package com.learn.mapper;
import com.learn.model.User;
import com.learn.payload.dto.UserDto;
// 这里的mapper其实就是类型映射
public class UserMapper {
public static UserDto toDTO(User savedUser) {
UserDto userDto = new UserDto();
userDto.setId(savedUser.getId());
userDto.setFullName(savedUser.getFullName());
userDto.setPhone(savedUser.getPhone());
userDto.setEmail(savedUser.getEmail());
userDto.setRole(savedUser.getRole());
userDto.setCreatedAt(savedUser.getCreatedAt());
userDto.setUpdatedAt(savedUser.getUpdatedAt());
userDto.setLastLogin(savedUser.getLastLogin());
userDto.setPhone(savedUser.getPhone());
return userDto;
}
}
exceptions目录
package com.learn.exceptions;
public class UserException extends Throwable{
public UserException(String message) {
super(message);
}
}
payload/response目录
package com.learn.payload.response;
import com.learn.payload.dto.UserDto;
import lombok.Data;
@Data
public class AuthResponse {
private String jwt;
private String message;
private UserDto user;
}
payload/dto目录
package com.learn.payload.dto;
import com.learn.domain.UserRole;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class UserDto {
private Long id;
private String fullName;
private String email;
private String phone;
private UserRole role;
private String password;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime lastLogin;
}
domain目录
package com.learn.domain;
public enum UserRole {
ROLE_USER, ROLE_ADMIN,
ROLE_CASHIER,
ROLE_BRANCH_MANAGER,
ROLE_STORE_MANAGER,
}
controller目录
package com.learn.controller;
import com.learn.exceptions.UserException;
import com.learn.payload.dto.UserDto;
import com.learn.payload.response.AuthResponse;
import com.learn.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/signup")
public ResponseEntity<AuthResponse> signupHandler(@RequestBody UserDto userDto) throws UserException {
return ResponseEntity.ok(authService.signup(userDto));
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> loginHandler(@RequestBody UserDto userDto) throws UserException {
return ResponseEntity.ok(authService.login(userDto));
}
}
package com.learn.controller;
import com.learn.exceptions.UserException;
import com.learn.mapper.UserMapper;
import com.learn.model.User;
import com.learn.payload.dto.UserDto;
import com.learn.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/profile")
public ResponseEntity<UserDto> getUserProfile(
@RequestHeader("Authorization") String jwt
) throws UserException {
User user = userService.getUserFromJwtToken(jwt);
return ResponseEntity.ok(UserMapper.toDTO(user));
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(
@RequestHeader("Authorization") String jwt,
@PathVariable Long id
) throws Exception {
User user = userService.getUserById(id);
return ResponseEntity.ok(UserMapper.toDTO(user));
}
}
主程序
package com.learn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
接口测试:
### signup
POST http://localhost:5000/auth/signup
Content-Type: application/json
{
"fullName": "Lucy",
"email": "lucy@qq.com",
"password": "lucy123456",
"role": "ROLE_STORE_MANAGER"
}
### login
POST http://localhost:5000/auth/login
Content-Type: application/json
{
"email": "lucy@qq.com",
"password": "lucy123456"
}
### user profile
GET http://localhost:5000/api/user/profile
Content-Type: application/json
Authorization: Bearer xxx
### get user by id
GET http://localhost:5000/api/user/5
Content-Type: application/json
Authorization: Bearer xxx