SpringSecurity OAuth2.0认证授权-part3

在part2中,我们已经完成了授权服务使用内存的操作;现在我们来完善整套微服务的认证授权;

篇幅过长,拆分为:

part1: 认证授权原理回顾及分布式系统认证方案;

part2: oauth2项目搭建、授权方式测试;

part3: 整合jwt、网关、注册中心 完善整套OAuth2.0

------------------------------------------------------------------------------------

3.3 JWT令牌

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。

官网:JSON Web Tokens - jwt.io

标准:RFC 7519 - JSON Web Token (JWT)

JWT令牌的优点:

1)jwt基于json,非常方便解析。

2)可以在令牌中自定义丰富的内容,易扩展。

3)通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

4)资源服务使用JWT可不依赖认证服务即可完成授权。

缺点: 1)JWT令牌较长,占存储空间比较大。

结构信息此处不做过多赘述。

3.3.1 配置JWT令牌服务

在oauth服务中配置jwt令牌服务,即可实现生成jwt格式的令牌。

1、TokenConfig

@Configuration
public class TokenConfig {
​
    private String SIGNING_KEY = "oauth2-Panghl$0";
​
​
    // 令牌存储策略
    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
​
    /***
     * 定义JJwtAccessTokenConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }
​
}

2、 定义JWT令牌服务

 @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
// 令牌管理服务
@Bean
public AuthorizationServerTokenServices tokenService() {
    DefaultTokenServices service=new DefaultTokenServices();
    service.setClientDetailsService(clientDetailsService); // 客户端信息服务
    service.setSupportRefreshToken(true);// 是否产生刷新令牌
    service.setTokenStore(tokenStore);//令牌存储策略
​
    // 令牌增强
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
    service.setTokenEnhancer(tokenEnhancerChain);
​
    service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
    service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
    return service;
}

3.3.2 生成jwt令牌

 

3.3.3 校验jwt令牌

资源服务需要和授权服务拥有一致的签字、令牌服务等:

1、将授权服务中的TokenConfig类拷贝到资源 服务中

2、屏蔽资源 服务原来的令牌服务类

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    public static final String RESOURCE_ID = "res1";
​
    @Autowired
    private TokenStore tokenStore;
​
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID) //资源id
                .tokenStore(tokenStore)
                //.tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    }
​
​
​
​
    /*//资源服务令牌解析服务
    @Bean
    public ResourceServerTokenServices tokenService() {
//使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:8005/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }*/
​
​
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
​
}

3、测试

(1) 生成jwt令牌

2)使用令牌请求资源  

 令牌申请成功可以使用/oauth/check_token校验令牌的有效性,并查询令牌的内容,例子如下:

3.4 完善环境配置

截止目前客户端信息和授权码仍然存储在内存中,生产环境中通过会存储在数据库中,下边完善环境的配置:

3.4.1 db环境的sql在前面(3.2.3)已经提供。

3.4.2 配置授权服务

(1)修改AuthorizationServer:

ClientDetailsService和AuthorizationCodeServices从数据库读取数据。

/**
 * @EnableAuthorizationServer 注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器。
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    //客户端详情服务
    @Autowired
    private ClientDetailsService clientDetailsService;
​
    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;
​
    @Autowired
    private AuthenticationManager authenticationManager;
​
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
​
    @Autowired
    private PasswordEncoder passwordEncoder;
​
    /**
     * 客户端详情服务
     * @param dataSource
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }
​
​
    // 令牌管理服务
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService); // 客户端信息服务
        service.setSupportRefreshToken(true);// 是否产生刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略
​
        // 令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);
​
        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }
​
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        //设置授权码模式的授权码如何存取,暂时采用内存方式
        //return new InMemoryAuthorizationCodeServices();
        return new JdbcAuthorizationCodeServices(dataSource);
    }
​
​
    /**
     * 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在
     * 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
         clients.withClientDetails(clientDetailsService);
        // 暂时使用内存方式
        //clients.inMemory()// 使用in‐memory存储
        //        .withClient("c1")// client_id
        //        .secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
        //        .resourceIds("res1") //资源列表
        //        .authorizedGrantTypes("authorization_code",
        //                "password","client_credentials","implicit","refresh_token")
        //        // 该client允许的授权类型 uthorization_code,password,refresh_token,implicit,client_credentials
        //        .scopes("all")// 允许的授权范围
        //        .autoApprove(false)// false 跳转到授权页面
        //        //加上验证回调地址
        //        .redirectUris("http://www.baidu.com");
    }
​
    /**
     * 用来配置令牌(token)的访问端点和令牌服务(token services)。
     * /oauth/authorize:授权端点。
     * /oauth/token:令牌端点。
     * /oauth/confirm_access:用户确认授权提交端点。
     * /oauth/error:授权服务错误信息端点。
     * /oauth/check_token:用于资源服务访问的令牌解析端点。
     * /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //密码模式需要
                .authorizationCodeServices(authorizationCodeServices) //授权码模式需要
                .tokenServices(tokenService()) // 令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST); //允许POST提交
    }
​
​
    /**
     * 用来配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")  //tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。
                .checkTokenAccess("permitAll()") //checkToken这个endpoint完全公开
                .allowFormAuthenticationForClients();  // 允许表单认证
    }
​
}

3.4.3测试

1、测试申请令牌

使用密码模式申请令牌,客户端信息需要和数据库中的信息一致。

2、测试授权码模式

生成的授权存储到数据库中。

4.Spring Security实现分布式系统授权

4.1 需求分析

回顾技术方案如下:

 

1、UAA认证服务负责认证授权。

2、所有请求经过 网关到达微服务

3、网关负责鉴权客户端以及请求转发

4、网关将token解析后传给微服务,微服务进行授权。

4.2.注册中心

所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。

本节完成注册中心的搭建,注册中心采用Eureka。

1、创建maven工程

 

2、pom.xml依赖如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>oauth2-parent</artifactId>
        <groupId>com.panghl</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>com.panghl</groupId>
    <artifactId>security-eureka</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
​
    </dependencies>
​
</project>

3、配置文件

在resources中配置application.yml

server:
  port: 8001
​
spring:
  application:
    name: security-eureka
​
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://127.0.0.1:${server.port}/eureka/
  instance:
    #    hostname: localhost   #当前eureka实例的主机名
    #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
    prefer-ip-address: true
    #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

启动类:

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

4.3.网关

网关整合 OAuth2.0 有两种思路,一种是认证服务器生成jwt令牌, 所有请求统一在网关层验证,判断权限等操作; 另一种是由各资源服务处理,网关只做请求转发。

我们选用第一种。我们把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当 前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。

API网关在认证授权体系里主要负责两件事:

(1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。

(2)令牌解析并转发当前登录用户信息(明文token)给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:

(1)用户授权拦截(看当前用户是否有权访问该资源)

(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

4.3.1 创建工程

 

1、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>oauth2-parent</artifactId>
        <groupId>com.panghl</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>com.panghl</groupId>
    <artifactId>security-gateway</artifactId>
    <dependencies>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
​
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
​
        <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>
​
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
​
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>
​
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
​
    </dependencies>
​
</project>

2、配置文件

配置application.yml

spring.application.name=security-gateway
spring.main.allow-bean-definition-overriding = true
server.port=8002
logging.level.root = info
logging.level.org.springframework = info
​
zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *
​
zuul.routes.security-auth.stripPrefix = true
zuul.routes.security-auth.path = /uaa/**
​
zuul.routes.security-order.stripPrefix = true
zuul.routes.security-order.path = /order/**
​
eureka.client.serviceUrl.defaultZone = http://localhost:8001/eureka/
eureka.instance.preferIpAddress = true
​
​
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true

启动类:

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

4.3.2 token配置

前面也介绍了,资源服务器由于需要验证并解析令牌,往往可以通过在授权服务器暴露check_token的Endpoint来 完成,而我们在授权服务器使用的是对称加密的jwt,因此知道密钥即可,资源服务与授权服务本就是对称设计, 那我们把授权服务的TokenConfig两个类拷贝过来就行 。

@Configuration
public class TokenConfig {
​
    private String SIGNING_KEY = "oauth2-Panghl$0";
​
​
    // 令牌存储策略
    @Bean
    public TokenStore tokenStore() {
        // 内存方式,生成普通令牌
        //return new InMemoryTokenStore();
        //JWT令牌存储方案
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
​
    /***
     * 定义JJwtAccessTokenConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }
​
}

4.3.3 配置资源服务

在ResouceServerConfig中定义资源服务配置,主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要 什么样的权限才能访问某个微服务,如:

package com.panghl.gateway.config;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
​
/**
 * @Author panghl
 * @Date 2022/2/20
 **/
@Configuration
public class ResourceServerConfig {
    public static final String RESOURCE_ID = "res1";
​
​
    @Configuration
    @EnableResourceServer
    //oauth资源服务配置
    public class AuthServerConfig  extends ResourceServerConfigurerAdapter{
        @Autowired
        private TokenStore tokenStore;
​
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(RESOURCE_ID) //资源id
                    .tokenStore(tokenStore)
                    .stateless(true);
        }
​
​
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                 .antMatchers("/uaa/**").permitAll();
        }
​
    }
​
    @Configuration
    @EnableResourceServer
    //oauth资源服务配置
    public class OrderServerConfig  extends ResourceServerConfigurerAdapter{
        @Autowired
        private TokenStore tokenStore;
​
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(RESOURCE_ID) //资源id
                    .tokenStore(tokenStore)
                    .stateless(true);
        }
​
​
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
        }
    }
​
    //配置其他微服务...
}

上面定义了两个微服务的资源,其中:

UAAServerConfig指定了若请求匹配/uaa/网关不进行拦截。

OrderServerConfig指定了若请求匹配/order/,也就是访问统一用户服务,接入客户端需要有scope中包含 read,并且authorities(权限)中需要包含ROLE_USER。

由于res1这个接入客户端,read包括ROLE_ADMIN,ROLE_USER,ROLE_API三个权限。

4.3.4 安全配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/**").permitAll();
    }
}

4.4 转发明文token给微服务

通过Zuul过滤器的方式实现,目的是让下游微服务能够很方便的获取到当前的登录用户信息(明文token)

(1)实现Zuul前置过滤器,完成当前登录用户信息提取,并放入转发微服务的request中

public class AuthFilter extends ZuulFilter {
​
    @Override
    public boolean shouldFilter() {
        return true;
    }
​
    @Override
    public String filterType() {
        return "pre";
    }
​
    @Override
    public int filterOrder() {
        return 0;
    }
​
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        //从安全上下文中拿 到用户身份对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!(authentication instanceof OAuth2Authentication)){
            return null;
        }
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        //取出用户身份信息
        String principal = userAuthentication.getName();
​
        //取出用户权限
        List<String> authorities = new ArrayList<>();
        //从userAuthentication取出权限,放在authorities
        userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));
​
        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if(userAuthentication!=null){
            jsonToken.put("principal",principal);
            jsonToken.put("authorities",authorities);
        }
​
        //把身份信息和权限信息放在json中,加入http的header中,转发给微服务
        ctx.addZuulRequestHeader("jsonToken", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
​
        return null;
    }
}

(2)将filter纳入spring 容器:

@Configuration
public class ZuulConfig {
​
    @Bean
    public AuthFilter preFileter() {
        return new AuthFilter();
    }
​
    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}
public class EncryptUtil {
    private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
​
    public static String encodeBase64(byte[] bytes){
        String encoded = Base64.getEncoder().encodeToString(bytes);
        return encoded;
    }
​
    public static byte[]  decodeBase64(String str){
        byte[] bytes = null;
        bytes = Base64.getDecoder().decode(str);
        return bytes;
    }
​
    public static String encodeUTF8StringBase64(String str){
        String encoded = null;
        try {
            encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            logger.warn("不支持的编码格式",e);
        }
        return encoded;
​
    }
​
    public static String  decodeUTF8StringBase64(String str){
        String decoded = null;
        byte[] bytes = Base64.getDecoder().decode(str);
        try {
            decoded = new String(bytes,"utf-8");
        }catch(UnsupportedEncodingException e){
            logger.warn("不支持的编码格式",e);
        }
        return decoded;
    }
​
    public static String encodeURL(String url) {
       String encoded = null;
      try {
         encoded =  URLEncoder.encode(url, "utf-8");
      } catch (UnsupportedEncodingException e) {
         logger.warn("URLEncode失败", e);
      }
      return encoded;
   }
​
​
   public static String decodeURL(String url) {
       String decoded = null;
      try {
         decoded = URLDecoder.decode(url, "utf-8");
      } catch (UnsupportedEncodingException e) {
         logger.warn("URLDecode失败", e);
      }
      return decoded;
   }
​
    public static void main(String [] args){
        String str = "abcd{'a':'b'}";
        String encoded = EncryptUtil.encodeUTF8StringBase64(str);
        String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
        System.out.println(str);
        System.out.println(encoded);
        System.out.println(decoded);
​
        String url = "== wo";
        String urlEncoded = EncryptUtil.encodeURL(url);
        String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
        
        System.out.println(url);
        System.out.println(urlEncoded);
        System.out.println(urlDecoded);
    }
​
​
}

4.5. 微服务用户鉴权拦截

当微服务收到明文token时,应该怎么鉴权拦截呢?自己实现一个filter?自己解析明文token,自己定义一套资源 访问策略?

能不能适配Spring Security呢,是不是突然想起了前面我们实现的Spring Security基于token认证例子。咱们还拿 统一用户服务作为网关下游微服务,对它进行改造,增加微服务用户鉴权拦截功能。

资源服务器-->新增过滤器[用于解析网关header中传的加密认证信息,并将认证信息存入SecurityContextHolder]

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //解析出头中的token
        String token = httpServletRequest.getHeader("jsonToken");
​
        if (token != null) {
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            // 将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);
            // 用户身份信息
            UserDTO userDTO = new UserDTO();
            String principal = jsonObject.getString("principal");
            userDTO.setUsername(principal);
            // 用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
​
            //将用户信息和权限填充到用户身份token对象中
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //将authenticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            filterChain.doFilter(httpServletRequest,httpServletResponse);
        }
    }
​
}

修改测试资源请求

@RestController
public class OrderController {
​
    @GetMapping(value = "/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        //获取用户身份信息
        UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDTO.getUsername() + "访问资源1";
    }
​
}

4.6 集成测试

本案例测试过程描述:

1、采用OAuth2.0的密码模式通过网关从授权服务获取token

 2、使用该token通过网关访问订单服务的测试资源

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值