前后端分离版的oauth2.0第三方登录,做个总结

场景

1.提供登录服务的平台adv
2.第三方app生成state通过拉起adv传输客户端id等信息,同时保留satate
3.adv调用授权码发送api,向第三方app的服务器发送授权码和satate
4.第三方app向自己服务器查询授权码。通过授权码访问鉴权服务器获取access_token
5.第三方app通过access_token访问受保护资源[资源配置需要关闭csrf]

搭建过程

adv

由于本案例中通过平台进行内网访问鉴权服务器,发送授权码,所以需要adv准备一个api向授权服务器发送授权码获取指令
同事adv平台没有密码登录,选择使用短信登录,使用jwt维持登录状态

向授权服务器发送申请授权码请求

/**
 * 发送授权码申请
 * @return
 */
public Object getAuthorizationCode(GetAuthorizationCodeParam param) {
     String requestParam = "?" +
             "client_id=" + param.getClient_id() +
             "&redirect_uri=" + param.getRedirect_uri() +
             "&response_type=" + param.getResponse_type() +
             "&scope=" + param.getScope() +
             "&state=" + param.getState();
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization",req.getHeader("Authorization"));
    headers.setContentType(MediaType.APPLICATION_JSON);
    restTemplate.exchange(authorizationServer+requestParam, HttpMethod.GET,new HttpEntity<>(null,headers),String.class);
    return null;
}

授权和资源

授权服务器

授权服务器,这里我将授权和资源全部放在一个服务器,记得分开的时候,SecurityConfig两个服务都需要配置,用户的登录和授权码的发送也是在这里完成

SecurityConfig

Security的配置文件,里面进行Security的基本配置

@Configuration
@EnableWebSecurity // 启动WebSecurity[可以写在配置类]
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private final CustomJwtTokenFilter customJwtTokenFilter;
    @Resource
    private final UserLoad userLoad;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  // 为了测试暂时关闭
                .authorizeRequests()// 配置认证请求
                .antMatchers("/oauth/**", "/user/**")  // 目前只开放鉴权入口;
                .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);
    }

    /**
     * TODO 为了使用前后端分离,自定义登录逻辑
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置用户加载类,以及加密方案
        auth.userDetailsService(userLoad)  // 用户加载类
                // 这里不使用默认。使用一个自定义的方法
                .passwordEncoder(new CustomJwtTokenEncoder());
    }

    // 当出现无法注入bean【AuthenticationManager】时添加
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
AuthorizationServerConfig

授权服务的配置,里面对授权服务进行基本的配置
此例基于内存搭建,合作商数量大以后可以使用jdbc搭建

@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    // 配置客户端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 加载合作伙伴应用的信息
        // TODO 正式环境使用这个,现在方便测试使用基于内存的配置 clients.jdbc(),账号密码数据也可以查数据库
        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) // 绝对自动授权
        ;
    }
}
AuthorizationServerConfig 加入数据库基于内存版配置
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AdopApplicationService adopApplicationService;
    // 配置客户端
    @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).map(AdopScopeDto::getScopeCode).toArray(String[]::new));
            }

            // 授权类型list
            List<AdopGrantType> grantTypes = partyPlatform.getGrantTypes();
            if (CollUtil.isNotEmpty(grantTypes)){
                builder.authorizedGrantTypes(grantTypes.stream().map(AdopGrantType::getGrantTypeCode).toArray(String[]::new));
            }
        }
    }
}

使用ClientDetails进行管理,即时请求数据库加载,有现成的教程我就不写了。直接贴连接
博客:https://www.cnblogs.com/charlypage/p/9383420.html

CustomJwtTokenFilter
@Component
public class CustomJwtTokenFilter extends OncePerRequestFilter {


    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private JwtUtils jwtUtils;


    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException, IOException {
        log.info("token filter执行了");
        String token = req.getHeader("Authorization");
        if (StringUtils.isNotBlank(token)) {
            // 这里的判定是为了兼容鉴权服务和资源服务的放行,如果是直接访问鉴权和资源的就进行放行,bearer是使用accessToken进行访问时使用的token,basic是资源访问时使用的商户token
            // 资源鉴权分离式中仅需鉴权配置bearer即可,,资源中不需要用户登录所以无需配置此配置
            if (!(token.contains("Basic") || token.contains("basic") ||
                    token.contains("Bearer") || token.contains("bearer"))) {
                User user = jwtUtils.checkToken(token);
                // 不为空再进行安全上下文的生成和赋予;如果为空直接放行,下一个过滤器会收拾他,不过不要修改加解密bean
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getId(), "success");
                // 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
                Authentication authenticate = authenticationManager.authenticate(authenticationToken);
                // 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
                SecurityContextHolder.getContext().setAuthentication(authenticate);
            }
        }
        chain.doFilter(req, resp);
    }
}

稍后配置文件中把过滤器进行排序
 http.addFilterBefore(customJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);

资源服务器

SecurityConfig

此处同上,根据自己的情况进行修改,需要拦截所有请求,然后在下面的配置中进行单独配置

ResourceServerConfig

资源服务配置,这里需要做的就是首先对每个资源配置不同的空间限制,然后到下面对资源进行开放访问,当然想访问必须要有accesstoken

@Configuration
@EnableResourceServer  // 启用资源服务
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 配置带资源域限制的资源信息
                .antMatchers("/userinfo/**").access("#oauth2.hasAnyScope('userinfo','all')")
                .antMatchers("/resource/**").access("#oauth2.hasAnyScope('resource','all')")
                .and()
                // 匹配资源,对上面的资源进行匹配地址,配置在里面的资源将受到保护,必须全部认证才能访问
                .requestMatchers().antMatchers("/resource/**")
                                    // 上面配置了这个资源的访问权限。这里依然需要配置保护
                                  .antMatchers("/userinfo/**")
                .and()
                // 指定任何请求,设r任何请求都需要授权以后才能访问
                .authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable(); // 资源需要关闭这个,否则第三方拿到token以后依然无法访问会被csrf拦截
    }
}

一些当前场景的奇怪自定义配置

UserLoad

这个是实现 UserDetailsService的类,需要实现用户信息的加载,返回一个用户详情对象;
这里由于我稍后的自定义密码校验并非使用密码。所以这里直接把查询到的user设置进new UserDetail里面

 @Override
 public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
     User user = userService.getUserById(Long.valueOf(userId));
     if (ObjectUtil.isEmpty(user))
         throw new UserNotFoundException(500, RespCodeEnum.USER_NOT_EXIST.getCode(), "登录的用户不存在");
     // 这里给的用户详情稍后会被authenticationManager调用,
     // 这里使用的加密方法直接会调用到加密,这里我们不使用加密方法所以无需调用,需要改变
     //return new UserDetail(passwordEncoder.encode(), user.getName(), user.getAuth());
 UserDetail userDetail = new UserDetail(user);
        userDetail.setClientId(123L);
        // 这里把Client_id写入进去,此时匹配的详情上下文是和token对应的,稍后代表当前用户身份过来取数据的也是这个对象,每个用户针对每个client每一个token都会有一个详情对象,独立内存地址
        userDetail.setClientId(Long.valueOf(req.getParameterMap().get("client_id")[0]));
        return userDetail;
 }

在这里插入图片描述

CustomJwtTokenEncoder

这个是自定义的加密方法实现了PasswordEncoder接口,实现两个一个加密一个解密方法
由于上下文持有者就是要调用这个方法,而我又没有使用密码登录,于时自己写了一个;
在密码校验部分仅仅校验是否在前面jwt中给他写入了一个sucecss;总觉得直接返回true不好于是我这样

@Slf4j
public class CustomJwtTokenEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return null;
    }

    /**
     * 解密,解密的时候会调用这个方法进行解密,这里完成token的实现
     * @param charSequence  前面jwt过滤器塞进来的处理结果
     * @param detailGetPwd 这里是从UserDetail的getPassword获取到的数据
     * @return  校验结果
     */
    @Override
    public boolean matches(CharSequence charSequence, String detailGetPwd) {
        // 流程中如果jwt校验通过就会在本该输入密码的地方写入success这里如果出现同样的就放行,按理说不该到这里的
        // 这里写这个是为了后续有人更改放行权限的时候放入了不该有的请求,进入了密码校验,如果什么都不写直接就会放行,如果使用默认的加密
        // 就得拿点东西敷衍它
        return "success".equals(charSequence.toString());
    }
}

执行流程

获取授权码

1.获取授权码的时候请求被customjwttokenfilter过滤器拦截,此时携带了平台的token,调用平台校验通过,打包校验对象UsernamePasswordAuthenticationToken,写入一个"success"给它,方便一会在解密里判断是否通过了jwt校验(常规流程这里校验对象应该是写username和密码)
2.将创建好的校验数据传输给authenticationManager.authenticate(),方法中先会调用loaduser方法传入username[principal参数],加载用户,这里就需要和我们的loadUser进行配合,根据你加载用户的方法进行传输,,,加载完成后会调用解密方法进行解密传入paswword[credentials参数]。这里也需要我们根据实际情况进行解密的配置,这里我只是检测一下是不是被jwt校验给写入了success;
3.校验通过获取到了安全上下文对象,将安全上下文传入安全上下文管理员,完成用户的登录;
4.进入UsernampassowrdFiler,检测到登录状态,放行
5.进行授权码的发放

授权码兑换access_token

1.第三方平台访问授权码兑换携带bearer token,tokenfilter被放行
2.进入pwdfilter被放行,进行code的校验,通过,匹配用户,根据用户生成actoken进行返回

授权码访问资源

1.携带商家token basice token,tokenfilter检测到basic字段,进行放行;
2.进入pwdfilter被放行,到达actoken校验,校验通过,匹配安全上下文对象,匹配身份信息,配置用户详情对象到安全上下文中
3.请求受保护资源,匹配资源url配置的scop信息,匹配通过,允许访问

待续…

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值