• SpringCloud微服务间安全调用实现(SpringSecurity+Oauth2+Jwt)


    SpringCloud服务间安全调用实现

    目前项目中用到了微服务认证这块的技术,查看了相当一大部分的资料发现现在网上的资料都很不全,很零散。而且很多都运行不起来。

    简单的介绍一下认证。 传统的项目都是使用session来管理用户的登录信息,返回前端sessionId保存在cookie中。但是在分布式情况下利用session去管理的场景就很少了,也不是不可以,如果你对session很热爱使用它的话,那就没必要继续往下看了。

    在我们微服务restful风格下,应该怎么去保证安全信息呢。相信大家可能了解过token,也就是我们熟说的令牌,有人问我什么是token,Token 和 Session ID 不同,并非只是一个 key。Token 一般会包含用户的相关信息,通过验证 Token 就可以完成身份校验。

    这里我们先简单介绍一下我们使用的token格式。

    采用jwt(JSON WEB TOKEN) JWT 是由三段信息构成的,第一段为头部(Header),第二段为载荷(Payload),第三段为签名(Signature)。每一段内容都是一个 JSON 对象,将每一段 JSON 对象采用 BASE64 编码,将编码后的内容用. 链接一起就构成了 JWT 字符串。

    认证协议采用Oauth2.0 OAuth 是一种开放的协议,为桌面程序或者基于 BS 的 web 应用提供了一种简单的,标准的方式去访问需要用户授权的 API 服务。OAUTH 认证授权具有以下特点:

    简单:不管是 OAuth 服务提供者还是应用开发者,都很容易于理解与使用;

    安全:没有涉及到用户密钥等信息,更安全更灵活;

    开放:任何服务提供商都可以实现 OAuth,任何软件开发商都可以使用 OAuth;

    关于jwt和Oauth我们就不做过多的介绍了。大家直接去百度一大堆,这种内容写出来就没有技术含量了。

    思考一个问题:就是在分布式情况下,我们怎么去生成这个token?在哪里生成是最好的呢?

    现在我们举例说明,有三个服务,会员,订单,支付,这个三个服务,他们都需要用户验证,我们不可能在每个系统中去写一个认证过程,所以,我们的认证是一个单独的服务。

    认证流程:用户提供用户信息,到认证中心验证,成功就返回一个token,访问其他服务的时候在请求头携带上token信息,服务只需要去解析token的值就可以了,这样做既满足了微服务轻量级的需求,也避免了了浏览器禁用cookie的情况,岂不美哉。

    我们要知道一个事情,对于我们的认证服务器来说,只有两种服务,那就是认证服务器和资源服务器,所有我们这里的会员,订单,支付服务,对于认证服务器来说都是资源服务器。

    搭建认证服务

    我们采用SpringSecurity Oauth2.0 JWT Redis搭建服务

    采用SpringSecurity的密码模式,其他模式请自行百度参考,这里提供当前认证的最需求的做法。

    pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.40</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.cdhenren.AuthApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    配置文件application.yml

    server:
      port: 8080
    spring:
      redis:
        host: 127.0.0.1
        password: null
        port: 6379
        pool:
          max-idle: 100
          min-idle: 1
          max-active: 1000
          max-wait: -1
    logging:
      level: 
        per.lx: DEBUG
    配置认证服务
    @Configuration
    @EnableAuthorizationServer
    public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
        // 资源ID
        private static final String SOURCE_ID = "order";
        private static final int ACCESS_TOKEN_TIMER = 60 * 60 * 24;
        private static final int REFRESH_TOKEN_TIMER = 60 * 60 * 24 * 30;
     
        @Autowired
        AuthenticationManager authenticationManager;
        @Autowired
        RedisConnectionFactory redisConnectionFactory;
     
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory().withClient("myapp").resourceIds(SOURCE_ID).authorizedGrantTypes("password", "refresh_token")
                    .scopes("all").authorities("ADMIN").secret("lxapp").accessTokenValiditySeconds(ACCESS_TOKEN_TIMER)
                    .refreshTokenValiditySeconds(REFRESH_TOKEN_TIMER);
        }
     
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.accessTokenConverter(accessTokenConverter());
            endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
        }
     
        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            // 允许表单认证
            oauthServer.allowFormAuthenticationForClients();
        }
     
        // JWT
        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
            JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
                /***
                 * 重写增强token方法,用于自定义一些token总需要封装的信息
                 */
                @Override
                public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                    String userName = authentication.getUserAuthentication().getName();
                    // 得到用户名,去处理数据库可以拿到当前用户的信息和角色信息(需要传递到服务中用到的信息)
                    final Map<String, Object> additionalInformation = new HashMap<>();
                    // Map假装用户实体
                    Map<String, String> userinfo = new HashMap<>();
                    userinfo.put("id", "1");
                    userinfo.put("username", "LiaoXiang");
                    userinfo.put("qqnum", "438944209");
                    userinfo.put("userFlag", "1");
                    additionalInformation.put("userinfo", JSON.toJSONString(userinfo));
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                    OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
                    return enhancedToken;
                }
            };
            // 测试用,资源服务使用相同的字符达到一个对称加密的效果,生产时候使用RSA非对称加密方式
            accessTokenConverter.setSigningKey("SigningKey");
            return accessTokenConverter;
        }
     
        @Bean
        public TokenStore tokenStore() {
            RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
            return tokenStore;
        }
     
    }

    安全认证配置

    @Configuration
    @EnableWebSecurity
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
     
        // 请配置这个,以保证在刷新Token时能成功刷新
        @Autowired
        public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
            // 配置用户来源于数据库
                    // 配置密码加密方式  BCryptPasswordEncoder,添加用户加密的时候请也用这个加密
            auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
        }
     
        @Bean
        @Override
        protected UserDetailsService userDetailsService() {
     
            // 这里是添加两个用户到内存中去,实际中是从#下面去通过数据库判断用户是否存在
            InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            
                    BCryptPasswordEncoder passwordEncode = new BCryptPasswordEncoder();
                    String pwd = passwordEncode.encode("123456");
                    manager.createUser(User.withUsername("user_1").password(pwd).authorities("USER").build());
            manager.createUser(User.withUsername("user_2").password(pwd).authorities("USER").build());
            return manager;
     
            // #####################实际开发中在下面写从数据库获取数据###############################
            // return new UserDetailsService() {
            // @Override
            // public UserDetails loadUserByUsername(String username) throws
            // UsernameNotFoundException {
            // // 通过用户名获取用户信息
            // boolean isUserExist = false;
            // if (isUserExist) {
            // //创建spring security安全用户和对应的权限(从数据库查找)
            // User user = new User("username", "password",
            // AuthorityUtils.createAuthorityList("admin", "manager"));
            // return user;
            // } else {
            // throw new UsernameNotFoundException("用户[" + username + "]不存在");
            // }
            // }
            // };
     
        }
     
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http.requestMatchers().anyRequest().and().authorizeRequests().antMatchers("/oauth/**").permitAll();
            // @formatter:on
        }
    }

    到这里一个认证服务器就搭建成功了。

    我们写一个启动类来启动

    @SpringBootApplication
    public class AuthApp {
        public static void main(String[] args) {
            SpringApplication.run(AuthApp.class, args);
        }
    }

    启动后发现控制台输出了我们的请求连接(表明集成成功)

    然后我们用postman进行请求模拟

    请求参数中的信息来自于配置文件,不同的话不能生成成功。用户信息请自行修改代码从数据库获取

    可以看到我们的返回信息中包含的信息,其中有固定的信息和我们自定义的信息。

    顺便看看我们的Redis中存储的认证信息,可以看到redis新增了很多信息

    到这里我们的认证服务就搭建成功了,就可以用来访问其他的服务了 #######################################################################

    我们这里来搭建资源服务器,也就是通过TOKEN去访问的服务

    pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
        <relativePath />
    </parent>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.40</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.cdhenren.AuthApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    新建一个资源服务配置文件ResourceConfiguration.java 我们这里配置以/order/*开头的请求不用认证

    @Configuration
    @EnableResourceServer
    public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
     
        private static final String SOURCE_ID = "order";
     
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
     
        @Override
        @CrossOrigin
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.resourceId(SOURCE_ID).stateless(true);
            resources.tokenServices(defaultTokenServices());
        }
     
        @Override
        public void configure(HttpSecurity http) throws Exception {
            // @formatter:off
                    // 我们这里放开/order/*的请求,以/order/*开头的请求不用认证
            http.authorizeRequests().antMatchers("/order/*").permitAll().and().authorizeRequests()
                    .antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated();
            // @formatter:on
        }
     
        // 自定义的Token存储器,存到Redis中
        @Bean
        public TokenStore tokenStore() {
            RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
            return tokenStore;
        }
     
        // Token转换器
        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
            JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
            };
            accessTokenConverter.setSigningKey("SigningKey");
            return accessTokenConverter;
        }
     
        /**
         * 创建一个默认的资源服务token
         */
        @Bean
        public ResourceServerTokenServices defaultTokenServices() {
            final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            // 使用自定义的Token转换器
            defaultTokenServices.setTokenEnhancer(accessTokenConverter());
            // 使用自定义的tokenStore
            defaultTokenServices.setTokenStore(tokenStore());
            return defaultTokenServices;
        }
    }

    然后我们写一个工具类,去获取我们在认证端传过来的userinfo

    public class AuthUtils {
        public static String getReqUser(HttpServletRequest req) {
            String header = req.getHeader("Authorization");
            String token = StringUtils.substringAfter(header, "bearer");
            Claims claims;
            try {
                claims = Jwts.parser().setSigningKey("SigningKey".getBytes("UTF-8")).parseClaimsJws(token).getBody();
            } catch (Exception e) {
                return null;
            }
            String localUser = (String) claims.get("userinfo");
            // 拿到当前用户
            return localUser;
        }
    }

    到这里我们就配置完了我们的所有请求,我们编写一个Controller进行验证。

    @RestController
    public class TestEndpoints {
        @GetMapping("/product/{id}")
        public String getProduct(@PathVariable String id, HttpServletRequest req) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            System.out.println("用户名  : " + JSON.toJSONString(authentication.getPrincipal()));
            System.out.println("封装的传递信息  : " + AuthUtils.getReqUser(req));
            return "(Need Auth Request)product id : " + id;
        }
     
        @GetMapping("/order/{id}")
        public String getOrder(@PathVariable String id) {
            return "(No Auth Request)order id : " + id;
        }
    }

    这下我们就测试完成了 首先访问我们不需要认证的请求:127.0.0.1:8081/order/1,可以正常访问返回数据

    然后不带我们的token进行访问另外一个链接(需要认证):127.0.0.1:8081/product/1

    可以看到返回没有认证不能访问的提示,如下图

    下面我们在请求头里面带上我们的token,这里注意一下,token是携带在Header中的Authorization属性中,而且我们需要用token类型+token的方式进行传递,这里我们的类型默认是bearer,加上我们的请求头就可以正常访问到数据了。如下图

    现在,我们可以直接拿去集成到我们真实的微服务项目中去了,保证我们的项目从这一篇博客开始。

    如果对你有用,请记得点赞。

  • 相关阅读:
    HDU5032 Always Cook Mushroom(树状数组&&离线)
    vue proxyTable
    vue-bus 组件通信插件
    gulp 静态资源版本控制
    js运算【按位非】~
    JS 的引用赋值与传值赋值
    手机端取消长按选中
    无刷新URL 更新
    移动端设计稿尺寸(微信端)
    4105: [Thu Summer Camp 2015]平方运算
  • 原文地址:https://www.cnblogs.com/yifanSJ/p/16338298.html
Copyright © 2020-2023  润新知