|
|
@@ -0,0 +1,213 @@
|
|
|
+
|
|
|
+package com.lxq.system.biz.controller;
|
|
|
+
|
|
|
+import cn.hutool.core.date.DatePattern;
|
|
|
+import cn.hutool.core.date.TemporalAccessorUtil;
|
|
|
+import cn.hutool.core.map.MapUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.lxq.common.core.constant.CacheConstants;
|
|
|
+import com.lxq.common.core.constant.CommonConstants;
|
|
|
+import com.lxq.common.core.constant.enums.ErrorTypeEnum;
|
|
|
+import com.lxq.common.core.util.R;
|
|
|
+import com.lxq.common.core.util.SpringContextHolder;
|
|
|
+import com.lxq.common.log.annotation.SysLog;
|
|
|
+import com.lxq.common.security.util.OAuth2EndpointUtils;
|
|
|
+import com.lxq.common.security.util.OAuthClientException;
|
|
|
+import com.lxq.system.biz.model.SysOauthClientDetails;
|
|
|
+import com.lxq.system.biz.service.SysOauthClientDetailsService;
|
|
|
+import com.lxq.system.biz.domain.vo.TokenVo;
|
|
|
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|
|
+import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.SneakyThrows;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.cache.CacheManager;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
|
|
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
|
|
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+import org.springframework.web.servlet.ModelAndView;
|
|
|
+
|
|
|
+import java.security.Principal;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @author LXQ
|
|
|
+ * @date 2021/2/1 删除token端点
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+@RequiredArgsConstructor
|
|
|
+@RequestMapping("/token")
|
|
|
+@Tag(description = "token", name = "令牌管理模块")
|
|
|
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
|
|
+public class TokenController {
|
|
|
+
|
|
|
+ private final OAuth2AuthorizationService authorizationService;
|
|
|
+
|
|
|
+ private final SysOauthClientDetailsService clientDetailsService;
|
|
|
+
|
|
|
+ private final RedisTemplate<String, Object> redisTemplate;
|
|
|
+
|
|
|
+ private final CacheManager cacheManager;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 认证页面
|
|
|
+ *
|
|
|
+ * @param error 表单登录失败处理回调的错误信息
|
|
|
+ * @param modelAndView 模型和视图
|
|
|
+ * @return ModelAndView
|
|
|
+ */
|
|
|
+ @GetMapping("/login")
|
|
|
+ public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
|
|
|
+ modelAndView.setViewName("ftl/login");
|
|
|
+ modelAndView.addObject("error", error);
|
|
|
+ return modelAndView;
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/confirm_access")
|
|
|
+ public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
|
|
|
+ @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
|
|
|
+ @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
|
|
|
+ @RequestParam(OAuth2ParameterNames.STATE) String state) {
|
|
|
+
|
|
|
+ SysOauthClientDetails clientDetails = clientDetailsService.getClientDetailsById(clientId);
|
|
|
+ if (clientDetails == null) {
|
|
|
+ throw new OAuthClientException("clientId 不合法");
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
|
|
|
+ modelAndView.addObject("clientId", clientId);
|
|
|
+ modelAndView.addObject("state", state);
|
|
|
+ modelAndView.addObject("scopeList", authorizedScopes);
|
|
|
+ modelAndView.addObject("principalName", principal.getName());
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
|
|
|
+ return removeToken(new String[]{tokenValue});
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验token
|
|
|
+ * @param token 令牌
|
|
|
+ */
|
|
|
+ @SneakyThrows
|
|
|
+ @GetMapping("/checkToken")
|
|
|
+ public R<Object> checkToken(String token) {
|
|
|
+ if (StrUtil.isBlank(token)) {
|
|
|
+
|
|
|
+ return R.failed(ErrorTypeEnum.TOKEN_EMPTY);
|
|
|
+ }
|
|
|
+ OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
|
|
+
|
|
|
+ // 如果令牌不存在 返回401
|
|
|
+ if (authorization == null || authorization.getAccessToken() == null) {
|
|
|
+
|
|
|
+ return R.failed(ErrorTypeEnum.TOKEN_EMPTY);
|
|
|
+ }
|
|
|
+ Map<String, Object> claims = authorization.getAccessToken().getClaims();
|
|
|
+ OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization,
|
|
|
+ claims);
|
|
|
+
|
|
|
+ return R.ok(sendAccessTokenResponse.getAccessToken());
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除
|
|
|
+ * @param tokens tokens
|
|
|
+ * @return success/false
|
|
|
+ */
|
|
|
+ @SysLog("删除用户token")
|
|
|
+ @PostMapping("/remove")
|
|
|
+ @PreAuthorize("@pms.hasPermission('sys_token_del')")
|
|
|
+ public R removeToken(@RequestBody String[] tokens) {
|
|
|
+ for (String token : tokens) {
|
|
|
+ OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
|
|
+ if (authorization == null) {
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+
|
|
|
+ OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
|
|
|
+ if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+ // 清空用户信息(立即删除)
|
|
|
+ Objects.requireNonNull(cacheManager.getCache(CacheConstants.USER_DETAILS)).evictIfPresent(authorization.getPrincipalName());
|
|
|
+ // 清空access token
|
|
|
+ authorizationService.remove(authorization);
|
|
|
+ // 处理自定义退出事件,保存相关日志
|
|
|
+ SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
|
|
|
+ authorization.getPrincipalName(), authorization.getRegisteredClientId())));
|
|
|
+ }
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 令牌列表
|
|
|
+ *
|
|
|
+ * @param params 分页参数
|
|
|
+ * @return r<page>
|
|
|
+ */
|
|
|
+ @PostMapping("/page")
|
|
|
+ public R<Page> tokenList(@RequestBody Map<String, Object> params) {
|
|
|
+ // 根据分页参数获取对应数据
|
|
|
+ String key = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS);
|
|
|
+ int current = MapUtil.getInt(params, CommonConstants.CURRENT);
|
|
|
+ int size = MapUtil.getInt(params, CommonConstants.SIZE);
|
|
|
+ Set<String> keys = redisTemplate.keys(key);
|
|
|
+ assert keys != null;
|
|
|
+ List<String> pages = keys.stream().skip((long) (current - 1) * size).limit(size).collect(Collectors.toList());
|
|
|
+ Page result = new Page(current, size);
|
|
|
+
|
|
|
+ List<TokenVo> tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> {
|
|
|
+ OAuth2Authorization authorization = (OAuth2Authorization) obj;
|
|
|
+ TokenVo tokenVo = new TokenVo();
|
|
|
+ tokenVo.setClientId(authorization.getRegisteredClientId());
|
|
|
+ tokenVo.setId(authorization.getId());
|
|
|
+ tokenVo.setUsername(authorization.getPrincipalName());
|
|
|
+ OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
|
|
|
+ tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
|
|
|
+
|
|
|
+ String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
|
|
|
+ DatePattern.NORM_DATETIME_PATTERN);
|
|
|
+ tokenVo.setExpiresAt(expiresAt);
|
|
|
+
|
|
|
+ String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
|
|
|
+ DatePattern.NORM_DATETIME_PATTERN);
|
|
|
+ tokenVo.setIssuedAt(issuedAt);
|
|
|
+ return tokenVo;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ result.setRecords(tokenVoList);
|
|
|
+ result.setTotal(keys.size());
|
|
|
+ return R.ok(result);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|