SpringSecurityoauth2.0自整理文档

  • 被标记为资源的url不会走用户认证过滤器,所以通过createBefor=>AuthFilter添加的过滤器无效
    • 也就是在ResourceServerConfigurerAdapter实现类中配置的资源路径
  • 记录一下手动加载用户和调用系统方法加载用户,以及他们的配置
  • 记录一下自动加载用户和自动密码校验的配置
  • 获取授权码的每个参数
  • 获取token的每个参数链接
  • 记录一下过滤器的处理
  • 记录一下前后端分离和不分离的区别
  • 记录如何添加过滤器到认证服务器前面
  • 记录一下重定向地址携带参数的问题

基本流程

业务流程

  1. 使用用户身份申请授权码(此时需保证用户登录状态否则需要登录)
  2. 使用客户端身份+授权码申请accessToken
  3. 使用accessToken访问受保护资源

集成流程

  1. 配置资源所有者认证规则,继承WebSecurityConfigurerAdapter实现相关配置,配置资源的访问规则,设置哪些需要鉴权哪些不需要鉴权
  2. 配置资源所有者加载器,实现UserDetailsService,然后在步骤1的configure(AuthenticationManagerBuilder auth)中配置进去
    1. 如果使用的是基于jwt的登录状态保持,需要添加一个过滤器到UsernamePasswordAuthenticationFilter过滤器之前,然后在里面完成身份的验证,验证通过以后创建一个用户详情对象UserDetails存入线程域中,完成登录,后续的过滤器看到这个对象就认为他已经完成了认证
    2. 如果基于会话的,可以直接使用自带的密码校验工具,在登录时主动调用如下方法,届时就会自动调用UserDetailService.loadUserByUsername然后调用我们在第3条传递的密码校验规则进行密码比对
// 不为空再进行安全上下文的生成和赋予;如果为空直接放行,下一个过滤器会收拾他,不过不要修改加解密bean
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
  1. 配置资源所有者验证规则,重写WebSecurityConfigurerAdapter的configure(AuthenticationManagerBuilder auth)方法以配置用户校验规则
  2. 配置资源认证服务,继承AuthorizationServerConfigurerAdapter实现相关配置,设置资源的访问权限,例如资源id和资源scope等
    1. 如果配置使用jdbc加载,需要配置客户端加载器,实现ClientDetailsService重写loadClientByClientId方法,最后返回一个ClientDetails,当客户端来鉴权的时候,不再去内存找,而是调用这个方法
    2. 如果需要自定义客户端校验规则可以重写configure(AuthorizationServerSecurityConfigurer security)方法以修改
  3. 配置资源服务,继承ResourceServerConfigurerAdapter重写configure(HttpSecurity http)以配置资源的访问规则
  4. 自定义授权页面

总而言之就是要:

  • 处理资源所有者的加载和验证,保证获取授权码的时候资源所有者是登录状态
  • 处理客户端的加载和验证,保证使用授权码的兑换token的时候客户端是登录状态
  • 处理资源的访问规则,
  • 处理资源的验证规则,
    依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>team.sss</groupId>
    <artifactId>open-platform</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
    </parent>

    <properties>
        <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <!--System-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.3.3.RELEASE</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--Tool-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.7.9</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.20</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.8.1</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.3.RELEASE</version>
            </plugin>
        </plugins>
    </build>


</project>

基于会话的Oauth2.0

-配置认证服务器

-配置资源服务器

基于Jwt的Oauth2.0

这里的jwt指的是用于维持资源所有者登录状态时使用jwt

-配置资源认证服务器

此配置管理资源的认证,用于配置资源的访问规则

基于内存加载客户端

把客户端写在内存

/**
 * 授权服务器配置
 *
 * @author Guochao
 */
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {



    private final PasswordEncoder passwordEncoder;
    private final AdopApplicationService adopApplicationService;
    private final CustomJwtTokenFilter customJwtTokenFilter;

    public AuthorizationServerConfig(PasswordEncoder passwordEncoder, AdopApplicationService adopApplicationService, CustomJwtTokenFilter customJwtTokenFilter) {
        this.passwordEncoder = passwordEncoder;
        this.adopApplicationService = adopApplicationService;
        this.customJwtTokenFilter = customJwtTokenFilter;
    }

    // 配置客户端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 加载合作伙伴应用的信息(模板)
        // clients.inMemory()
        //         .withClient("pzh") // clientId,客户端id
        //         // 客户端密码,客户端传输过来的密钥会进行加密,就用你注入进去的那个,所以如果你是明文就需要在这里进行加密以后写入,如果你数据库存的就是密文,则直接写入
        //         .secret(passwordEncoder.encode("123456"))
        //         // 重定向的地址,用户同意授权以后会携带授权码请求回调地址,从而获取授权码
        //         .redirectUris("http://localhost:9998/oauth/call-back")
        //         .scopes("resource", "userinfo", "all")  // 授权允许的范围
        //         .authorizedGrantTypes("authorization_code", "refresh_token") // 授权类型,这里选择授权码模式
        //         .autoApprove(true) // 绝对自动授权,开启以后不用用户手动确认,不推荐,除非实在不想和用户交互
        // ;

        // 改为从数据库加载第三方平台信息,第三方接入量超过1W以后使用分页,小声bb:达到这个数量级有点难阿;
        List<LoadThirdPartyPlatformsDto> thirdPartyPlatforms = adopApplicationService.getAllToLoadThirdPartyPlatformsDto();
        // 获取内存写入对象,一定要在循环外创建,否则每次循环都是拿到一个新的,这样只有最后一个会生效
        InMemoryClientDetailsServiceBuilder inMemory = clients.inMemory();
        for (LoadThirdPartyPlatformsDto partyPlatform : thirdPartyPlatforms) {
            ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder>.ClientBuilder builder = inMemory
                    .withClient(partyPlatform.getClientId().toString())
                    .secret(partyPlatform.getSecret())
                    .redirectUris(partyPlatform.getRedirectUri());

            // 授权空间list
            List<AdopScopeDto> scopes = partyPlatform.getScopes();
            if (CollUtil.isNotEmpty(scopes)) {
                builder.scopes(scopes.stream().map(AdopScope::getScopeCode).toArray(String[]::new))
                        .autoApprove(scopes.stream().filter(s -> s.getAutoStatus() == 1 && s.getId() != null).map(AdopScopeDto::getScopeCode).toArray(String[]::new));
            }

            // 授权类型list
            List<AdopGrantType> grantTypes = partyPlatform.getGrantTypes();
            if (CollUtil.isNotEmpty(grantTypes)) {
                builder.authorizedGrantTypes(grantTypes.stream().filter(g -> g.getId() != null).map(AdopGrantType::getGrantTypeCode).toArray(String[]::new));
            } else {
                // 如果为空就默认授权码+刷新模式
                builder.authorizedGrantTypes("authorization_code");
            }
        }
    }
}

基于JDBC加载客户端

客户端认证时查询服务器获取结果

/**
 * 授权服务器配置
 *
 * @author Guochao
 */
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    private final PasswordEncoder passwordEncoder;
    private final AdopApplicationService adopApplicationService;
    private final ClientDetailServiceJDBCImpl jdbcClientDetailService;

    public AuthorizationServerConfig(PasswordEncoder passwordEncoder, AdopApplicationService adopApplicationService, ClientDetailServiceJDBCImpl jdbcClientDetailService) {
        this.passwordEncoder = passwordEncoder;
        this.adopApplicationService = adopApplicationService;
        this.jdbcClientDetailService = jdbcClientDetailService;
    }

    // 配置客户端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 加载应用客户端的信息
        // 配置客户端详情加载器
        final ClientDetailsServiceBuilder<?> serviceBuilder = clients.withClientDetails(jdbcClientDetailService);
        final JdbcClientDetailsServiceBuilder jdbc = serviceBuilder.jdbc();
        // 配置加密解密
        jdbc.passwordEncoder(passwordEncoder);
        // 配置数据源
        jdbc.dataSource(new DruidDataSource());
    }
}

客户端详情加载器

实现org.springframework.security.oauth2.provider.ClientDetailsService;接口并重写他的loadClientByClientId接口,然后把这个对象注入到资源认证服务器配置中,并设置进withClientDetails中
这样在客户端验证的时候就会自动调用我们的实现方法,我们只需要在这里返回对应的ClientDetails就可以了

@Component
public class ClientDetailServiceJDBCImpl implements ClientDetailsService {

    private final AdopApplicationService adopApplicationService;

    public ClientDetailServiceJDBCImpl(AdopApplicationService adopApplicationService) {
        this.adopApplicationService = adopApplicationService;
    }

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        final LoadThirdPartyPlatformsDto appDto = Optional.ofNullable(adopApplicationService.getClientById(Long.valueOf(clientId)))
                .orElseThrow(() -> new RuntimeException("ClientId not found"));
        return cpToClientDetails(appDto);
    }

    public ClientDetails cpToClientDetails(LoadThirdPartyPlatformsDto adopApplication) {
        // 实现将AdopApplication对象转换为ClientDetails对象的逻辑
        return new CustomClientDetails(adopApplication);
    }
}

客户端详情对象

实现org.springframework.security.oauth2.provider.ClientDetails;接口即可定义一个自定义的客户端详情对象

@Data
@AllArgsConstructor
public class CustomClientDetails implements ClientDetails {

    private LoadThirdPartyPlatformsDto clientInfo;

    @Override
    public String getClientId() {
        return this.clientInfo.getClientId().toString();
    }

    @Override
    public Set<String> getResourceIds() {
        return null;
    }

    @Override
    public boolean isSecretRequired() {
        return true;
    }

    @Override
    public String getClientSecret() {
        return this.clientInfo.getSecret();
    }

    @Override
    public boolean isScoped() {
        return true;
    }

    // 返回允许的授权空间
    @Override
    public Set<String> getScope() {
        final List<AdopScopeDto> scopes = this.clientInfo.getScopes();
        return scopes.stream().map(AdopScopeDto::getScopeCode).collect(Collectors.toSet());
    }

    //  返回允许的授权类型
    @Override
    public Set<String> getAuthorizedGrantTypes() {
        final TreeSet<String> set = new TreeSet<>();
        set.add("authorization_code");
        set.add("refresh_token");
        return set;
    }

    // 回调地址
    @Override
    public Set<String> getRegisteredRedirectUri() {
        final String redirectUri = this.clientInfo.getRedirectUri();
        return new TreeSet<String>() {{
            add(redirectUri);
        }};
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return new ArrayList<>();
    }

    @Override
    public Integer getAccessTokenValiditySeconds() {
        return null;
    }

    @Override
    public Integer getRefreshTokenValiditySeconds() {
        return null;
    }

    // 是否自动授权
    @Override
    public boolean isAutoApprove(String scope) {
        final Boolean enableAutoConfirm = this.clientInfo.getEnableAutoConfirm();
        return enableAutoConfirm != null && enableAutoConfirm;
    }

    @Override
    public Map<String, Object> getAdditionalInformation() {
        return null;
    }
}

-配置资源权限服务器

用于配置每个资源访问所需的权限

// 资源服务配置
@Configuration
@EnableResourceServer  // 启用资源服务
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private AdopScopeService adopScopeService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        List<AdopScope> scopeList = adopScopeService.findAll();

        for (AdopScope scope : scopeList) {
            registry.antMatchers(scope.getScopeUri())
                    .access("#oauth2.hasAnyScope('"+scope.getScopeCode()+"')")
                    .and()
                    .requestMatchers().antMatchers(scope.getScopeUri());
        }

        http.authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable();

        // 旧版硬编码样例
//        registry
//                // 配置带资源域限制的资源信息
//                .antMatchers("/resource/private/**").access("#oauth2.hasAnyScope('private')")
//                .antMatchers("/resource/userInfo/**").access("#oauth2.hasAnyScope('userInfo')")
//                .antMatchers("/resource/login/**").access("#oauth2.hasAnyScope('login')")
//                .antMatchers("/resource/login/openId").access("#oauth2.hasAnyScope('login')")
//                .and()
//                // 匹配资源,对上面的资源进行匹配地址,配置在里面的资源将受到保护,必须全部认证才能访问
//                // 上面配置了这个资源的访问权限。这里依然需要配置保护
//                .requestMatchers()
//                .antMatchers("/resource/private/**")
//                .antMatchers("/resource/userInfo/**")
//                .antMatchers("/resource/login/**")
//                .antMatchers("/resource/login/openId")
//                .and()
//                // 指定任何请求,设r任何请求都需要授权以后才能访问
//                .authorizeRequests().anyRequest().authenticated()
//                .and().csrf().disable(); // 资源需要关闭这个,否则第三方拿到token以后依然无法访问会被拦截
    }
}

-配置资源所有者加认证流程

最终目的就是验证后把UserDetails设置到SecurityContextHolder中

资Security全局配置

主要用于配置全局的访问控制,以及资源所有者的加载&登录方法
这里我们用到的流程是:主动配置加载和解密的Bean,最后通过默认的表单提交行为,或者主动触发 authenticationManager.authenticate()调用加载和校验最终实现的方法来进行用户详情对象的创建

想要定制的话可以自己添加过滤器,在喜欢的地方自己创建用户详情对象写入到SecurityContextHolder中完成身份的认证;

@Configuration
@EnableWebSecurity // 启动WebSecurity[可以写在配置类]
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomJwtTokenFilter customJwtTokenFilter;
    private final UserDetailLoader userLoad;

    public SecurityConfig(CustomJwtTokenFilter customJwtTokenFilter, UserDetailLoader userLoad) {
        this.customJwtTokenFilter = customJwtTokenFilter;
        this.userLoad = userLoad;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  // 单页面应用或者app可以选择关闭这个,只要不是基于会话的都可以
                .cors().and() // 允许跨域
                .authorizeRequests()// 配置认证请求
                .antMatchers("/auth/login","/index.html")  // 目前只开放鉴权入口;
                .permitAll() // 对上面描述的匹配规则进行放行
                // 切换到任何请求,设置都要进行认证之后才能访问
                .anyRequest().authenticated();
        //配置这个会造成user后的404响应,可能是因为配合了规则却没有配置后文
        //http.and().requestMatchers().antMatchers("/user/**");
        //http.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
        http.formLogin().permitAll(); // 对表单认证进行放行,同时自定义登录验证路由

        // 添加jwt过滤器到密码校验之前,在那之前完成jwt的校验和放入安全上下文对象
        http.addFilterBefore(customJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 配置用户加载器和密码校验器
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置用户加载类,以及加密方案
        auth.userDetailsService(userLoad)  // 用户加载类
                // 这里不使用默认。使用一个自定义的方法
                .passwordEncoder(new CustomJwtTokenEncoder());
    }

    // 当出现无法注入bean【AuthenticationManager】时添加,这个Bean用于主动调用框架的密码校验
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 配置加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 跨域配置
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("*"); // 允许所有域访问
        configuration.addAllowedMethod("*"); // 允许所有方法
        configuration.addAllowedHeader("*"); // 允许所有头部
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

用户身份的解析

—方案一:登录后基于默认的会话实现

默认基于会话完成,我们可以在登录以后把这个对象设置好,在会话结束之前都可以保持登录

// 使用用户名密码创建一个用户密码对象,交给校验器校验
UsernamePasswordAuthenticationToken authenticationToken = 
new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 我们预先配置好的用户加载器和密码校验器这时候就会被调用
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 验证成功后得到安全上下文对象,设置到持有者中就可以了
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
—方案二:基于jwt实现

登录后前端保存token,后端在每一次请求来的时候解析token,并把解析的内容(id,auth,role)创建成UserDetail设置到SecurityContextHolder中

public class JwtFilter implements Filter {

    private final AdminJwtUtils jwtUtils;

    public JwtFilter(AdminJwtUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Admin-Token");
        if (StringUtils.isNotBlank(token)){
            // 校验token
            UserDetails user = jwtUtils.parseToken(token);
            // 不为空即为校验通过
            if (user!=null){
                // 手动创建安全上下文,设置到线程域中
                SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
                        user,"", user.getAuthorities()
                ));
            }
        }
        chain.doFilter(request, response);
    }
}

客户端操作流程

-基于会话

  1. 使用用户密码登录后维持session
  2. 调用获取授权码的接口
  3. 收到授权码
  4. 调用兑换accessToken接口,传递客户端id和密码,以及授权码,获取accessToken
  5. 使用accessToken获取用户受保护数据

-基于Jwt

  1. 使用用户密码登录后把Token保存在localstorege
  2. 调用获取授权码的接口,同时传递Token在请求头中,
    1. 后台通过解析jwt维持用户登录状态,认证状态
  3. 收到授权码
  4. 调用兑换accessToken接口,传递客户端id和密码,以及授权码,获取accessToken
  5. 使用accessToken获取用户受保护数据

要点总结

-操作的发起主体分别是谁

  • 在获取授权码的时候[/oauth/authorize],操作的主体是资源所有者,也就是拥有这个资源的用户
  • 在使用授权码兑换access_token的时候[/oauth/token],操作的主体是客户端本体,需要使用客户端在平台注册的身份获取token
  • 在使用access_token访问受保护资源的时候,操作的主体又是资源所有者了,也就是拥有这个资源的用户,因为这时候是客户端使用用户的临时受限身份进行资源的访问

-客户的状态如何保持

  • 如果不进行处理的话,默认是基于会话进行状态保持
  • 这里我使用jwt进行会话状态保持,我会解析jwt里的用户基本信息,然后创建一个安全身份上下文,传入到上下文对象中,这样鉴权过滤器就会识别到这个身份,进行放行,同时校验过滤器也会跳过

-客户端的状态如何保持

客户端是获取access_token的时候,通过表单传递客户端的client_id和client_secret进行身份状态的保持的,
用户同意授权后,并成功兑换accessToken,再次申请相同权限会自动允许

–默认方案

默认使用的是basic auth的方式进行身份认证的
basic auth的认证规范是在请求头中设置Authorization值,
Value内容格式为Basic ${Base64.create(username:password)}
以下是JS代码示例

const username = 'your_username';
const password = 'your_password';

// 将用户名和密码以 "username:password" 的形式拼接,并进行 Base64 编码
const base64Credentials = btoa(`${username}:${password}`);

// 设置请求头,包含 Authorization 字段
const headers = new Headers({
  'Authorization': `Basic ${base64Credentials}`,
  'Content-Type': 'application/json', // 根据你的请求需要设置其他头部
});

// 构建请求对象
const requestOptions = {
  method: 'GET', // 根据你的请求类型设置
  headers: headers,
  // 其他请求选项(例如:body)
};

// 发起请求
fetch('https://api.example.com/resource', requestOptions)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

–自定义方案

在授权配置中重写configure(AuthorizationServerSecurityConfigurer security)添加我们自己定义的过滤器进行身份校验就可以了,校验通过以后同样创建一个UserDetail安全上下文到上下文持有者中就可以了,离谱,没想到和用户居然共用一个类
你可以把客户端的用户名和密码写在请求头里或者body里,然后取出来进行校验

/**
 * 授权服务器配置
 *
 * @author Guochao
 */
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {



    /**
     * 配置自定义的客户端认证过滤器
     * @param security
     * @throws Exception
     */
   @Override
   public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
       security.addTokenEndpointAuthenticationFilter(customJwtTokenFilter);
   }
}

自定义授权页面

https://baijiahao.baidu.com/s?id=1736936966974655693&wfr=spider&for=pc
授权表单的信息是基于Session保持的
也就是发起授权时保存了一个session在浏览器
然后表单提价的时候携带session进行提交,然后处理提交的表单

我们创建一个新的页面覆盖原来的/oauth/confirm_access就可以了
注意@SessionAttributes(“authorizationRequest”)一定不能少,表单是基于session维持会话的

后端部分

这里我们用到了模板引擎

@RestController
@RequestMapping(value = "/oauth")
@SessionAttributes("authorizationRequest")
public class OauthController {

    private final AdopApplicationService adopApplicationService;
    private final AdopScopeService adopScopeService;

    public OauthController(AdopApplicationService adopApplicationService, AdopScopeService adopScopeService) {
        this.adopApplicationService = adopApplicationService;
        this.adopScopeService = adopScopeService;
    }

    @RequestMapping(value = "/confirm_access")
    public ModelAndView userConfirm(Model model) {
        // 这里先提取一下我们传递过来的参数,例如客户端id,state,回调地址等
        final AuthorizationRequest value = (AuthorizationRequest) model.getAttribute("authorizationRequest");
        if (value == null) {
            throw new RuntimeException("无法获取授权请求参数");
        }
        final String clientId = value.getClientId();
        if (StringUtils.isBlank(clientId)) {
            throw new RuntimeException("没有提供客户端参数");
        }
        // 查询一下客户端名称方便页面显示授权方
        final LoadThirdPartyPlatformsDto clientDto = Optional.ofNullable(adopApplicationService.getClientById(Long.valueOf(clientId)))
                .orElseThrow(() -> new RuntimeException("客户端不存在"));
        final Set<String> scope = value.getScope();
        // set转换为list
        final List<AdopScope> scopes = adopScopeService.findByCodes(new ArrayList<>(scope));
        model.addAttribute("client", clientDto.getAppName());
        model.addAttribute("scopeList", scopes);
        return new ModelAndView("userConfirm.html");
    }
}

前端部分

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div class="container">
    <h1>授权认证</h1>
    <p th:text="'是否授权给'+${client}+'使用您的如下资源:'"></p>
    <form id="confirmationForm" name="confirmationForm" action="/open-api/oauth/authorize" method="post"><input
            name="user_oauth_approval" value="true" type="hidden">
        <div id="atr" th:attr="scopeList = ${scopeList}"></div>
        <ul class="scope-list">
            <li class="scope-item" th:each="scopeItem : ${scopeList}">
                <div class="form-group">
                    <span th:text="${scopeItem.scopeName}"></span>
                    <!--                这里的name一定要是'scope.'+scope在资源服务注册的name-->
                    <span class="boxes">
                        <input type="radio" th:name="'scope.'+${scopeItem.scopeCode}" value="true" checked="">允许
                        <input type="radio" th:name="'scope.'+${scopeItem.scopeCode}" value="false">拒绝
                    </span>
                </div>
            </li>
        </ul>
        <label class="btn-container" ><input class="submit" name="authorize" value="授权" type="submit"></label>
    </form>
</div>
</body>
</html>
<style>
    body,html{
        padding: 0;
        margin: 0;
        border: none;
    }
    body{
        display: flex;
        background-color: #efeefc;
        justify-content: center;
        align-items: center;
        height: 100vh;
        color: white;
    }
    .container{
        padding: 20px;
        min-width: 400px;
        border-radius: 10px;
        background-color: #7e75ff;
        box-shadow: 5px 5px 5px rgba(0,0,0,.1);
    }
    h1{
        text-align: center;
    }
    #confirmationForm{
        border: 1px;
        position: relative;
    }

    .scope-list{
        font-size: 18px;
    }
    .scope-item{
        margin: 15px 0;
        font-size: 16px;
    }
    .boxes{
        flex-direction: row;
        display: flex;
        align-items: center;
        font-size: 16px;
    }

    .btn-container{
        display: block;
        min-width: 100%;
        text-align: center;
    }
    .submit{
        bottom: 0px;
        left: calc(50% - 100px);
        border: none;
        width: 200px;
        height: 35px;
        background-color: rgba(255,255,255,.8);
        border-radius: 6px;
        box-shadow: 5px 5px 5px rgba(0,0,0,.1);
    }
</style>

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值