泽兴芝士网

一站式 IT 编程学习资源平台

Spring Cloud实战 | 第六篇:Spring Cloud 实现微服务统一认证授权

Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权

1、ljf-admin添加数据库表和数据

CREATE TABLE oauth_client_details ( client_id varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, resource_ids varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, client_secret varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, scope varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, authorized_grant_types varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, web_server_redirect_uri varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, authorities varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, access_token_validity int(11) NULL DEFAULT NULL, refresh_token_validity int(11) NULL DEFAULT NULL, additional_information varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, autoapprove varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (client_id) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO oauth_client_details VALUES ('ljf-admin', NULL, '123456', 'all', 'password,refresh_token', '', NULL, NULL, NULL, NULL, NULL); 

2、生成CRUD

3、ljf-admin模块提供client查询接口

@RestController
@RequestMapping("/api/v1/Oauth-client")
public class OauthController {


    @Autowired
    private OauthClientDetailsService oauthClientDetailsService;


    @GetMapping("/{clientId}")
    public Result detail(@PathVariable String clientId) {
        OauthClientDetails client = oauthClientDetailsService.selectByPrimaryKey(clientId);
        return Result.success(client);
    }
}

4、ljf-admin-api提供feign接口

@FeignClient(value = "ljf-admin")
public interface OAuthClientFeign {


    @GetMapping("/api/v1/Oauth-client/{clientId}")
    Result getOAuthClientById(@PathVariable(value = "clientId") String clientId);
}

5、ljf-auth服务测试访问feign接口

6、ljf-auth添加依赖


    org.springframework.cloud
    spring-cloud-starter-oauth2




    org.springframework.security
    spring-security-oauth2-jose

7、安全拦截配置

/**
* 安全配置主要是配置请求访问权限、定义认证管理器、密码加密配置
*/
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * http安全配置
     *
     * @param http http安全对象
     * @throws Exception http安全异常信息
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/oauth/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf()
                .disable();
    }


    /**
     * 如果不配置SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

8、认证授权配置

/**
* @Auther: lijinfeng
* @Date: 2021/12/8
* @Description 描述:认证服务配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private ClientDetailsServiceImpl clientDetailsService;


    // 认证管理器 WebSecurityConfig 中创建bean
    @Autowired
    private AuthenticationManager authenticationManager;


    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    /**
     * 客户端信息配置:client存储方式
     */
    @Override
    @SneakyThrows
    public void configure(ClientDetailsServiceConfigurer clients) {
        System.out.println("ljf-auth:AuthorizationServerConfig::configure::OAuth2客户端【数据库加载】");
        clients.withClientDetails(clientDetailsService);
    }


    /**
     * 配置授权(authorization)
     * 以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        System.out.println("ljf-auth:配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)");
        //新建TokenEnhancerChain类,为了整和token增强的多个配置,一次性注入endpoints中
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer()); //对token内容进行增强
        tokenEnhancers.add(jwtAccessTokenConverter()); //token使用非对称加密算法
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);


        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain) //将令牌增强器注入endpoints中
                .userDetailsService(userDetailsService) //用户认证
                // refresh token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
                //      1 重复使用:access token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
                //      2 非重复使用:access token过期刷新时, refresh token过期时间延续,在refresh token有效期内刷新便永不失效达到无需再次登录的目的
                .reuseRefreshTokens(true);
    }


    /**
     * 使用非对称加密算法对token签名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {


        System.out.println("从classpath下的密钥库中获取密钥对(公钥+私钥)");
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("ljf.jks"), "123456".toCharArray());
        KeyPair keyPair = factory.getKeyPair("ljf", "123456".toCharArray());


        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair);
        return converter;
    }


    /**
     * JWT内容增强
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            // 获取登录用户信息
            OAuthUserDetails OAuthUserDetails = (OAuthUserDetails) authentication.getUserAuthentication().getPrincipal();
            // 将用户信息放到accessToken中返回
            Map additionalInfo = CollectionUtil.newHashMap();
            additionalInfo.put("userId", OAuthUserDetails.getId());
            additionalInfo.put("username", OAuthUserDetails.getUsername());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
            return accessToken;
        };
    }


    /**
     * 从classpath下的密钥库中获取密钥对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        System.out.println("从classpath下的密钥库中获取密钥对(公钥+私钥)");
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("ljf.jks"), "123456".toCharArray());
        KeyPair keyPair = factory.getKeyPair("ljf", "123456".toCharArray());
        return keyPair;
    }


    /**
     * 允许表单认证
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.allowFormAuthenticationForClients();
    }




    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setHideUserNotFoundExceptions(false); // 用户不存在异常抛出
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }


    /**
     * 密码编码器
     * 注意:想要密码编码器生效,需要authenticationProvider()该方法注入
     * 

* 委托方式,根据密码的前缀选择对应的encoder,例如:{bcypt}前缀->标识BCYPT算法加密;{noop}->标识不使用任何加密即明文的方式 * 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks * * @return */ @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }

认证授权配置类主要实现功能:

  1. 指定构建用户认证信息UserDetailsService为UserDetailsServiceImpl,从数据库获取用户信息和前端传值进行密码判读
  2. 指定构建客户端认证信息ClientDetailsService为ClientDetailsServiceImpl,从数据库获取客户端信息和前端传值进行密码判读
  3. JWT加签,从密钥库获取密钥对完成对JWT的签名,密钥库生成
  4. JWT增强

9、 UserDetailService自定义实现加载用户认证信息

/**
* 【重要】从数据库获取用户信息,用于和前端传过来的用户信息进行密码判读
*/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {


    @Autowired
    private UserAdminFeign userAdminFeign;


    @Autowired
    private OAuthClientFeign oAuthClientFeign;


//    @Autowired
//    private MemberFeignClient memberFeignClient;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String clientId = JwtUtils.getAuthClientId();
        Result result;
        OAuthUserDetails oauthUserDetails = null;

        OauthClientDetails oauthClientDetails = oAuthClientFeign.getOAuthClientById(clientId).getData();
        if (!ObjectUtils.isEmpty(oauthClientDetails)) {
            result = userAdminFeign.getUserByUsername(username);
            System.out.println("ljf-auth:-----------------result.toString():"+result.toString());


            if (ResultEnum.SUCCESS.getCode().equals(result.getCode())) {
                SysUser sysUser = result.getData();
                System.out.println("ljf-auth:-----------------sysUser.toString():"+sysUser.toString());

                //TODO: 完善SysUser对象装换为UserDTO对象,实现角色权限的注入
                oauthUserDetails = new OAuthUserDetails(sysUser);
                System.out.println("ljf-auth:-----------------oauthUserDetails.toString():"+oauthUserDetails.toString());

            }
        } else {
            throw new NoSuchClientException("该clientId不存在: " + clientId);
        }


        if (oauthUserDetails == null || oauthUserDetails.getId() == null) {
            throw new UsernameNotFoundException(ResultEnum.USER_NO_EXIST.getMessage());
        } else if (!oauthUserDetails.isEnabled()) {
            throw new DisabledException("该账户已被禁用!");
        } else if (!oauthUserDetails.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定!");
        } else if (!oauthUserDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期!");
        }
        return oauthUserDetails;
    }

}

10、 ClientDetailsService自定义加载客户端认证信息

/**
* 在配置类 AuthorizationServerConfig 中使用
*
* 重写loadClientByClientId方法,从而达到自定义封装client信息的目的
*
* 从数据库获取client信息
*/
@Service
public class ClientDetailsServiceImpl implements ClientDetailsService {

    @Autowired
    private OAuthClientFeign oAuthClientFeign;

    @Override
    @SneakyThrows
    public ClientDetails loadClientByClientId(String clientId) {
        try {
            Result result = oAuthClientFeign.getOAuthClientById(clientId);
          //  System.out.println("ljf-auth:loadClientByClientId::result"+result.toString());
            if (Result.success().getCode().equals(result.getCode())) {
          //      System.out.println("ljf-auth:loadClientByClientId::result"+result.getData());
                OauthClientDetails client = result.getData();
                BaseClientDetails clientDetails = new BaseClientDetails(
                        client.getClientId(),
                        client.getResourceIds(),
                        client.getScope(),
                        client.getAuthorizedGrantTypes(),
                        client.getAuthorities(),
                        client.getWebServerRedirectUri());
                clientDetails.setClientSecret(PasswordEncoderTypeEnum.NOOP.getPrefix() + client.getClientSecret());
                return clientDetails;
            } else {
                throw new NoSuchClientException("该clientId不存在: " + clientId);
            }
        } catch (EmptyResultDataAccessException var4) {
            throw new NoSuchClientException("该clientId不存在: " + clientId);
        }
    }
}

11、测试是否可以正常获取token

12、配置OAuth2资源服务器 即 ljf-gateway

OAuth2资源服务器是提供给客户端资源的服务器,有验证token的能力,token有效则放开资源,对应【有来项目】的youlai-gateway网关。

pom依赖



    org.springframework.security
    spring-security-config


    org.springframework.security
    spring-security-oauth2-resource-server


    org.springframework.security
    spring-security-oauth2-jose

配置ResourceServerConfig资源服务器配置 网关层的安全配置

/**
* @Auther: lijinfeng
* @Date: 2021/12/13
* @Description 描述: 资源服务器配置 网关层的安全配置
*/
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {


    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {


    //jwt认证
        http.oauth2ResourceServer()
                .jwt()
                .publicKey(rsaPublicKey());// 本地获取公钥
    
        http.csrf().disable();
    //处理异常
        http.exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler()) // 处理未授权自定义响应
            .authenticationEntryPoint(authenticationEntryPoint());//处理未认证自定义响应


     //这段配置表示除pathMatchers外其它请求都必须是认证(登陆成功)之后才可以访问。
        http.authorizeExchange()
                .pathMatchers("/ljf-auth/oauth/token/**")
                .permitAll()
                .anyExchange()
                .authenticated();
        return http.build();
    }


    /**
     * 本地获取JWT验签公钥
     *
     * @return
     */
    @SneakyThrows
    @Bean
    public RSAPublicKey rsaPublicKey() {
        Resource resource = new ClassPathResource("public.key");
        InputStream is = resource.getInputStream();
        String publicKeyData = IoUtil.read(is).toString();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec((Base64.decode(publicKeyData)));


        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
        return rsaPublicKey;
    }


    /**
     * @return
     * @link https://blog.csdn.net/qq_24230139/article/details/105091273
     * ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
     * 需要把jwt的Claim中的authorities加入
     * 方案:重新定义权限管理器,默认转换器JwtGrantedAuthoritiesConverter
     */


    /**
     * 未授权自定义响应
     *
     * @return
     */
    @Bean
    ServerAccessDeniedHandler accessDeniedHandler() {
        return (exchange, denied) -> {
            Mono mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
                    .flatMap(response -> ResponseUtils.writeErrorInfo(response, ResultEnum.ACCESS_UNAUTHORIZED));
            return mono;
        };
    }


    /**
     * token无效或者已过期自定义响应
     */
    @Bean
    ServerAuthenticationEntryPoint authenticationEntryPoint() {
        return (exchange, e) -> {
            Mono mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
                    .flatMap(response -> ResponseUtils.writeErrorInfo(response, ResultEnum.TOKEN_INVALID_OR_EXPIRED));
            return mono;
        };
    }
}

添加工具类

/**
* 封装http返回响应对象
*/
public class ResponseUtils {

    public static Mono writeErrorInfo(ServerHttpResponse response, ResultEnum resultCode){
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin", "*");
        response.getHeaders().set("Cache-Control", "no-cache");
        String body = JSONUtil.toJsonStr(Result.error(resultCode));
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
        return response.writeWith(Mono.just(buffer))
                .doOnError(error -> DataBufferUtils.release(buffer));
    }
}



控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言