springcloud+oauth2+sso单点登录+自定义获取用户token+第三方系统访问获取token

  1. springcloud+oauth2+sso单点登录实现
  2. 自定义获取某个用户token值和权限
  3. 第三方应用系统访问本应用系统接口获取指定用户名的token和权限

  • 指定用户名和权限定义
    package com.shpl.scp.fin.controller;
    
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.shpl.scp.admin.api.dto.UserTokenDTO;
    import com.shpl.scp.admin.api.feign.RemoteTokenService;
    import com.shpl.scp.common.core.util.R;
    import com.shpl.scp.common.security.annotation.Inner;
    import io.swagger.v3.oas.annotations.security.SecurityRequirement;
    import io.swagger.v3.oas.annotations.tags.Tag;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpHeaders;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    import java.util.Map;
    
    /**
     * 第三方系统Token接口
     *
     * @author Dionys
     * @date 2025-12-09
     */
    @Slf4j
    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/external/cloudpense")
    @Tag(description = "cloudcheckToken", name = "云检系统Token接口")
    @SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
    public class FinApiCloudPenseTokenController {
    
        private final RemoteTokenService remoteTokenService;
    
        @Inner(false)
        @RequestMapping("/token")
        public R<UserTokenDTO.Response> token() {
            // 创建请求对象
            UserTokenDTO.Request request = new UserTokenDTO.Request();
            request.setUsername("FinCloudAPI");  // 只需要提供用户名
            request.setAuthorities(List.of("ROLE_1", "fin_cloudpense")); // 设置用户角色(ROLE_ID 固定写法)权限字符串
    
            // 调用Token生成服务
            return remoteTokenService.generateToken(request);
        }
    
        @Inner(false)
        @RequestMapping("/tokenp")
        public R<Page> tokenp() {
            // 创建请求对象
            Map<String, Object> params = Map.of("current", 1, "size", 10);
            // 调用Token生成服务
            return remoteTokenService.getTokenPage(params);
        }
    
    }

  • 自定义一个用户名的token实现
    /**
         * 获取token  -  自定义token
         * @param request 请求参数
         * @return {@link UserTokenDTO.Response}
         */
        @Inner  // 这个注解表示只有内部系统可以调用
        @SneakyThrows
        @PostMapping("/token/generate-token")
        public R<UserTokenDTO.Response> generateToken(@RequestBody UserTokenDTO.Request request) {
    
            // 第1步:创建一个"客户端配置",告诉系统这个Token的基本规则
            RegisteredClient registeredClient = RegisteredClient.withId(SecurityConstants.FROM)
                    .clientId(SecurityConstants.FROM)
                    .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                    .tokenSettings(TokenSettings.builder()
                            .accessTokenTimeToLive(Duration.ofHours(24)) // Token有效期:24小时
                            .refreshTokenTimeToLive(Duration.ofDays(7))  // 刷新Token有效期:7天
                            .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                            .build())
                    .build();
    
            // 第2步:构建用户信息对象
            // 这里我们根据传入的用户名创建一个完整的用户对象
            ScpUser pigUser = new ScpUser(
                    110L,                    // 用户ID
                    request.getUsername(),   // 用户名
                    null,                    // 密码(这里不需要)
                    "", "", "", "", "",      // 其他用户信息(暂时为空)
                    1L,                      // 部门ID
                    "",                      // 其他信息
                    true, true,              // 账户状态
                    UserTypeEnum.TOB.getStatus(),
                    true, false,
                    // 将权限字符串转换为系统认识的权限对象
                    request.getAuthorities().stream()
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList())
            );
    
            // 第3步:创建认证对象
            Authentication usernamePasswordAuthentication =
                    new UsernamePasswordAuthenticationToken(pigUser, StrUtil.EMPTY);
    
            // 第4步:设置Token生成的上下文环境
            DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
                    .registeredClient(registeredClient)
                    .principal(usernamePasswordAuthentication)
                    .authorizationServerContext(new AuthorizationServerContext() {
                        @Override
                        public String getIssuer() {
                            return "http://ai.com";  // Token发行者
                        }
    
                        @Override
                        public AuthorizationServerSettings getAuthorizationServerSettings() {
                            return AuthorizationServerSettings.builder().build();
                        }
                    });
    
            // 第5步:开始构建授权信息
            OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
                    .withRegisteredClient(registeredClient)
                    .principalName(usernamePasswordAuthentication.getName());
    
            // 第6步:生成访问Token(主要的通行证)
            OAuth2TokenContext tokenContext = tokenContextBuilder
                    .tokenType(OAuth2TokenType.ACCESS_TOKEN)
                    .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                    .authorizationGrant(new OAuth2ClientAuthenticationToken(
                            registeredClient,
                            ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
                            null))
                    .build();
    
            OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
            OAuth2AccessToken accessToken = new OAuth2AccessToken(
                    OAuth2AccessToken.TokenType.BEARER,
                    generatedAccessToken.getTokenValue(),
                    generatedAccessToken.getIssuedAt(),
                    generatedAccessToken.getExpiresAt(),
                    tokenContext.getAuthorizedScopes());
    
            // 第7步:保存访问Token信息
            if (generatedAccessToken instanceof ClaimAccessor) {
                authorizationBuilder.id(accessToken.getTokenValue())
                        .token(accessToken, (metadata) -> metadata.put(
                                OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
                                ((ClaimAccessor) generatedAccessToken).getClaims()))
                        .attribute(Principal.class.getName(), usernamePasswordAuthentication);
            } else {
                authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
            }
    
            // 第8步:生成刷新Token(用来获取新的访问Token)
            OAuth2RefreshToken refreshToken;
            tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
            OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
            refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
            authorizationBuilder.refreshToken(refreshToken);
    
            // 第9步:保存完整的授权信息到数据库
            OAuth2Authorization authorization = authorizationBuilder
                    .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                    .build();
            this.authorizationService.save(authorization);
    
            // 第10步:返回生成的Token
            return R.ok(new UserTokenDTO.Response(
                    accessToken.getTokenValue(),
                    refreshToken.getTokenValue()));
        }
    package com.shpl.scp.admin.api.dto;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Data
    public class UserTokenDTO {
    
        // 请求信封:装载我们要发送的数据
        @Data
        public static class Request {
            private String username;  // 用户名(必填)
            private List<String> authorities = new ArrayList<>();  // 用户权限列表(可选)
        }
    
        // 响应信封:装载系统返回给我们的数据
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public static class Response {
            private String accessToken;   // 访问令牌(主要的通行证)
            private String refreshToken;  // 刷新令牌(用来获取新的访问令牌)
        }
    }
    /*
     *    Copyright (c) 2018-2025, scp 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: scp
     */
    
    package com.shpl.scp.common.security.service;
    
    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.Getter;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityCoreVersion;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author scp
     * @date 2020/4/16 扩展用户信息
     */
    public class ScpUser extends User implements OAuth2AuthenticatedPrincipal {
    
    	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
    	/**
    	 * 扩展属性,方便存放oauth 上下文相关信息
    	 */
    	private final Map<String, Object> attributes = new HashMap<>();
    
    	/**
    	 * 用户ID
    	 */
    	@Getter
    	private Long id;
    
    	/**
    	 * 部门ID
    	 */
    	@Getter
    	private Long deptId;
    
    	/**
    	 * 手机号
    	 */
    	@Getter
    	private String phone;
    
    	/**
    	 * 头像
    	 */
    	@Getter
    	private String avatar;
    
    	/**
    	 * 租户ID
    	 */
    	@Getter
    	private Long tenantId;
    
    	/**
    	 * 拓展字段:昵称
    	 */
    	@Getter
    	private String nickname;
    
    	/**
    	 * 拓展字段:姓名
    	 */
    	@Getter
    	private String name;
    
    	/**
    	 * 拓展字段:邮箱
    	 */
    	@Getter
    	private String email;
    
    	@Getter
    	private String userType;
    
    	/**
    	 * Construct the <code>User</code> with the details required by
    	 * {@link DaoAuthenticationProvider}.
    	 * @param id 用户ID
    	 * @param deptId 部门ID
    	 * @param tenantId 租户ID
    	 * @param nickname 昵称
    	 * @param name 姓名
    	 * @param email 邮箱 the username presented to the
    	 * <code>DaoAuthenticationProvider</code>
    	 * @param password the password that should be presented to the
    	 * <code>DaoAuthenticationProvider</code>
    	 * @param enabled set to <code>true</code> if the user is enabled
    	 * @param accountNonExpired set to <code>true</code> if the account has not expired
    	 * @param credentialsNonExpired set to <code>true</code> if the credentials have not
    	 * expired
    	 * @param accountNonLocked set to <code>true</code> if the account is not locked
    	 * @param authorities the authorities that should be granted to the caller if they
    	 * presented the correct username and password and the user is enabled. Not null.
    	 * @throws IllegalArgumentException if a <code>null</code> value was passed either as
    	 * a parameter or as an element in the <code>GrantedAuthority</code> collection
    	 */
    	@JsonCreator
    	public ScpUser(@JsonProperty("id") Long id, @JsonProperty("username") String username,
    			@JsonProperty("deptId") Long deptId, @JsonProperty("phone") String phone,
    			@JsonProperty("avatar") String avatar, @JsonProperty("nickname") String nickname,
    			@JsonProperty("name") String name, @JsonProperty("email") String email,
    			@JsonProperty("tenantId") Long tenantId, @JsonProperty("password") String password,
    			@JsonProperty("enabled") boolean enabled, @JsonProperty("accountNonExpired") boolean accountNonExpired,
    			@JsonProperty("userType") String userType,
    			@JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
    			@JsonProperty("accountNonLocked") boolean accountNonLocked,
    			@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
    		super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    		this.id = id;
    		this.deptId = deptId;
    		this.phone = phone;
    		this.avatar = avatar;
    		this.tenantId = tenantId;
    		this.nickname = nickname;
    		this.name = name;
    		this.email = email;
    		this.userType = userType;
    	}
    
    	@Override
    	public Map<String, Object> getAttributes() {
    		return this.attributes;
    	}
    
    }
    
    package com.shpl.scp.common.security.service;
    
    import cn.hutool.core.collection.CollUtil;
    import com.shpl.scp.common.core.constant.SecurityConstants;
    import lombok.RequiredArgsConstructor;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.lang.Nullable;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.oauth2.core.OAuth2AccessToken;
    import org.springframework.security.oauth2.core.OAuth2RefreshToken;
    import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
    import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
    import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
    import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
    import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
    import org.springframework.util.Assert;
    
    import java.security.Principal;
    import java.time.temporal.ChronoUnit;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Objects;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author scp
     * @date 2022/5/27
     */
    @RequiredArgsConstructor
    public class ScpRedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
    
    	private final static Long TIMEOUT = 10L;
    
    	private static final String AUTHORIZATION = "token";
    
    	private final RedisTemplate<String, Object> redisTemplate;
    
    	private static boolean isState(OAuth2Authorization authorization) {
    		return Objects.nonNull(authorization.getAttribute("state"));
    	}
    
    	private static boolean isCode(OAuth2Authorization authorization) {
    		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
    			.getToken(OAuth2AuthorizationCode.class);
    		return Objects.nonNull(authorizationCode);
    	}
    
    	private static boolean isRefreshToken(OAuth2Authorization authorization) {
    		return Objects.nonNull(authorization.getRefreshToken());
    	}
    
    	private static boolean isAccessToken(OAuth2Authorization authorization) {
    		return Objects.nonNull(authorization.getAccessToken());
    	}
    
    	@Override
    	public void save(OAuth2Authorization authorization) {
    		Assert.notNull(authorization, "authorization cannot be null");
    
    		if (isState(authorization)) {
    			String token = authorization.getAttribute("state");
    			redisTemplate.setValueSerializer(RedisSerializer.java());
    			redisTemplate.opsForValue()
    				.set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT, TimeUnit.MINUTES);
    		}
    
    		if (isCode(authorization)) {
    			OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
    				.getToken(OAuth2AuthorizationCode.class);
    			OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
    			long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
    					authorizationCodeToken.getExpiresAt());
    			redisTemplate.setValueSerializer(RedisSerializer.java());
    			redisTemplate.opsForValue()
    				.set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()), authorization,
    						between, TimeUnit.MINUTES);
    		}
    
    		if (isRefreshToken(authorization)) {
    			OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
    			long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
    			redisTemplate.setValueSerializer(RedisSerializer.java());
    			redisTemplate.opsForValue()
    				.set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()), authorization, between,
    						TimeUnit.SECONDS);
    		}
    
    		if (isAccessToken(authorization)) {
    			OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
    			long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
    			redisTemplate.setValueSerializer(RedisSerializer.java());
    			redisTemplate.opsForValue()
    				.set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, between,
    						TimeUnit.SECONDS);
    
    			UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = authorization
    				.getAttribute(Principal.class.getName());
    
    			if (Objects.nonNull(usernamePasswordAuthenticationToken)
    					&& usernamePasswordAuthenticationToken.getPrincipal() instanceof ScpUser scpUser) {
    				// 扩展记录 access-token 、username 的关系 token::username::admin::tenantId::xxx
    				String tokenUsername = String.format("%s::%s::%s::%s::%s::%s", AUTHORIZATION,
    						SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(),
    						authorization.getRegisteredClientId(), scpUser.getTenantId(), accessToken.getTokenValue());
    				redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
    			}
    		}
    	}
    
    	@Override
    	public void remove(OAuth2Authorization authorization) {
    		Assert.notNull(authorization, "authorization cannot be null");
    
    		List<String> keys = new ArrayList<>();
    		if (isState(authorization)) {
    			String token = authorization.getAttribute("state");
    			keys.add(buildKey(OAuth2ParameterNames.STATE, token));
    		}
    
    		if (isCode(authorization)) {
    			OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
    				.getToken(OAuth2AuthorizationCode.class);
    			OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
    			keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
    		}
    
    		if (isRefreshToken(authorization)) {
    			OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
    			keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
    		}
    
    		if (isAccessToken(authorization)) {
    			OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
    			keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
    
    			// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
    			String key = String.format("%s::%s::%s::%s::*::%s", AUTHORIZATION, SecurityConstants.DETAILS_USERNAME,
    					authorization.getPrincipalName(), authorization.getRegisteredClientId(),
    					accessToken.getTokenValue());
    			Set<String> pattenKey = redisTemplate.keys(key);
    			if (CollUtil.isNotEmpty(pattenKey)) {
    				keys.addAll(pattenKey);
    			}
    		}
    
    		redisTemplate.delete(keys);
    	}
    
    	@Override
    	@Nullable
    	public OAuth2Authorization findById(String id) {
    		throw new UnsupportedOperationException();
    	}
    
    	@Override
    	@Nullable
    	public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
    		Assert.hasText(token, "token cannot be empty");
    		redisTemplate.setValueSerializer(RedisSerializer.java());
    		return (OAuth2Authorization) redisTemplate.opsForValue()
    			.get(buildKey(Objects.requireNonNullElse(tokenType, OAuth2TokenType.ACCESS_TOKEN).getValue(), token));
    	}
    
    	private String buildKey(String type, String id) {
    		return String.format("%s::%s::%s", AUTHORIZATION, type, id);
    	}
    
    	/**
    	 * 根据用户名移除相关授权信息
    	 * @param authentication 认证信息,包含用户名
    	 */
    	public void removeByUsername(Authentication authentication) {
    		// 根据 username查询对应access-token
    		String authenticationName = authentication.getName();
    
    		// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
    		String tokenUsernameKey = String.format("%s::%s::%s::*", AUTHORIZATION, SecurityConstants.DETAILS_USERNAME,
    				authenticationName);
    		Set<String> keys = redisTemplate.keys(tokenUsernameKey);
    		if (CollUtil.isEmpty(keys)) {
    			return;
    		}
    
    		List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
    
    		for (Object token : tokenList) {
    			// 根据token 查询存储的 OAuth2Authorization
    			OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
    
    			if (Objects.isNull(authorization)) {
    				continue;
    			}
    			// 根据 OAuth2Authorization 删除相关令牌
    			this.remove(authorization);
    		}
    
    		redisTemplate.delete(keys);
    	}
    
    }
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值