Spring Cloud OAuth2 优雅集成登录

实现/oauth/token路由下可以适配所有的登录类型,自定义参数

0.准备

基于Spring Boot创建项目server-auth
https://start.spring.io/
《Spring Cloud OAuth2 优雅集成登录》
在pom.xml添加lombok,并且idea安装了lombok插件(不会安装,百度一下)

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

使用idea打开项目,设置配置文件application.properties

server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/cloud-auth?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

1.定义集成认证实体

@Data
public class IntegrationAuthenticationEntity {
    private String authType;//请求登录认证类型
    private Map<String,String[]> authParameters;//请求登录认证参数集合

    public String getAuthParameter(String paramter){
        String[] values = this.authParameters.get(paramter);
        if(values != null && values.length > 0){
            return values[0];
        }
        return null;
    }
}

2.定义集成认证-认证器接口

public interface IntegrationAuthenticator {

    /**
     * 处理集成认证
     * @param entity    集成认证实体
     * @return 用户表实体
     */
    UserPojo authenticate(IntegrationAuthenticationEntity entity);

    /**
     * 预处理
     * @param entity    集成认证实体
     */
    void prepare(IntegrationAuthenticationEntity entity);

    /**
     * 判断是否支持集成认证类型
     * @param entity    集成认证实体
     */
    boolean support(IntegrationAuthenticationEntity entity);

    /**
     * 认证结束后执行
     * @param entity    集成认证实体
     */
    void complete(IntegrationAuthenticationEntity entity);
}

3.定义集成认证-认证器抽象类

public abstract class AbstractPreparableIntegrationAuthenticator implements IntegrationAuthenticator {

    @Override
    public void prepare(IntegrationAuthenticationEntity entity) {
        
    }
    
    @Override
    public void complete(IntegrationAuthenticationEntity entity) {

    }
}

4.定义集成认证上下文

public class IntegrationAuthenticationContext {
    private static ThreadLocal<IntegrationAuthenticationEntity> holder = new ThreadLocal<>();

    public static void set(IntegrationAuthenticationEntity entity){
        holder.set(entity);
    }

    public static IntegrationAuthenticationEntity get(){
        return holder.get();
    }

    public static void clear(){
        holder.remove();
    }
}

5.定义集成认证拦截器

@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
    private static final String AUTH_TYPE_PARM_NAME = "auth_type";//登录类型参数名
    private static final String OAUTH_TOKEN_URL = "/oauth/token";//需要拦截的路由
    private RequestMatcher requestMatcher;
    private ApplicationContext applicationContext;
    private Collection<IntegrationAuthenticator> authenticators;

    public IntegrationAuthenticationFilter() {
        this.requestMatcher = new OrRequestMatcher(
                new AntPathRequestMatcher(OAUTH_TOKEN_URL, "GET"),
                new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST")
        );
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (requestMatcher.matches(request)){
            RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request);
            if (requestParameterWrapper.getParameter("password") == null){
                requestParameterWrapper.addParameter("password","");
            }
            IntegrationAuthenticationEntity entity = new IntegrationAuthenticationEntity();
            entity.setAuthType(requestParameterWrapper.getParameter(AUTH_TYPE_PARM_NAME));
            entity.setAuthParameters(requestParameterWrapper.getParameterMap());
            IntegrationAuthenticationContext.set(entity);
            try {
                this.prepare(entity);
                filterChain.doFilter(requestParameterWrapper,servletResponse);
                this.complete(entity);
            } finally {
                IntegrationAuthenticationContext.clear();
            }
        }
        else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 认证前回调
     * @param entity    集成认证实体
     */
    private void prepare(IntegrationAuthenticationEntity entity) {
        if (entity != null){
            synchronized (this){
                Map<String, IntegrationAuthenticator> map = applicationContext.getBeansOfType(IntegrationAuthenticator.class);
                if (map != null){
                    this.authenticators = map.values();
                }
            }
        }
        if (this.authenticators == null){
            this.authenticators = new ArrayList<>();
        }
        for (IntegrationAuthenticator authenticator : this.authenticators){
            if (authenticator.support(entity)){
                authenticator.prepare(entity);
            }
        }
    }

    /**
     * 认证结束后回调
     * @param entity    集成认证实体
     */
    private void complete(IntegrationAuthenticationEntity entity) {
        for (IntegrationAuthenticator authenticator: authenticators) {
            if(authenticator.support(entity)){
                authenticator.complete(entity);
            }
        }
    }

    /**
     * 用途:在拦截时给Request添加参数
     * Cloud OAuth2 密码模式需要判断Request是否存在password参数,
     * 如果不存在会抛异常结束认证
     * 所以在调用doFilter方法前添加password参数
     */
    class RequestParameterWrapper extends HttpServletRequestWrapper {
        private Map<String, String[]> params = new HashMap<String, String[]>();

        public RequestParameterWrapper(HttpServletRequest request) {
            super(request);
            this.params.putAll(request.getParameterMap());
        }

        public RequestParameterWrapper(HttpServletRequest request, Map<String, Object> extraParams) {
            this(request);
            addParameters(extraParams);
        }

        public void addParameters(Map<String, Object> extraParams) {
            for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
                addParameter(entry.getKey(), entry.getValue());
            }
        }

        @Override
        public String getParameter(String name) {
            String[]values = params.get(name);
            if(values == null || values.length == 0) {
                return null;
            }
            return values[0];
        }

        @Override
        public String[] getParameterValues(String name) {
            return params.get(name);
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return params;
        }

        public void addParameter(String name, Object value) {
            if (value != null) {
                if (value instanceof String[]) {
                    params.put(name, (String[]) value);
                } else if (value instanceof String) {
                    params.put(name, new String[]{(String) value});
                } else {
                    params.put(name, new String[]{String.valueOf(value)});
                }
            }
        }

    }
}

6.定义用户表实体

@Data
public class UserPojo implements Serializable {

    private Integer id;
    private String name;
    private String mobile;
    private String mail;
    private String pwd;

    public UserPojo() {
    }
}

7.集成认证-用户细节服务

@Service
public class IntegrationUserDetailsService implements UserDetailsService {

    private List<IntegrationAuthenticator> authenticators;

    @Autowired(required = false)
    public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {
        this.authenticators = authenticators;
    }

    @Override
    public UserDetails loadUserByUsername(String str) throws UsernameNotFoundException {
        IntegrationAuthenticationEntity entity = IntegrationAuthenticationContext.get();
        if (entity == null){
            entity = new IntegrationAuthenticationEntity();
        }
        UserPojo pojo = this.authenticate(entity);
        if (pojo == null){
            throw new UsernameNotFoundException("登录失败");
        }
        User user = new User(pojo.getName(),pojo.getPwd(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROOT_USER"));
        return user;
    }

    private UserPojo authenticate(IntegrationAuthenticationEntity entity) {
        if (this.authenticators != null) {
            for (IntegrationAuthenticator authenticator : authenticators) {
                if (authenticator.support(entity)) {
                    return authenticator.authenticate(entity);
                }
            }
        }
        return null;
    }
}

8.启用Security

项目需要用到密码模式所以将AuthenticationManager添加到容器中,不需要用到密码模式,这步骤可以跳过

@EnableWebSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

9.启用授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private IntegrationUserDetailsService integrationUserDetailsService;

    //这里true,使全局密码结果为true,因为有些登录类型不需要验证密码,比如验证码登录,第三方系统登录等等,所以需要认证密码的要单独认证
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return "";
            }
            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return true;
            }
        };
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(integrationUserDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .authorizedGrantTypes("password")
                .secret("server")
                .scopes("all");
    }
}

10.创建数据库colue-auth,执行SQL语句

数据库名:colue-auth,不是colue_auth

CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `name` varchar(100) NOT NULL COMMENT '昵称',
    `mobile` varchar(100) NOT NULL COMMENT '手机号',
    `mail` varchar(100) NOT NULL COMMENT '邮箱',
    `pwd` varchar(100) NOT NULL COMMENT '密码',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8 COMMENT='用户表';

INSERT INTO user VALUES(NULL,'root','13555555555','10086@qq.com','$2a$10$hcMi5tIUGGGim/Xe0Z7q4e5Zz3QlK.EAek3an3nZf0B.ZdN0GJgSe')

11.定义用到UserMapper

@Mapper
public interface UserMapper {

    @Select("SELECT * FROM user WHERE name = #{name}")
    public UserPojo findByName(String name);

    @Select("SELECT * FROM user WHERE mobile = #{mobile}")
    public UserPojo findByMobile(String mobile);

    @Select("SELECT * FROM user WHERE mail = #{mail}")
    public UserPojo findByMail(String mail);
}

12.定义密码登录认证器

@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {

    @Autowired
    private UserMapper mapper;

    @Override
    public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
        String name = entity.getAuthParameter("name");
        String pwd = entity.getAuthParameter("pwd");
        if(name == null || pwd == null){
            throw new OAuth2Exception("用户名或密码不能为空");
        }
        UserPojo pojo = mapper.findByName(name);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        if(encoder != null && encoder.matches(pwd,pojo.getPwd())){
            return pojo;
        }
        return null;
    }

    @Override
    public boolean support(IntegrationAuthenticationEntity entity) {
        return StringUtils.isEmpty(entity.getAuthType());
    }
}

Postman执行效果
《Spring Cloud OAuth2 优雅集成登录》

13.短信登录认证器

@Component
public class SmsAuthenticator extends AbstractPreparableIntegrationAuthenticator {

    private final static String AUTH_TYPE = "sms";
    @Autowired
    private UserMapper mapper;

    @Override
    public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
        String mobile = entity.getAuthParameter("mobile");
        if(StringUtils.isEmpty(mobile)){
            throw new OAuth2Exception("手机号不能为空");
        }
        String code = entity.getAuthParameter("code");
        //测试项目,所以将验证码顶死为:1234
        if(! "1234".equals(code)){
            throw new OAuth2Exception("验证码错误或已过期");
        }
        return mapper.findByMobile(mobile);
    }

    @Override
    public boolean support(IntegrationAuthenticationEntity entity) {
        return AUTH_TYPE.equals(entity.getAuthType());
    }
}

Postman执行效果
《Spring Cloud OAuth2 优雅集成登录》

总结

1.流程思路:通过拦截器IntegrationAuthenticationFilter拦截所有oauth/token请求,根据类型参数(参数名:auth_type)匹配对应认证器(在所有继承AbstractPreparableIntegrationAuthenticator类中调用support方法筛选),在匹配的成功的认证器调用authenticate方法执行用户认证处理。
2.扩展其他登录方式只要实现自定义的IntegrationAuthenticator就好了。

3.项目源码
https://gitee.com/yugu/cloud-…

    原文作者:裕谷
    原文地址: https://segmentfault.com/a/1190000016527822
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞