Spring Security Oauth2关于自定义登录的几种解决方案(一)

Spring Security Oauth2关于自定义登录的几种解决方案(一)

目前大部分公众号很少有直观的去写如何集成多种登录的解决方案,例如:短信登录,微信扫码登录,双用户表登录等,图形验证码相对简单,可以通过多种方案进行解决,本文讲述都为REST请求。去除使用security默认的表单登录方式。

第一种:实现AuthenticationProvider完成校验

AuthenticationProvider接口主要有两个方法,

package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
//认证处理器
public interface AuthenticationProvider {

    //这里主要做相关校验,例如密码校验,短信验证码校验等,最终返回一个认证过的AuthenTicationToken
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	//这里主要负责是否需要该验证器进行校验
	boolean supports(Class<?> authentication);
}

在默认调用的/oauth/token接口的源码中,可以查看org.springframework.security.authentication.ProviderManager类中的authenticate方法,其中通过循环所有的AuthenticationProvider 实现类,通过调用supports决定是否要采用该验证器进行校验,否则将进入下一个验证器。

原理:AuthenticationProvider通常按照认证请求链顺序去执行,一个返回非null响应表示程序验证通过, 不再尝试验证其它的provider;如果后续提供的身份验证程序 成功地对请求进行身份认证,则忽略先前的身份验证异常及null响应,并将使用成功的身份验证。如果没有provider提供一个非null响应,或者有一个新的抛出AuthenticationException,那么最后的AuthenticationException将会抛出。

由于Oauth2源码中最终需要获取一个验证过的Oauth2Authentication对象,参考org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter的getOAuth2Authentication方法,默认创建了一个UsernamePasswordAuthenticationToken,所以我们在上述使用supports方法的时候,只能去适配这个Token。
贴上一个我自己写的后台admin-password登录代码以及admin-sms登录代码

package com.example.customoauth.authentication.admin;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

/**
    后端短信认证校验
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/22 15:51
 */
@Component
public class AdminSmsAuthenticationProvider implements AuthenticationProvider {

	//由于我有两个用户表,所以针对不同的userDetailservice需要指定
    @Autowired
    @Qualifier("adminUserDetailsService")
    private UserDetailsService adminUserDetailsService;

    //认证方法
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken adminLoginToken = (UsernamePasswordAuthenticationToken) authentication;
        System.out.println("===进入AdminSMS登录验证环节====="+ JSON.toJSONString(adminLoginToken));
        UserDetails userDetails = adminUserDetailsService.loadUserByUsername(adminLoginToken.getName());
        //匹配短信验证码。进行密码校验,最终需要返回一个验证过的Token
        if("666666".equals(authentication.getCredentials().toString())){
            return  new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        }
        throw  new BadCredentialsException("验证码不正确");
    }

    //判断是否支持当前Token,从而决定是否需要进行认证校验
    //由于oauth2默认采用的userNamePasswordToken所以在最简方式中,可以依然采用
    @Override
    public boolean supports(Class<?> authentication) {
    	//为了简化代码,默认采用UsernamePasswordAuthenticationToken
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

admin-sms短信认证方式:

package com.example.customoauth.authentication.admin;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
    后端短信认证校验
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/22 15:51
 */
@Component
public class AdminPwdAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    @Qualifier("adminUserDetailsService")
    private UserDetailsService adminUserDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    //认证方法
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken adminLoginToken = (UsernamePasswordAuthenticationToken) authentication;
        System.out.println("===进入Admin密码登录验证环节====="+ JSON.toJSONString(adminLoginToken));
        UserDetails userDetails = adminUserDetailsService.loadUserByUsername(adminLoginToken.getName());
        //matches方法,前面为明文,后续为加密后密文
        //匹配密码。进行密码校验
        if(passwordEncoder.matches(authentication.getCredentials().toString(),userDetails.getPassword())){
            return  new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        }
        throw  new BadCredentialsException("用户名密码不正确");
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

最后,只需要将这两个provide 放入authenticationmanage即可

package com.example.customoauth.config;

import com.example.customoauth.authentication.admin.AdminPwdAuthenticationProvider;
import com.example.customoauth.authentication.admin.AdminSmsAuthenticationProvider;
import com.example.customoauth.services.SecurityUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/8 16:13
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String[] excludedAuthPages = {
            "/login/**",
           // "/login*",
    };
    @Override
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    @Autowired
    private SecurityUserDetailsService securityUserDetailsService;

    @Autowired
    private AdminSmsAuthenticationProvider adminSmsAuthenticationProvider;

    @Autowired
    private AdminPwdAuthenticationProvider adminPwdAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	//此处注入一个默认的service,也可以不注入,我做测试时使用,根据需求,该service非admin-userservice
        auth.userDetailsService(securityUserDetailsService);
        //添加两个Provider
        auth.authenticationProvider(adminSmsAuthenticationProvider);
        auth.authenticationProvider(adminPwdAuthenticationProvider);
    }


    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().contentTypeOptions().disable()
                .frameOptions().sameOrigin()
                .and()
                .authorizeRequests()
                .antMatchers(excludedAuthPages).permitAll()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .and().authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin().disable()
                .exceptionHandling()
                .and().csrf().disable()
                .logout().disable().authorizeRequests();
    }
}

接下来,我们看以下登录接口。默认我们可以采用/oauth/token进行获取accessToken,具体可以参考org.springframework.security.oauth2.provider.endpoint.TokenEndpoint 的源码,里面写了整个获取流程,我们自定义也可以做一部分调整。tokenEndpoint为请求入口。

package com.example.customoauth.web;

import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;

import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/9 16:44
 */
@RestController
@RequestMapping("/")
public class LoginController {

    @Autowired
    private OAuth2ClientProperties oauth2ClientProperties;
    @Autowired
    private TokenEndpoint tokenEndpoint;

    @PostMapping("/login/admin")
    public OAuth2AccessToken adminLogin(UserRequest request) throws HttpRequestMethodNotSupportedException {
        //创建客户端信息,客户端信息可以写死进行处理,因为Oauth2密码模式,客户端双信息必须存在,所以伪装一个
        //如果不想这么用,需要重写比较多的代码
        User  clientUser= new User(oauth2ClientProperties.getClientId(),oauth2ClientProperties.getClientSecret(), new ArrayList<>());
        //生成已经认证的client
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(clientUser,null, new ArrayList<>());
        Map<String, String> parameters = new HashMap<String, String>();
        //封装成一个UserPassword方式的参数体,放入手机号
        parameters.put("username", request.getPhone());
        //放入验证码
        parameters.put("password", request.getVcode());
        //授权模式为:密码模式
        parameters.put("grant_type", "password");
        //调用自带的获取token方法。
        OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(token, parameters).getBody();
        return oAuth2AccessToken;
    }

	//对外接口请求
    @Data
    public class UserRequest {
        private String phone;
        private String password;
        private String randomid;
        private String vcode;
    }

}

以上就是一个用最简单的方案,实现多种登录模式的案例,重点就是巧妙的应用AuthenticationProvider,通过封装oauth2认证中需要的参数,实现自定义的认证方式。

后续的文章,会写另外比较复杂的模式,修改tokenGrante,从而达到token分离,不使用UserPasswordAuthenticationToken,采用自定义token进行相关认证操作。

以上解决方案,总体感觉还是有点投机取巧了一点,正确的方式,还是应当自定义拓展Oauth2的验证模式,以下参考文章就是这么做的,采用自定义模式,进行不同的认证校验,感觉更加规范。因为从代码逻辑严整性来看,短信验证码并无密码,不应当采用密码模式来进行相关处理,总觉得有点不规范。自己也是通过伪造信息来达到自己想要的验证效果。

接下来我将持续更新自己总结的Spring security oauth2的干货。拒绝生搬硬套,拒绝能用主义,我将给大家呈现的是实际项目中的实际写法,授权服务是一个很严谨庞大的服务,整个权限体系非常多。例如微信开放平台,目前我司也在成立一个开放平台,现在目前用的仅仅是spring security,这次改造成security oauth2 也需要调整不少东西,所以仔细阅读了大部分的文档,从而弄清整个环节,有合适的文章,我会继续完善更新。谢谢

参考他人文章:
Spring Security如何优雅的增加OAuth2协议授权模式

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值