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
This commit is contained in:
Chaos
2025-11-25 16:53:43 +08:00
parent a5a23a6b52
commit 2a94f493e6
7 changed files with 117 additions and 31 deletions

View File

@@ -73,6 +73,7 @@ public interface UserMapper extends BaseMapper<User> {
@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<User> {
@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<User> {
@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"))
})

View File

@@ -40,6 +40,11 @@ public class User {
*/
private String nickname;
/**
* 头像
*/
private String avatar;
/**
* 状态 (1:启用, 0:禁用)
*/

View File

@@ -6,5 +6,4 @@ import lombok.Data;
public class AuthTokenResponse {
private String tokenHead;
private String token;
private UserProfileResponse userProfile;
}

View File

@@ -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<? extends GrantedAuthority> authorities;
public CustomUserDetails(User user){
this.user = user;
List<Role> roles = user.getRoles();
this.authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role.getCode())).toList();
}
/**
* 获取该用户的权限集合
* Spring Security 会根据这些权限进行鉴权(如 @PreAuthorize
*/
@Override
public Collection<? extends GrantedAuthority> 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();
}
}

View File

@@ -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);

View File

@@ -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<User>().eq("username", username));
User user = userMapper.findUserWithRolesByUsername(username);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
// 2. 查询该用户的权限信息 (角色 + 权限)
List<String> authorities = userMapper.findAuthoritiesByUsername(username);
log.info("用户权限列表: {}", authorities);
// 3. 将权限字符串列表转换为 GrantedAuthority 集合
List<GrantedAuthority> 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);
}
}

View File

@@ -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);
}