全栈开发

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.Driver

configuration目录

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