Spring Cloud OAuth2 優雅集成登陸

實現/oauth/token路由下能夠適配全部的登陸類型,自定義參數mysql

0.準備

基於Spring Boot建立項目server-auth
https://start.spring.io/
圖片描述
在pom.xml添加lombok,而且idea安裝了lombok插件(不會安裝,百度一下)git

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

使用idea打開項目,設置配置文件application.propertiesspring

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添加到容器中,不須要用到密碼模式,這步驟能夠跳過sql

@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執行效果
圖片描述服務器

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執行效果
圖片描述app

總結

1.流程思路:經過攔截器IntegrationAuthenticationFilter攔截全部oauth/token請求,根據類型參數(參數名:auth_type)匹配對應認證器(在全部繼承AbstractPreparableIntegrationAuthenticator類中調用support方法篩選),在匹配的成功的認證器調用authenticate方法執行用戶認證處理。
2.擴展其餘登陸方式只要實現自定義的IntegrationAuthenticator就行了。ide

3.項目源碼
https://gitee.com/yugu/cloud-...測試

相關文章
相關標籤/搜索