Skip to content
第 86 / 250 章后端⏱ 10 分钟阅读

第 86 章:用户与认证模块

学习目标

  • 实现 User 模块的 CRUD
  • 集成 Spring Security + JWT 完成登录认证
  • 实现双 Token 机制

一、模块结构

taskflow-system/
└── src/main/java/com/taskflow/modules/system/
    ├── controller/
    │   ├── AuthController.java
    │   └── UserController.java
    ├── service/
    │   ├── UserService.java
    │   └── impl/UserServiceImpl.java
    ├── mapper/UserMapper.java
    ├── entity/
    │   ├── User.java
    │   └── UserRole.java
    ├── dto/
    │   ├── LoginDTO.java
    │   ├── UserCreateDTO.java
    │   └── UserUpdateDTO.java
    ├── vo/
    │   ├── LoginVO.java
    │   └── UserVO.java
    └── convert/UserConvert.java

二、实体

java
package com.taskflow.modules.system.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.taskflow.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseEntity {

    private String username;
    private String password;
    private String nickname;
    private String email;
    private String phone;
    private String avatar;
    private Integer gender;
    private Integer status;
    private Long deptId;
    private LocalDateTime lastLoginTime;
    private String lastLoginIp;
    private Integer version;
}

三、Mapper

java
package com.taskflow.modules.system.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.taskflow.modules.system.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface UserMapper extends BaseMapper<User> {

    User selectByUsername(@Param("username") String username);

    List<Long> selectRoleIdsByUserId(@Param("userId") Long userId);
}
xml
<!-- UserMapper.xml -->
<mapper namespace="com.taskflow.modules.system.mapper.UserMapper">
    <select id="selectByUsername" resultType="User">
        SELECT * FROM sys_user
        WHERE username = #{username} AND is_deleted = 0
        LIMIT 1
    </select>

    <select id="selectRoleIdsByUserId" resultType="Long">
        SELECT role_id FROM sys_user_role WHERE user_id = #{userId}
    </select>
</mapper>

四、DTO / VO

java
@Data
public class LoginDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
}

@Data
public class UserCreateDTO {
    @NotBlank @Length(min = 3, max = 50)
    private String username;
    @NotBlank @Length(min = 6, max = 100)
    private String password;
    @NotBlank
    private String nickname;
    @Email
    private String email;
    private String phone;
    private Integer gender;
    private Long deptId;
    private List<Long> roleIds;
}

@Data
public class LoginVO {
    private String accessToken;
    private String refreshToken;
    private Long expiresIn;
    private UserVO user;
}

@Data
public class UserVO {
    private Long id;
    private String username;
    private String nickname;
    private String email;
    private String phone;
    private String avatar;
    private Integer gender;
    private Integer status;
    private Long deptId;
    private String deptName;
    private List<RoleVO> roles;
    private LocalDateTime lastLoginTime;
}

五、Service

java
public interface UserService extends IService<User> {

    LoginVO login(LoginDTO dto);

    LoginVO refreshToken(String refreshToken);

    void logout();

    Long createUser(UserCreateDTO dto);

    void updateUser(Long id, UserUpdateDTO dto);

    void deleteUser(Long id);

    void resetPassword(Long id, String newPassword);

    void assignRoles(Long userId, List<Long> roleIds);

    PageResult<UserVO> page(UserPageQuery query);

    UserVO getVOById(Long id);
}
java
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final RefreshTokenService refreshTokenService;
    private final TokenBlacklistService blacklistService;
    private final RoleMapper roleMapper;
    private final DeptMapper deptMapper;
    private final UserRoleMapper userRoleMapper;
    private final AuthenticationManager authenticationManager;
    private final UserConvert userConvert;

    @Override
    @Transactional
    public LoginVO login(LoginDTO dto) {
        // ① Spring Security 认证
        Authentication auth;
        try {
            auth = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            dto.getUsername(), dto.getPassword()));
        } catch (BadCredentialsException e) {
            throw new BusinessException(ErrorCode.USER_PASSWORD_ERROR);
        }

        CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();

        // ② 更新登录信息
        User user = baseMapper.selectById(userDetails.getUserId());
        user.setLastLoginTime(LocalDateTime.now());
        user.setLastLoginIp(SecurityUtils.getCurrentRequestIp());
        baseMapper.updateById(user);

        // ③ 生成 Token
        List<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList();
        String access = jwtUtil.generateAccess(user.getId(), user.getUsername(), roles);
        String refresh = jwtUtil.generateRefresh(user.getId());

        // ④ 保存 Refresh Token
        refreshTokenService.save(user.getId(), refresh);

        // ⑤ 返回
        LoginVO vo = new LoginVO();
        vo.setAccessToken(access);
        vo.setRefreshToken(refresh);
        vo.setExpiresIn(7200L);
        vo.setUser(toVO(user, roles));
        return vo;
    }

    @Override
    public LoginVO refreshToken(String refreshToken) {
        Claims claims = jwtUtil.parse(refreshToken);
        if (!"refresh".equals(claims.get("type", String.class))) {
            throw new BusinessException(ErrorCode.TOKEN_INVALID);
        }
        Long userId = Long.valueOf(claims.getSubject());

        if (refreshTokenService.isRevoked(refreshToken)) {
            throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
        }
        if (!refreshTokenService.matches(userId, refreshToken)) {
            throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
        }

        User user = baseMapper.selectById(userId);
        List<String> roles = getUserRoles(userId);
        String newAccess = jwtUtil.generateAccess(userId, user.getUsername(), roles);
        String newRefresh = jwtUtil.generateRefresh(userId);
        refreshTokenService.save(userId, newRefresh);
        refreshTokenService.revoke(refreshToken);

        LoginVO vo = new LoginVO();
        vo.setAccessToken(newAccess);
        vo.setRefreshToken(newRefresh);
        vo.setExpiresIn(7200L);
        vo.setUser(toVO(user, roles));
        return vo;
    }

    @Override
    public void logout() {
        Long userId = SecurityUtils.getCurrentUserId();
        refreshTokenService.revokeByUser(userId);
    }

    @Override
    @Transactional
    public Long createUser(UserCreateDTO dto) {
        // ① 检查用户名
        if (baseMapper.selectByUsername(dto.getUsername()) != null) {
            throw new BusinessException(ErrorCode.USER_EXISTS);
        }

        // ② 创建用户
        User user = userConvert.toEntity(dto);
        user.setPassword(passwordEncoder.encode(dto.getPassword()));
        user.setStatus(1);
        baseMapper.insert(user);

        // ③ 分配角色
        if (dto.getRoleIds() != null && !dto.getRoleIds().isEmpty()) {
            assignRoles(user.getId(), dto.getRoleIds());
        }
        return user.getId();
    }

    @Override
    @Transactional
    public void updateUser(Long id, UserUpdateDTO dto) {
        User user = baseMapper.selectById(id);
        Assert.notNull(user, ErrorCode.USER_NOT_FOUND);
        userConvert.updateEntity(dto, user);
        baseMapper.updateById(user);

        if (dto.getRoleIds() != null) {
            userRoleMapper.deleteByUserId(id);
            assignRoles(id, dto.getRoleIds());
        }
    }

    @Override
    @Transactional
    public void deleteUser(Long id) {
        Assert.isTrue(!id.equals(1L), "超级管理员不可删除");
        baseMapper.deleteById(id);
        userRoleMapper.deleteByUserId(id);
    }

    @Override
    public void resetPassword(Long id, String newPassword) {
        User user = baseMapper.selectById(id);
        Assert.notNull(user, ErrorCode.USER_NOT_FOUND);
        user.setPassword(passwordEncoder.encode(newPassword));
        baseMapper.updateById(user);
    }

    @Override
    public void assignRoles(Long userId, List<Long> roleIds) {
        userRoleMapper.deleteByUserId(userId);
        if (roleIds != null) {
            for (Long roleId : roleIds) {
                userRoleMapper.insert(new UserRole(userId, roleId));
            }
        }
        // ① 清理权限缓存
        permissionService.clearCache(userId);
    }

    @Override
    public PageResult<UserVO> page(UserPageQuery query) {
        Page<User> page = new Page<>(query.getCurrent(), query.getSize());
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                .like(StringUtils.hasText(query.getKeyword()), User::getUsername, query.getKeyword())
                .or().like(StringUtils.hasText(query.getKeyword()), User::getPhone, query.getKeyword())
                .eq(query.getStatus() != null, User::getStatus, query.getStatus())
                .eq(query.getDeptId() != null, User::getDeptId, query.getDeptId())
                .orderByDesc(User::getCreateTime);
        IPage<User> result = baseMapper.selectPage(page, wrapper);
        return PageResult.of(result, this::toVO);
    }

    @Override
    public UserVO getVOById(Long id) {
        User user = baseMapper.selectById(id);
        Assert.notNull(user, ErrorCode.USER_NOT_FOUND);
        List<String> roles = getUserRoles(id);
        return toVO(user, roles);
    }

    private List<String> getUserRoles(Long userId) {
        return roleMapper.selectByUserId(userId).stream()
                .map(Role::getCode).toList();
    }

    private UserVO toVO(User user, List<String> roles) {
        UserVO vo = userConvert.toVO(user);
        vo.setRoles(roleMapper.selectByUserId(user.getId()).stream()
                .map(userConvert::toRoleVO).toList());
        if (user.getDeptId() != null) {
            Dept dept = deptMapper.selectById(user.getDeptId());
            if (dept != null) vo.setDeptName(dept.getName());
        }
        return vo;
    }
}

六、Controller

java
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Tag(name = "认证管理")
public class AuthController {

    private final UserService userService;

    @Operation(summary = "登录")
    @PostMapping("/login")
    public Result<LoginVO> login(@RequestBody @Valid LoginDTO dto) {
        return Result.ok(userService.login(dto));
    }

    @Operation(summary = "刷新 Token")
    @PostMapping("/refresh")
    public Result<LoginVO> refresh(@RequestHeader("Refresh-Token") String refreshToken) {
        return Result.ok(userService.refreshToken(refreshToken));
    }

    @Operation(summary = "登出")
    @PostMapping("/logout")
    public Result<Void> logout() {
        userService.logout();
        return Result.ok();
    }

    @Operation(summary = "获取当前用户信息")
    @GetMapping("/me")
    public Result<UserVO> me() {
        return Result.ok(userService.getVOById(SecurityUtils.getCurrentUserId()));
    }
}
java
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@Tag(name = "用户管理")
public class UserController {

    private final UserService userService;

    @Operation(summary = "分页查询")
    @GetMapping("/page")
    public Result<PageResult<UserVO>> page(UserPageQuery query) {
        return Result.ok(userService.page(query));
    }

    @Operation(summary = "查询详情")
    @GetMapping("/{id}")
    public Result<UserVO> getById(@PathVariable Long id) {
        return Result.ok(userService.getVOById(id));
    }

    @Operation(summary = "创建用户")
    @RequiresPermission("user:create")
    @PostMapping
    public Result<Long> create(@RequestBody @Valid UserCreateDTO dto) {
        return Result.ok(userService.createUser(dto));
    }

    @Operation(summary = "更新用户")
    @RequiresPermission("user:update")
    @PutMapping("/{id}")
    public Result<Void> update(@PathVariable Long id, @RequestBody @Valid UserUpdateDTO dto) {
        userService.updateUser(id, dto);
        return Result.ok();
    }

    @Operation(summary = "删除用户")
    @RequiresPermission("user:delete")
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.deleteUser(id);
        return Result.ok();
    }

    @Operation(summary = "重置密码")
    @RequiresPermission("user:reset-password")
    @PutMapping("/{id}/reset-password")
    public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
        userService.resetPassword(id, newPassword);
        return Result.ok();
    }
}

七、Spring Security 配置

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;
    private final CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .requestMatchers("/actuator/health/**").permitAll()
                .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authEntryPoint)
                .accessDeniedHandler(accessDeniedHandler))
            .build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration cfg) throws Exception {
        return cfg.getAuthenticationManager();
    }
}

八、JWT 过滤器

java
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final CustomUserDetailsService userDetailsService;
    private final TokenBlacklistService blacklistService;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse resp,
                                    FilterChain chain) throws ServletException, IOException {
        String token = extractToken(req);
        if (token == null) {
            chain.doFilter(req, resp);
            return;
        }

        // ① 黑名单检查
        if (blacklistService.isBlacklisted(token)) {
            chain.doFilter(req, resp);
            return;
        }

        try {
            Claims claims = jwtUtil.parse(token);
            String username = claims.get("username", String.class);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                UsernamePasswordAuthenticationToken auth =
                        new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        } catch (ExpiredJwtException e) {
            log.debug("Token 已过期");
        } catch (JwtException e) {
            log.warn("Token 解析失败: {}", e.getMessage());
        }

        chain.doFilter(req, resp);
    }

    private String extractToken(HttpServletRequest req) {
        String bearer = req.getHeader("Authorization");
        if (bearer != null && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

九、UserDetailsService

java
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserMapper userMapper;
    private final RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        List<GrantedAuthority> authorities = roleMapper.selectByUserId(user.getId()).stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getCode()))
                .collect(Collectors.toList());

        // 加上按钮权限
        Set<String> permissions = permissionService.getUserPermissions(user.getId());
        permissions.forEach(p -> authorities.add(new SimpleGrantedAuthority(p)));

        CustomUserDetails details = new CustomUserDetails(
                user.getId(), user.getUsername(), user.getPassword(),
                user.getStatus() == 1, authorities);

        return details;
    }
}

十、本章小结

要点关键
模块system 模块(controller / service / mapper / dto / vo)
认证Spring Security + JWT
双 TokenAccess(2h)+ Refresh(30d)轮换
权限注解@RequiresPermission("user:create")
密码BCrypt 加密
关键操作重置密码、分配角色、删除用户

动手练习

练习 1:基础题

实现登录接口:用 admin / admin123 登录成功,返回 JWT。

练习 2:进阶题

实现双 Token 机制:登录返回 access + refresh,refresh 接口能生成新 token。

练习 3:思考题

你的用户表有 100 万行,登录查询 WHERE username=? 很慢。如何优化?


下一章第 87 章:角色与权限模块

本站基于 VitePress 构建 · 由 Codebook 团队维护