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:
@@ -73,6 +73,7 @@ public interface UserMapper extends BaseMapper<User> {
|
|||||||
@Result(id = true, property = "id", column = "id"),
|
@Result(id = true, property = "id", column = "id"),
|
||||||
@Result(property = "username", column = "username"),
|
@Result(property = "username", column = "username"),
|
||||||
@Result(property = "nickname", column = "nickname"),
|
@Result(property = "nickname", column = "nickname"),
|
||||||
|
@Result(property = "avatar", column = "avatar"),
|
||||||
@Result(property = "roles", column = "id",
|
@Result(property = "roles", column = "id",
|
||||||
many = @Many(select = "findRolesByUserId"))
|
many = @Many(select = "findRolesByUserId"))
|
||||||
})
|
})
|
||||||
@@ -96,6 +97,7 @@ public interface UserMapper extends BaseMapper<User> {
|
|||||||
@Result(id = true, property = "id", column = "id"),
|
@Result(id = true, property = "id", column = "id"),
|
||||||
@Result(property = "username", column = "username"),
|
@Result(property = "username", column = "username"),
|
||||||
@Result(property = "nickname", column = "nickname"),
|
@Result(property = "nickname", column = "nickname"),
|
||||||
|
@Result(property = "avatar", column = "avatar"),
|
||||||
@Result(property = "roles", column = "id",
|
@Result(property = "roles", column = "id",
|
||||||
|
|
||||||
many = @Many(select = "findRolesByUserId"))
|
many = @Many(select = "findRolesByUserId"))
|
||||||
@@ -123,6 +125,7 @@ public interface UserMapper extends BaseMapper<User> {
|
|||||||
@Result(id = true, property = "id", column = "id"),
|
@Result(id = true, property = "id", column = "id"),
|
||||||
@Result(property = "username", column = "username"),
|
@Result(property = "username", column = "username"),
|
||||||
@Result(property = "nickname", column = "nickname"),
|
@Result(property = "nickname", column = "nickname"),
|
||||||
|
@Result(property = "avatar", column = "avatar"),
|
||||||
@Result(property = "roles", column = "id",
|
@Result(property = "roles", column = "id",
|
||||||
many = @Many(select = "findRolesByUserId"))
|
many = @Many(select = "findRolesByUserId"))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ public class User {
|
|||||||
*/
|
*/
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态 (1:启用, 0:禁用)
|
* 状态 (1:启用, 0:禁用)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ import lombok.Data;
|
|||||||
public class AuthTokenResponse {
|
public class AuthTokenResponse {
|
||||||
private String tokenHead;
|
private String tokenHead;
|
||||||
private String token;
|
private String token;
|
||||||
private UserProfileResponse userProfile;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.dto.response.UserProfileResponse;
|
||||||
import cn.nopj.chaos_api.mapper.RoleMapper;
|
import cn.nopj.chaos_api.mapper.RoleMapper;
|
||||||
import cn.nopj.chaos_api.mapper.UserMapper;
|
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.service.AuthService;
|
||||||
import cn.nopj.chaos_api.util.JwtTokenUtil;
|
import cn.nopj.chaos_api.util.JwtTokenUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -71,28 +72,24 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
Authentication authentication = authenticationManager.authenticate(
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
new UsernamePasswordAuthenticationToken(username, password)
|
new UsernamePasswordAuthenticationToken(username, password)
|
||||||
);
|
);
|
||||||
|
log.info("用户登录成功: {}", username);
|
||||||
// 将认证结果保存在 SecurityContextHolder 中
|
// 将认证结果保存在 SecurityContextHolder 中
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
CustomUserDetails userDetails = (CustomUserDetails)authentication.getPrincipal();
|
||||||
// 获取用户详情
|
// 获取用户详情
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
|
||||||
if (!userDetails.isEnabled()) {
|
if (!userDetails.isEnabled()) {
|
||||||
throw new BizException(ErrorCode.USER_NOT_ENABLED);
|
throw new BizException(ErrorCode.USER_NOT_ENABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userMapper.findUserWithRolesByUsername(username);
|
|
||||||
UserProfileResponse userProfileResponse = new UserProfileResponse(user);
|
|
||||||
|
|
||||||
AuthTokenResponse res = new AuthTokenResponse();
|
AuthTokenResponse res = new AuthTokenResponse();
|
||||||
res.setToken(jwtTokenUtil.generateToken(userDetails));
|
res.setToken(jwtTokenUtil.generateToken(userDetails));
|
||||||
res.setTokenHead(tokenHead);
|
res.setTokenHead(tokenHead);
|
||||||
res.setUserProfile(userProfileResponse);
|
|
||||||
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
}catch (BadCredentialsException | InternalAuthenticationServiceException e) {
|
}catch (BadCredentialsException | InternalAuthenticationServiceException e) {
|
||||||
|
log.error("用户登录失败",e);
|
||||||
throw new BizException(ErrorCode.USER_NOT_EXISTS_OR_PASSWORD_WRONG);
|
throw new BizException(ErrorCode.USER_NOT_EXISTS_OR_PASSWORD_WRONG);
|
||||||
} catch (DisabledException e) {
|
} catch (DisabledException e) {
|
||||||
throw new BizException(ErrorCode.USER_NOT_ENABLED);
|
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.common.exceotion.BizException;
|
||||||
import cn.nopj.chaos_api.domain.entity.User;
|
import cn.nopj.chaos_api.domain.entity.User;
|
||||||
import cn.nopj.chaos_api.mapper.UserMapper;
|
import cn.nopj.chaos_api.mapper.UserMapper;
|
||||||
|
import cn.nopj.chaos_api.security.CustomUserDetails;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -23,32 +24,13 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
UserMapper userMapper;
|
UserMapper userMapper;
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) {
|
public CustomUserDetails loadUserByUsername(String username) {
|
||||||
// 1. 从数据库查询用户
|
// 1. 从数据库查询用户
|
||||||
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
|
User user = userMapper.findUserWithRolesByUsername(username);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new BizException(ErrorCode.USER_NOT_EXISTS);
|
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 对象
|
// 4. 构建并返回 Spring Security 的 User 对象
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new CustomUserDetails(user);
|
||||||
user.getUsername(),
|
|
||||||
user.getPassword(),
|
|
||||||
user.getEnabled(),
|
|
||||||
user.getAccountNonExpired(),
|
|
||||||
user.getCredentialsNonExpired(),
|
|
||||||
user.getAccountNonLocked(),
|
|
||||||
grantedAuthorities
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package cn.nopj.chaos_api.util;
|
package cn.nopj.chaos_api.util;
|
||||||
|
|
||||||
|
import cn.nopj.chaos_api.security.CustomUserDetails;
|
||||||
import com.auth0.jwt.JWT;
|
import com.auth0.jwt.JWT;
|
||||||
import com.auth0.jwt.JWTVerifier;
|
import com.auth0.jwt.JWTVerifier;
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
@@ -67,7 +68,7 @@ public class JwtTokenUtil {
|
|||||||
/**
|
/**
|
||||||
* 生成 Token
|
* 生成 Token
|
||||||
*/
|
*/
|
||||||
public String generateToken(UserDetails userDetails) {
|
public String generateToken(CustomUserDetails userDetails) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
Date expiryDate = new Date(now.getTime() + expiration * 1000);
|
Date expiryDate = new Date(now.getTime() + expiration * 1000);
|
||||||
|
|
||||||
@@ -82,6 +83,9 @@ public class JwtTokenUtil {
|
|||||||
.withIssuedAt(now)
|
.withIssuedAt(now)
|
||||||
.withExpiresAt(expiryDate)
|
.withExpiresAt(expiryDate)
|
||||||
.withClaim("authorities", authorities)
|
.withClaim("authorities", authorities)
|
||||||
|
.withClaim("userId", userDetails.getUserId())
|
||||||
|
.withClaim("nickname", userDetails.getNickname())
|
||||||
|
.withClaim("avatar", userDetails.getAvatar())
|
||||||
.sign(algorithm);
|
.sign(algorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user