SpringCloud微服务集成oauth2.0权限验证

    在微服务大行其道的今天,出门面试不会整两句springboot、springcloud都不好意思去面试。在java领域springcloud已经是微服务开发的事实标准了,使用springcloud+springboot开发微服务也很简单,但是在在开发过程中微服务的权限验证是个问题,怎么保证各微服务的安全,微服务的权限验证普遍用的都是osuth2.0的验证,但是在oauth2.0种验证方式也有不同:

     1、客户端通过oauth服务器直接获取token,客户端携带token访问微服务、微服务链接oauth服务器验证token:

      2、客户端通过oauth服务器获取jwt,客户端携带jwt访问微服务,微服务自己解析jwt验证权限

第一种使用简单但是有个致命的问题,所有的微服务都要访问ozuth服务器,势必会导致oauth服务器压力过大、所以本文基于第二种配置集成oauth,项目源码github传送https://github.com/qxxg/platform,感觉可以请帮忙点个star

项目模块结构:

 

 项目创建不会的可以自行百度,直接介绍oauth2模块

创建platform-oauth项目添加oauth依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

由于oauth验证会访问数据库还要添加mybatis依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

配置项目文件(这里用的时mysql8.0版本,8.0以下版本的自行修改数据库链接)

 

使用mybatis逆向工程生成数据库访问dao(不会使用逆向工程的可以自行百度或者手写,这里dao访问很简单)

 

接下来时oauth相关类,可以照搬。具体注意以下两个类:

SmsAuthenticator -------------》短信登录验证器
UsernamePasswordAuthenticator --》账号密码验证器

 

这两个类负责数据库的查询和验证。

1、IntegrationAuthenticationContext:
/**
 * @Description: 集成认证上下文
 */

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();
    }
}

2、IntegrationAuthenticationEntity:

/**
 * @Description: 集成认证实体
 */

@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;
    }
}
3、IntegrationAuthenticationFilter:
/**
 * @Description: 集成认证拦截器
 */
@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)});
                }
            }
        }

    }
}

 4、IntegrationUserDetailsService:

/**
 * @Description: 集成认证-用户细节服务
 */
@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 OAuth2Exception("用户名或密码错误");
        }
        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;
    }
}

5、AbstractPreparableIntegrationAuthenticator:

/**
 * @Description: 集成认证-认证器抽象类
 */
public abstract class AbstractPreparableIntegrationAuthenticator implements IntegrationAuthenticator {

    @Override
    public void prepare(IntegrationAuthenticationEntity entity) {

    }

    @Override
    public void complete(IntegrationAuthenticationEntity entity) {

    }
}

6、IntegrationAuthenticator:

/**
 * @Description: 集成认证-认证器接口
 */
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);
}

7、SmsAuthenticator:

/**
 * @Description: 短信认证器
 */
@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());
    }
}

8、UsernamePasswordAuthenticator:

/**
 * @Description: 普通认证器(用户名+密码)
 */
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {

    @Autowired
    private UserMapper mapper;

    @Override
    public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
        String name = entity.getAuthParameter("username");
        String pwd = entity.getAuthParameter("password");
        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());
    }
}

接下来配置oauth授权服务配置,这里使用的时密钥的方式生成jwt,在微服务端使用公钥解析jwt:密钥生成命令

keytool -genkeypair -alias ltd-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=jwt,L=zurich,C=CH" -keypass ltd123 -keystore ltd-jwt.jks -storepass ltd123密钥生成需要使用到ssl现在自己电脑安装ssl如果只是测试下oauth在github项目源码种提供了一个key文件可以直接使用

 

1、AuthorizationServerConfigurer:

/**
 * @Description: 授权服务器配置
 */
@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.tokenStore(tokenStore())
                .accessTokenConverter(jwtAccessTokenConverter())
                .authenticationManager(authenticationManager )
                .userDetailsService(integrationUserDetailsService);
        super.configure(endpoints);
    }

    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * keytool -genkeypair -alias ltd-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=jwt,L=zurich,C=CH" -keypass ltd123 -keystore ltd-jwt.jks -storepass ltd123
     * @return
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        ClassPathResource resource = new ClassPathResource("ltd-jwt.jks");
        KeyStoreKeyFactory ksf = new KeyStoreKeyFactory(resource,"ltd123".toCharArray());
        KeyPair kp = ksf.getKeyPair("ltd-jwt");
        jwtAccessTokenConverter.setKeyPair(kp);
        return jwtAccessTokenConverter;
    }


    @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("web")
                .secret("web-secret")
                .authorizedGrantTypes("password").scopes("web")
                .and()
                .withClient("andorid")
                .secret("andorid-secret")
                .authorizedGrantTypes("password").scopes("adnorid");
        super.configure(clients);
    }
}

这里的JWT不需要保存数据库直接保存在客户端(如果是token的形式验证的话,token一般是存放在redis中)

2、WebSecurityConfigurer:

/**
 * @Description: Security配置
 */
@EnableWebSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
ResourceServerConfig这个类暂时可以不用这是springboot-admin要用的配置类

到这里oauth服务器就写完了。。。可以访问gihub查看源码github传送给,如果感觉可以帮忙点个star

使用postman获取JWT:

参数:

grant_type:password
username:macro
password:macro123
client_id:web
client_secret:web-secret
scope:web

client_id、client_secret和scope需要对应的是AuthorizationServerConfigurer类中的

这里使用的是spring gateway自己测试的时候可以直接访问/oauth/token接口参数不变

测试JWT的有效性:

1、创建测试项目这里使用的是platform-user模块测试

创建的测试项目中配置文件添加:

 

这里由于使用的是JWT所以不需要访问oauth服务器验证所以也就不需要配置oauth

项目添加依赖:

<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>

添加ResourceServerConfig权限验证类、由于oauth用的是密钥的方式生成的JWT所以在微服务中需要用到密钥的公钥解析JWT不然是无法验证的本项目中提供了密钥和公钥可以直接使用

 

使用JWT访问测试模块的接口:

访问成功。这里使用了spring gateway自己测试的时候可以创建一个普通的springboot项目直接测试

这里所有的源码都在gihub上,喜欢的可以下载下来测试学习。项目中包括了微服务中的eureka注册中心、spring gateway网关、oauth2.0安全验证、springboot-admin服务监控、openfeign远程调用、zipkin+Sleuth分布式链路追踪、jta-atomikos分布式事务处理(后期会改成阿里分布式事务seata)等

代码传送

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值