You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot OAuth2授权服务器登录后动态生成同意页与JWT作用域

Spring Boot OAuth2授权服务器:动态角色匹配的自定义同意页实现

核心思路

用户登录后拦截授权请求,根据当前用户角色过滤出其可访问的作用域,渲染自定义同意页供用户选择部分/全部权限,最终签发包含所选作用域的访问令牌。


一、自定义授权请求解析器(过滤作用域)

实现OAuth2AuthorizationRequestResolver,在用户登录后根据角色筛选允许的作用域,替换原请求中的作用域集合。

@Component
public class DynamicScopeAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private final OAuth2AuthorizationRequestResolver defaultResolver;

    public DynamicScopeAuthorizationRequestResolver(OAuth2AuthorizationRequestResolver defaultResolver) {
        this.defaultResolver = defaultResolver;
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request);
        return processAuthRequest(authRequest);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientId) {
        OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request, clientId);
        return processAuthRequest(authRequest);
    }

    private OAuth2AuthorizationRequest processAuthRequest(OAuth2AuthorizationRequest authRequest) {
        if (authRequest == null) return null;

        // 获取当前登录用户的认证信息
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!(auth instanceof UserDetails)) return authRequest;

        UserDetails user = (UserDetails) auth;
        Set<String> allowedScopes = getScopesByRole(user.getAuthorities());

        // 过滤客户端请求的作用域,仅保留用户允许的范围
        Set<String> filteredScopes = authRequest.getScopes().stream()
                .filter(allowedScopes::contains)
                .collect(Collectors.toSet());

        // 构建更新后的授权请求
        return OAuth2AuthorizationRequest.from(authRequest)
                .scopes(filteredScopes)
                .build();
    }

    // 角色-作用域映射逻辑
    private Set<String> getScopesByRole(Collection<? extends GrantedAuthority> authorities) {
        Set<String> scopes = new HashSet<>();
        for (GrantedAuthority authority : authorities) {
            switch (authority.getAuthority()) {
                case "ROLE_READER":
                    scopes.add("read");
                    break;
                case "ROLE_ADMIN":
                    scopes.add("read");
                    scopes.add("write");
                    break;
                case "ROLE_SUPER_ADMIN":
                    scopes.add("read");
                    scopes.add("write");
                    scopes.add("delete");
                    break;
                default:
                    break;
            }
        }
        return scopes;
    }
}

在授权服务器配置中注册该解析器:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final DynamicScopeAuthorizationRequestResolver dynamicScopeResolver;

    public AuthorizationServerConfig(DynamicScopeAuthorizationRequestResolver dynamicScopeResolver) {
        this.dynamicScopeResolver = dynamicScopeResolver;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authorizationRequestResolver(dynamicScopeResolver);
        // 补充其他配置:tokenStore、authenticationManager等
    }
}

二、自定义同意页控制器与视图

替换默认同意页,实现用户选择作用域的交互逻辑。

1. 同意页控制器

@Controller
public class CustomConsentController {

    // 渲染同意页
    @GetMapping("/oauth/confirm_access")
    public String showConsentPage(HttpServletRequest request, Model model) {
        OAuth2AuthorizationRequest authRequest = (OAuth2AuthorizationRequest) request.getAttribute(
                OAuth2AuthorizationRequestResolver.DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME
        );

        model.addAttribute("clientId", authRequest.getClientId());
        model.addAttribute("scopes", authRequest.getScopes());
        model.addAttribute("state", authRequest.getState());

        return "consent"; // 对应Thymeleaf视图文件名
    }

    // 处理用户选择的作用域
    @PostMapping("/oauth/confirm_access")
    public String processConsent(@RequestParam("scope") Set<String> selectedScopes,
                                 @RequestParam("state") String state,
                                 HttpServletRequest request) {
        OAuth2AuthorizationRequest originalRequest = (OAuth2AuthorizationRequest) request.getAttribute(
                OAuth2AuthorizationRequestResolver.DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME
        );

        // 更新授权请求为用户选择的作用域
        OAuth2AuthorizationRequest updatedRequest = OAuth2AuthorizationRequest.from(originalRequest)
                .scopes(selectedScopes)
                .build();

        request.setAttribute(
                OAuth2AuthorizationRequestResolver.DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME,
                updatedRequest
        );

        // 转发回授权端点完成令牌签发流程
        return "forward:/oauth/authorize";
    }
}

2. Thymeleaf同意页视图(consent.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>权限授权确认</title>
</head>
<body>
    <h2>应用 <span th:text="${clientId}"></span> 请求以下权限</h2>
    <form method="post" th:action="@{/oauth/confirm_access}">
        <input type="hidden" name="state" th:value="${state}">
        <div th:each="scope : ${scopes}">
            <label>
                <input type="checkbox" name="scope" th:value="${scope}" checked>
                <span th:text="${scope}"></span>
            </label>
        </div>
        <button type="submit">确认授权</button>
        <a th:href="@{/oauth/authorize?error=access_denied&state=${state}}">取消授权</a>
    </form>
</body>
</html>

三、确保令牌包含所选作用域

默认情况下,授权服务器会使用更新后的授权请求中的作用域生成令牌。如果需要在令牌中额外携带角色信息,可自定义TokenEnhancer:

@Component
public class CustomTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> extraInfo = new HashMap<>();
        UserDetails user = (UserDetails) authentication.getPrincipal();
        extraInfo.put("roles", user.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()));
        
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(extraInfo);
        return accessToken;
    }
}

在授权服务器配置中添加该增强器:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
    enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, jwtAccessTokenConverter));

    endpoints.authorizationRequestResolver(dynamicScopeResolver)
            .tokenEnhancer(enhancerChain);
    // 其他配置
}

四、关键注意事项

  • 确保登录流程优先级高于授权端点,在SecurityFilterChain配置中先处理登录请求。
  • 客户端注册时,需允许请求所有可能的作用域(read/write/delete),保证初始请求的作用域能被正常过滤。
  • 测试时验证不同角色用户登录后,同意页显示的作用域是否匹配,令牌的scope字段是否与用户选择一致。

内容的提问来源于stack exchange,提问作者Harshad Prajapati

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:18:19