From 2a94f493e66e6402ed5ee61302b01c9d4bb341c2 Mon Sep 17 00:00:00 2001 From: Chaos Date: Tue, 25 Nov 2025 16:53:43 +0800 Subject: [PATCH] feat(auth): implement custom user details and enhance JWT token payload - Introduce CustomUserDetails to encapsulate user information and authorities - Remove redundant UserProfileResponse from AuthTokenResponse - Add userId, nickname, and avatar claims to JWT token generation - Update UserDetailsServiceImpl to use CustomUserDetails - Modify JwtTokenUtil to accept CustomUserDetails and include additional claims - Add avatar field to User entity - Update UserMapper to map avatar field in result mappings - Add logging for successful and failed login attempts --- .../cn/nopj/chaos_api/mapper/UserMapper.java | 3 + .../cn/nopj/chaos_api/domain/entity/User.java | 5 + .../dto/response/AuthTokenResponse.java | 1 - .../chaos_api/security/CustomUserDetails.java | 96 +++++++++++++++++++ .../service/impl/AuthServiceImpl.java | 11 +-- .../service/impl/UserDetailsServiceImpl.java | 26 +---- .../cn/nopj/chaos_api/util/JwtTokenUtil.java | 6 +- 7 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 chaos_api_web/src/main/java/cn/nopj/chaos_api/security/CustomUserDetails.java diff --git a/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java b/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java index 4e4ac09..3061a89 100644 --- a/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java +++ b/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java @@ -73,6 +73,7 @@ public interface UserMapper extends BaseMapper { @Result(id = true, property = "id", column = "id"), @Result(property = "username", column = "username"), @Result(property = "nickname", column = "nickname"), + @Result(property = "avatar", column = "avatar"), @Result(property = "roles", column = "id", many = @Many(select = "findRolesByUserId")) }) @@ -96,6 +97,7 @@ public interface UserMapper extends BaseMapper { @Result(id = true, property = "id", column = "id"), @Result(property = "username", column = "username"), @Result(property = "nickname", column = "nickname"), + @Result(property = "avatar", column = "avatar"), @Result(property = "roles", column = "id", many = @Many(select = "findRolesByUserId")) @@ -123,6 +125,7 @@ public interface UserMapper extends BaseMapper { @Result(id = true, property = "id", column = "id"), @Result(property = "username", column = "username"), @Result(property = "nickname", column = "nickname"), + @Result(property = "avatar", column = "avatar"), @Result(property = "roles", column = "id", many = @Many(select = "findRolesByUserId")) }) diff --git a/chaos_api_domain/src/main/java/cn/nopj/chaos_api/domain/entity/User.java b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/domain/entity/User.java index 94c81c8..747d6d8 100644 --- a/chaos_api_domain/src/main/java/cn/nopj/chaos_api/domain/entity/User.java +++ b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/domain/entity/User.java @@ -40,6 +40,11 @@ public class User { */ private String nickname; + /** + * 头像 + */ + private String avatar; + /** * 状态 (1:启用, 0:禁用) */ diff --git a/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/response/AuthTokenResponse.java b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/response/AuthTokenResponse.java index 5f3d864..d25fd2f 100644 --- a/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/response/AuthTokenResponse.java +++ b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/response/AuthTokenResponse.java @@ -6,5 +6,4 @@ import lombok.Data; public class AuthTokenResponse { private String tokenHead; private String token; - private UserProfileResponse userProfile; } diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/security/CustomUserDetails.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/security/CustomUserDetails.java new file mode 100644 index 0000000..b933d48 --- /dev/null +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/security/CustomUserDetails.java @@ -0,0 +1,96 @@ +package cn.nopj.chaos_api.security; + +import cn.nopj.chaos_api.domain.entity.Role; +import cn.nopj.chaos_api.domain.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +public class CustomUserDetails implements UserDetails { + private final User user; + private final Collection authorities; + public CustomUserDetails(User user){ + this.user = user; + List roles = user.getRoles(); + this.authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role.getCode())).toList(); + } + + + /** + * 获取该用户的权限集合 + * Spring Security 会根据这些权限进行鉴权(如 @PreAuthorize) + */ + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + /** + * 账户是否未过期 + * 返回 true 表示未过期,false 表示已过期(无法登录) + */ + @Override + public boolean isAccountNonExpired() { + return user.getAccountNonExpired(); // 这里可以根据业务逻辑修改,例如判断 expire_time + } + + /** + * 账户是否未锁定 + * 返回 true 表示未锁定 + */ + @Override + public boolean isAccountNonLocked() { + return user.getAccountNonLocked(); // 可根据由登录失败次数判断锁定逻辑 + } + + /** + * 凭证(密码)是否未过期 + * 返回 true 表示未过期 + */ + @Override + public boolean isCredentialsNonExpired() { + return user.getCredentialsNonExpired(); // 可根据密码最后修改时间判断 + } + + /** + * 账户是否可用 + * 通常对应数据库中的 status 或 enabled 字段 + */ + @Override + public boolean isEnabled() { + return user.getEnabled(); + } + /** + * 获取用户ID + */ + public Long getUserId() { + return user.getId(); + } + + /** + * 获取用户昵称 + */ + public String getNickname() { + return user.getNickname(); + } + + /** + * 获取用户头像 + */ + + public String getAvatar() { + return user.getAvatar(); + } +} diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java index d7ebdaa..44dc67f 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java @@ -9,6 +9,7 @@ import cn.nopj.chaos_api.dto.response.AuthTokenResponse; import cn.nopj.chaos_api.dto.response.UserProfileResponse; import cn.nopj.chaos_api.mapper.RoleMapper; import cn.nopj.chaos_api.mapper.UserMapper; +import cn.nopj.chaos_api.security.CustomUserDetails; import cn.nopj.chaos_api.service.AuthService; import cn.nopj.chaos_api.util.JwtTokenUtil; import lombok.extern.slf4j.Slf4j; @@ -71,28 +72,24 @@ public class AuthServiceImpl implements AuthService { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password) ); - + log.info("用户登录成功: {}", username); // 将认证结果保存在 SecurityContextHolder 中 SecurityContextHolder.getContext().setAuthentication(authentication); + CustomUserDetails userDetails = (CustomUserDetails)authentication.getPrincipal(); // 获取用户详情 - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); if (!userDetails.isEnabled()) { throw new BizException(ErrorCode.USER_NOT_ENABLED); } - User user = userMapper.findUserWithRolesByUsername(username); - UserProfileResponse userProfileResponse = new UserProfileResponse(user); AuthTokenResponse res = new AuthTokenResponse(); res.setToken(jwtTokenUtil.generateToken(userDetails)); res.setTokenHead(tokenHead); - res.setUserProfile(userProfileResponse); - - return res; }catch (BadCredentialsException | InternalAuthenticationServiceException e) { + log.error("用户登录失败",e); throw new BizException(ErrorCode.USER_NOT_EXISTS_OR_PASSWORD_WRONG); } catch (DisabledException e) { throw new BizException(ErrorCode.USER_NOT_ENABLED); diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/UserDetailsServiceImpl.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/UserDetailsServiceImpl.java index c7ab58d..4d4deca 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/UserDetailsServiceImpl.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/UserDetailsServiceImpl.java @@ -4,6 +4,7 @@ import cn.nopj.chaos_api.common.constants.ErrorCode; import cn.nopj.chaos_api.common.exceotion.BizException; import cn.nopj.chaos_api.domain.entity.User; import cn.nopj.chaos_api.mapper.UserMapper; +import cn.nopj.chaos_api.security.CustomUserDetails; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -23,32 +24,13 @@ public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserMapper userMapper; @Override - public UserDetails loadUserByUsername(String username) { + public CustomUserDetails loadUserByUsername(String username) { // 1. 从数据库查询用户 - User user = userMapper.selectOne(new QueryWrapper().eq("username", username)); + User user = userMapper.findUserWithRolesByUsername(username); if (user == null) { throw new BizException(ErrorCode.USER_NOT_EXISTS); } - - // 2. 查询该用户的权限信息 (角色 + 权限) - List authorities = userMapper.findAuthoritiesByUsername(username); - - log.info("用户权限列表: {}", authorities); - // 3. 将权限字符串列表转换为 GrantedAuthority 集合 - List grantedAuthorities = authorities.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - - // 4. 构建并返回 Spring Security 的 User 对象 - return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), - user.getEnabled(), - user.getAccountNonExpired(), - user.getCredentialsNonExpired(), - user.getAccountNonLocked(), - grantedAuthorities - ); + return new CustomUserDetails(user); } } diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java index 6f89c3d..f9a5511 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java @@ -1,5 +1,6 @@ package cn.nopj.chaos_api.util; +import cn.nopj.chaos_api.security.CustomUserDetails; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; @@ -67,7 +68,7 @@ public class JwtTokenUtil { /** * 生成 Token */ - public String generateToken(UserDetails userDetails) { + public String generateToken(CustomUserDetails userDetails) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration * 1000); @@ -82,6 +83,9 @@ public class JwtTokenUtil { .withIssuedAt(now) .withExpiresAt(expiryDate) .withClaim("authorities", authorities) + .withClaim("userId", userDetails.getUserId()) + .withClaim("nickname", userDetails.getNickname()) + .withClaim("avatar", userDetails.getAvatar()) .sign(algorithm); }