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

Spring Security权限控制异常:/admin路径403 Forbidden求助

问题描述

尝试将/admin/**路径限制为仅ADMIN角色可访问,但访问时出现403 Forbidden错误,错误信息如下:

此应用未为/error配置显式映射,因此您看到的是回退页面。
Mon Apr 08 01:49:02 EDT 2024
发生意外错误(类型=Forbidden,状态=403)。

安全配置代码

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
    private final UserService userService;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        return http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/").permitAll()
                        .requestMatchers("/login**", "/error**", "/resource/**").permitAll()
                        .requestMatchers("/admin/**").hasRole("ADMIN")

                        .anyRequest().authenticated()
                )
                .formLogin(withDefaults())
                .oauth2Login((userInfo) -> userInfo
                        .userInfoEndpoint((config) -> config.oidcUserService(userService))
                )
                .build();
    }
}

User实体代码

@Entity
@Table(name = "user")
@Data
@NoArgsConstructor
public class UserEntity implements Serializable, UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(unique = true)
    private String username;

    @Column
    private String password;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
    @JoinTable(name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private Set<Role> roles = new HashSet<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return getRoles();
    }
}

Role实体代码

@Entity
@Table(name = "role")
@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(unique = true)
    private String name;

    @Override
    public String getAuthority() {
        return name;
    }
}

UserService代码

@Service
@AllArgsConstructor
public class UserService extends OidcUserService {
    private final UserRepository userRepository;
    private final RoleRepository roleRepository;

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcUser oidcUser = super.loadUser(userRequest);
        try {
            return processOidcUser(userRequest, oidcUser);
        } catch (Exception e) {
            throw new InternalAuthenticationServiceException(e.getMessage(), e.getCause());
        }
    }

    private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) {
        GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());

        Optional<UserEntity> userOptional = userRepository.findByUsername(googleUserInfo.getEmail());
        if (userOptional.isEmpty()) {
            UserEntity user = new UserEntity();
            user.setUsername(googleUserInfo.getEmail());
            user.setPassword(googleUserInfo.getName());
            Role role = roleRepository.findByName("ADMIN");
            user.getRoles().add(role);
            userRepository.save(user);
        }
        return oidcUser;
    }
}

已尝试以下配置但均无效:

.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.requestMatchers("/admin/**").hasAuthority("ADMIN")

解决方案

核心问题:OAuth2登录后返回的原生OidcUser对象,并没有携带你数据库中存储的ADMIN角色权限,导致Spring Security无法识别用户的角色。

1. 自定义OidcUser实现类

创建自定义类,整合原生Oidc信息与数据库角色权限:

public class CustomOidcUser implements OidcUser {
    private final OidcUser oidcUser;
    private final Collection<? extends GrantedAuthority> authorities;

    public CustomOidcUser(OidcUser oidcUser, Collection<? extends GrantedAuthority> authorities) {
        this.oidcUser = oidcUser;
        this.authorities = authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return oidcUser.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getName() {
        return oidcUser.getName();
    }

    @Override
    public Map<String, Object> getClaims() {
        return oidcUser.getClaims();
    }

    @Override
    public OidcIdToken getIdToken() {
        return oidcUser.getIdToken();
    }

    @Override
    public OidcUserInfo getUserInfo() {
        return oidcUser.getUserInfo();
    }
}

2. 修改UserService的processOidcUser方法

不再返回原生OidcUser,而是返回携带数据库角色的自定义对象:

private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) {
    GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());

    Optional<UserEntity> userOptional = userRepository.findByUsername(googleUserInfo.getEmail());
    UserEntity user;
    if (userOptional.isEmpty()) {
        user = new UserEntity();
        user.setUsername(googleUserInfo.getEmail());
        user.setPassword(googleUserInfo.getName());
        Role role = roleRepository.findByName("ADMIN");
        user.getRoles().add(role);
        userRepository.save(user);
    } else {
        user = userOptional.get();
    }
    // 返回携带数据库角色的自定义OidcUser
    return new CustomOidcUser(oidcUser, user.getAuthorities());
}

3. 匹配角色权限格式

Spring Security的hasRole方法会自动给角色名称加上ROLE_前缀,所以需要二选一:

  • 如果数据库中角色名称是ADMIN,则安全配置改用.hasAuthority("ADMIN")
  • 如果想用.hasRole("ADMIN"),则数据库中角色名称需改为ROLE_ADMIN

4. 验证权限加载情况

添加调试接口确认权限是否正确注入:

@GetMapping("/user/authorities")
@ResponseBody
public Collection<? extends GrantedAuthority> getCurrentUserAuthorities(Authentication authentication) {
    return authentication.getAuthorities();
}

访问该接口,查看返回结果中是否包含对应角色权限。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:03:16