Smart-SSO单点登录接入后,如何优雅地获取用户信息和权限?实战代码示例
Smart-SSO单点登录接入后如何优雅地获取用户信息和权限?实战代码示例
当你已经完成了Smart-SSO的基础接入,接下来面临的实际问题可能是:如何在业务代码中高效地获取用户信息?如何基于权限数据实现动态菜单和接口控制?本文将深入探讨这些实战场景,提供可直接复用的代码方案。
1. 理解Smart-SSO的核心上下文
Smart-SSO在用户登录后,会在请求上下文中注入两个关键对象:
- TokenUser:包含用户基础信息(如username、id等)
- TokenPermission:包含权限数据(如菜单列表、权限集合等)
这两个对象通过ClientContextHolder工具类获取,这是整个权限体系的核心入口。理解它们的结构是后续开发的基础:
// 典型TokenUser结构示例 public class TokenUser { private String username; private Long id; private String email; // 其他用户属性... } // 典型TokenPermission结构示例 public class TokenPermission { private List<Menu> menuList; // 菜单树 private Set<String> permissionSet; // 权限标识集合 // 其他权限属性... }2. 后端实战:从Controller到权限控制
2.1 基础用户信息获取
在Spring MVC的Controller中,最直接的方式是通过HttpServletRequest获取用户上下文:
@RestController @RequestMapping("/api/user") public class UserController { @GetMapping("/profile") public ResponseEntity<UserProfile> getProfile(HttpServletRequest request) { TokenUser user = ClientContextHolder.getUser(request); // 构建响应对象 UserProfile profile = new UserProfile(); profile.setUsername(user.getUsername()); profile.setUserId(user.getId()); return ResponseEntity.ok(profile); } }2.2 权限校验的三种实现方式
根据不同的业务场景,权限校验可以有多种实现路径:
方式一:编程式校验
@GetMapping("/admin/report") public ResponseEntity<ReportData> getAdminReport(HttpServletRequest request) { TokenPermission permission = ClientContextHolder.getPermission(request); if (!permission.getPermissionSet().contains("report:admin")) { throw new AccessDeniedException("无权访问管理员报表"); } // 业务逻辑... }方式二:注解式校验(结合Spring Security)
@PreAuthorize("hasPermission(#request, 'report:admin')") @GetMapping("/admin/report") public ResponseEntity<ReportData> getAdminReport(HttpServletRequest request) { // 业务逻辑... }需要先配置自定义的权限表达式处理器:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { @Override protected MethodSecurityExpressionHandler createExpressionHandler() { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); handler.setPermissionEvaluator(new SmartSsoPermissionEvaluator()); return handler; } }方式三:动态数据过滤
对于数据敏感的查询接口,可以在Service层实现数据过滤:
public List<Order> getUserOrders(HttpServletRequest request) { TokenUser user = ClientContextHolder.getUser(request); return orderRepository.findByUserId(user.getId()); }3. 前端集成方案
3.1 用户信息在前端的存储与使用
建议在登录成功后,将必要的用户信息存储在前端状态管理中:
// Vue示例 - 登录成功后 login().then(response => { const userInfo = response.data store.commit('SET_USER', { id: userInfo.id, name: userInfo.username, // 其他需要前端使用的字段 }) })3.2 动态菜单的实现
从后端获取菜单数据后,可以动态生成路由配置:
// React示例 - 动态路由 const App = () => { const [menu, setMenu] = useState([]) useEffect(() => { fetchMenu().then(data => { const routes = transformMenuToRoutes(data) setMenu(routes) }) }, []) return ( <Router> <Layout> <Sidebar menus={menu} /> <Switch> {menu.map(route => ( <Route key={route.path} {...route} /> ))} </Switch> </Layout> </Router> ) }菜单数据结构转换示例:
function transformMenuToRoutes(menus) { return menus.map(menu => ({ path: menu.path, component: lazy(() => import(`@/views/${menu.component}`)), meta: { title: menu.title, icon: menu.icon } })) }4. 高级应用场景
4.1 微服务间的用户上下文传递
在微服务架构中,需要将用户信息通过请求头传递给下游服务:
// 使用Feign Client的拦截器 public class UserInfoFeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { HttpServletRequest request = getCurrentRequest(); if (request != null) { TokenUser user = ClientContextHolder.getUser(request); if (user != null) { template.header("X-User-Id", user.getId().toString()); template.header("X-Username", user.getUsername()); } } } }下游服务可以通过过滤器还原用户上下文:
public class UserInfoFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String userId = request.getHeader("X-User-Id"); String username = request.getHeader("X-Username"); if (userId != null) { TokenUser user = new TokenUser(); user.setId(Long.parseLong(userId)); user.setUsername(username); ClientContextHolder.setUser(user); } chain.doFilter(request, response); } }4.2 权限数据的缓存优化
频繁从SSO服务端获取权限数据可能影响性能,可以考虑本地缓存:
@Aspect @Component public class PermissionCacheAspect { @Autowired private CacheManager cacheManager; @Around("execution(* ClientContextHolder.getPermission(..))") public Object cachePermission(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = (HttpServletRequest) joinPoint.getArgs()[0]; String cacheKey = buildCacheKey(request); Cache cache = cacheManager.getCache("permissionCache"); TokenPermission permission = cache.get(cacheKey, TokenPermission.class); if (permission == null) { permission = (TokenPermission) joinPoint.proceed(); cache.put(cacheKey, permission); } return permission; } private String buildCacheKey(HttpServletRequest request) { TokenUser user = ClientContextHolder.getUser(request); return "perm:" + user.getId(); } }5. 安全最佳实践
在获取和使用用户信息时,需要注意以下安全要点:
敏感信息过滤:返回给前端的用户信息应该经过过滤
// 不返回完整用户对象 public UserProfile safeUserProfile(TokenUser user) { UserProfile profile = new UserProfile(); profile.setUsername(user.getUsername()); // 不包含password、email等敏感字段 return profile; }权限变更的实时性:考虑权限变更时的缓存失效策略
@EventListener public void handlePermissionChange(PermissionChangeEvent event) { cacheManager.getCache("permissionCache").evict("perm:" + event.getUserId()); }接口防越权:即使前端隐藏了无权限的菜单,后端也必须二次校验
@PreAuthorize("hasPermission(#request, 'order:query')") @GetMapping("/orders") public List<Order> queryOrders(OrderQuery query, HttpServletRequest request) { // 必须添加方法级权限校验 }日志审计:关键操作应记录操作人信息
@Aspect @Component public class OperationLogAspect { @AfterReturning("execution(* com..service.*.*(..)) && @annotation(loggable)") public void logOperation(JoinPoint joinPoint, Loggable loggable) { HttpServletRequest request = getCurrentRequest(); TokenUser user = ClientContextHolder.getUser(request); // 记录操作日志,包含用户信息 } }
