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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user