第 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 |
| 双 Token | Access(2h)+ Refresh(30d)轮换 |
| 权限注解 | @RequiresPermission("user:create") |
| 密码 | BCrypt 加密 |
| 关键操作 | 重置密码、分配角色、删除用户 |
动手练习
练习 1:基础题
实现登录接口:用 admin / admin123 登录成功,返回 JWT。
练习 2:进阶题
实现双 Token 机制:登录返回 access + refresh,refresh 接口能生成新 token。
练习 3:思考题
你的用户表有 100 万行,登录查询 WHERE username=? 很慢。如何优化?
下一章:第 87 章:角色与权限模块 →