书接上文,我们刚实现了一个简单的登录功能,那么我们要如何才能拿到登录用户的具体信息呢?我进行了一下整理
"登录用户的信息" 是保存在 SecurityContext 里的,可以通过多种方式获取这个用户的信息,下面是最常用的方式:
在 Controller 里用 @AuthenticationPrincipal
java
@GetMapping("/me")
public String getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return "当前登录用户:" + userDetails.getUsername();
}
这个方式适用于:你使用了 Spring Security 默认的 UserDetailsService 或者自定义了用户实体实现了 UserDetails 接口。
SecurityContextHolder.getContext().getAuthentication()
java
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails userDetails) {
System.out.println("用户名:" + userDetails.getUsername());
}
这个是 最底层、最万能的方式,可以在任何地方(包括 Service 层)使用。
用 Spring 注入 Principal 对象(Controller 中)
java
@GetMapping("/me")
public String getCurrentUser(Principal principal) {
return "当前登录用户:" + principal.getName();
}
同样的,这个方式也适用于:你使用了 Spring Security 默认的 UserDetailsService 或者自定义了用户实体实现了 UserDetails 接口。
用 HttpServletRequest.getUserPrincipal()
java
@GetMapping("/me")
public String getUser(HttpServletRequest request) {
Principal principal = request.getUserPrincipal();
return principal.getName();
}
这个也常见于过滤器或 Servlet 场景。
自定义实现UserDetails接口
假设我们已经有了一个实体类TUser,如下:
java
package org.pp.springsecurity.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("t_user")
public class TUser implements Serializable {
private Integer id;
private String loginAct;
private String loginPwd;
private String name;
private String phone;
private String email;
private LocalDateTime createTime;
private Integer createBy;
private LocalDateTime editTime;
private Integer editBy;
private LocalDateTime lastLoginTime;
}
此时我们有两个选择:
- 直接让TUser实现UserDetails接口
- 额外写一个LoginUser实现UserDetails,并包装TUser
GPT推荐的是后者,原因如下:
- 职责分离(Separation of Concerns)
- TUser 是数据库实体类,只关注"数据结构"和"持久化";
- LoginUser 是安全上下文中的用户视图,关注"登录逻辑"和"权限校验"。
👉 如果你让 TUser 实现 UserDetails,就把认证逻辑和数据库模型耦合在了一起,违背了单一职责原则(SRP)。
- 避免污染实体类
UserDetails 接口有七八个方法(用户名、密码、权限、状态等),可能不是你 TUser 表实际需要的字段;
将这些方法塞进实体类里,会让 TUser 看起来非常臃肿、难维护。
- 增强灵活性,方便扩展
LoginUser 可以自由扩展,比如你想额外在 LoginUser 里放 JWT Token、IP 地址、登录时间等字段;
而 TUser 是实体类,不能随便乱加跟表无关的字段,否则容易导致 MyBatis 查询异常或字段映射错误。
- 便于做权限模型嵌套
假设以后你设计 RBAC 权限系统,LoginUser 可以包装更多内容:
java
public class LoginUser implements UserDetails {
private TUser user;
private List<String> roles;
private List<String> permissions;
}
如果用 TUser 本身,结构就固定死了,扩展权限字段很不优雅。
- 跟框架使用方式一致
Spring Security + MyBatis Plus 实战项目里,绝大多数工程师都选择使用封装类(比如 LoginUser),而不是直接污染实体类。你在企业代码里也会常见:
java
public class JwtUser implements UserDetails {
private final TUser user;
}
ok最终来实现一下LoginUser:
java
package org.pp.springsecurity.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
@Getter
@AllArgsConstructor
public class LoginUser implements UserDetails {
private final TUser user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 你可以从 user 表中读取权限字段并转换为 Authority,这里先返回空集合
return Collections.emptyList();
}
@Override
public String getPassword() {
return user.getLoginPwd();
}
@Override
public String getUsername() {
return user.getLoginAct();
}
@Override
public boolean isAccountNonExpired() {
return user.getAccountNoExpired() == 1;
}
@Override
public boolean isAccountNonLocked() {
return user.getAccountNoLocked() == 1;
}
@Override
public boolean isCredentialsNonExpired() {
return user.getCredentialsNoExpired() == 1;
}
@Override
public boolean isEnabled() {
return user.getAccountEnabled() == 1;
}
}
然后就可以通过以下方式,使得返回给security框架的UserDetails是我们自己定义的,也就是包含登录用户信息的对象。现在你在用前面提到的四种方式去处理获取数据就行。
java
package org.pp.springsecurity.security;
import org.pp.springsecurity.entity.TUser;
import org.pp.springsecurity.service.TUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private TUserService tUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser tUser = tUserService.lambdaQuery()
.eq(TUser::getLoginAct, username)
.one();
if (tUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new LoginUser(tUser);
}
}
什么????你问我数据在哪???
你都获取到LoginUser了,它调用.getTUser不久获取到了对应数据库实体类的对象!!!
这里我们还是来讲一下吧
在controller层的方式:
java
@GetMapping("/me")
public TUser getLoginUser(@AuthenticationPrincipal LoginUser loginUser) {
return loginUser.getUser();
}
在其他层的方式
java
@GetMapping("/me")
public TUser getLoginUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof LoginUser loginUser) {
return loginUser.getUser(); // LoginUser 包装了 TUser,你可以自定义这个字段
}
throw new RuntimeException("未登录");
}
你用principle的方式也可以
java
@GetMapping("/me")
public String getCurrentUser(Principal principal) {
if (principal instanceof LoginUser loginUser) {
return "当前登录用户:" + loginUser.getTUser().getName();
}
return "未知用户";
}