/* * * Copyright (c) 2018-2025, hnqz All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * Neither the name of the pig4cloud.com developer nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * Author: hnqz * */ package com.qunzhixinxi.hnqz.auth.endpoint; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.qunzhixinxi.hnqz.common.core.constant.CacheConstants; import com.qunzhixinxi.hnqz.common.core.constant.PaginationConstants; import com.qunzhixinxi.hnqz.common.core.constant.SecurityConstants; import com.qunzhixinxi.hnqz.common.core.util.R; import com.qunzhixinxi.hnqz.common.data.tenant.TenantContextHolder; import com.qunzhixinxi.hnqz.common.security.annotation.Inner; import com.qunzhixinxi.hnqz.common.security.util.SecurityUtils; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.CacheManager; import org.springframework.data.redis.core.ConvertingCursor; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author hnqz * @date 2018/6/24 删除token端点 */ @Slf4j @RestController @AllArgsConstructor @RequestMapping("/token") public class HnqzTokenEndpoint { private static final String PIGX_OAUTH_ACCESS = SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "auth_to_access:"; private final ClientDetailsService clientDetailsService; private final RedisTemplate redisTemplate; private final TokenStore tokenStore; private final CacheManager cacheManager; /** * 认证页面 * @param modelAndView * @param error 表单登录失败处理回调的错误信息 * @return ModelAndView */ @GetMapping("/login") public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) { modelAndView.setViewName("ftl/login"); modelAndView.addObject("error", error); return modelAndView; } /** * 确认授权页面 * @param request * @param session * @param modelAndView * @return */ @GetMapping("/confirm_access") public ModelAndView confirm(HttpServletRequest request, HttpSession session, ModelAndView modelAndView) { Map scopeList = (Map) request.getAttribute("scopes"); modelAndView.addObject("scopeList", scopeList.keySet()); Object auth = session.getAttribute("authorizationRequest"); if (auth != null) { AuthorizationRequest authorizationRequest = (AuthorizationRequest) auth; ClientDetails clientDetails = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId()); modelAndView.addObject("app", clientDetails.getAdditionalInformation()); modelAndView.addObject("user", SecurityUtils.getUser()); } modelAndView.setViewName("ftl/confirm"); return modelAndView; } /** * 退出token * @param authHeader Authorization */ @DeleteMapping("/logout") public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) { if (StrUtil.isBlank(authHeader)) { return R.ok(Boolean.FALSE, "退出失败,token 为空"); } String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim(); return delToken(tokenValue); } /** * 令牌管理调用 * @param token token * @return */ @Inner @DeleteMapping("/{token}") public R delToken(@PathVariable("token") String token) { OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) { return R.ok(Boolean.TRUE, "退出失败,token 无效"); } OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken); // 清空用户信息 cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName()); // 清空access token tokenStore.removeAccessToken(accessToken); // 清空 refresh token OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); tokenStore.removeRefreshToken(refreshToken); return R.ok(); } /** * 查询token * @param params 分页参数 * @return */ @Inner @PostMapping("/page") public R tokenList(@RequestBody Map params) { // 根据分页参数获取对应数据 String key = String.format("%s*:%s", PIGX_OAUTH_ACCESS, TenantContextHolder.getTenantId()); List pages = findKeysForPage(key, MapUtil.getInt(params, PaginationConstants.CURRENT), MapUtil.getInt(params, PaginationConstants.SIZE)); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); Page result = new Page(MapUtil.getInt(params, PaginationConstants.CURRENT), MapUtil.getInt(params, PaginationConstants.SIZE)); result.setRecords(redisTemplate.opsForValue().multiGet(pages)); result.setTotal(redisTemplate.keys(key).size()); return R.ok(result); } private List findKeysForPage(String patternKey, int pageNum, int pageSize) { ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build(); RedisSerializer redisSerializer = (RedisSerializer) redisTemplate.getKeySerializer(); Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection( redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize)); List result = new ArrayList<>(); int tmpIndex = 0; int startIndex = (pageNum - 1) * pageSize; int end = pageNum * pageSize; assert cursor != null; while (cursor.hasNext()) { if (tmpIndex >= startIndex && tmpIndex < end) { result.add(cursor.next().toString()); tmpIndex++; continue; } if (tmpIndex >= end) { break; } tmpIndex++; cursor.next(); } try { cursor.close(); } catch (IOException e) { log.error("关闭cursor 失败"); } return result; } }