SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

大家好,我是一安~

导读:在上一篇文章中我们讲解了如何利用Spring Cloud Gateway实现微服务对外统一入口访问,今天继续讲解如何基于Spring Security OAuth2实现微服务统一认证。

大致流程:SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

简介

OAuth是一个开放的授权标准,而Spring Security Oauth2是对OAuth2协议的一种实现框架。

OAuth2的服务提供方包含两个服务,即授权服务(Authorization Server,也叫做认证服务)和资源服务(Resource Server),使用Spring Security OAuth2的时候,可以选择在同一个应用中来实现这两个服务,也可以拆分成多个应用来实现同一组授权服务。

  • 授权服务(Authorization Server):负责对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌的请求断点由Spring MVC控制器进行实现,下面是配置一个认证服务必须的endpoints:
    • AuthorizationEndpoint服务于认证请求。默认URL:/oauth/authorize
    • TokenEndpoint服务于访问令牌的请求。默认URL:/oauth/token
    • OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌进行解析。
  • 资源服务(Resource Server):负责保护和控制访问受保护的资源,它使用访问令牌来验证客户端及其用户是否具有访问特定资源的正确权限。

正文

新增授权信息表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;



-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token`  (
  `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'MD5加密的access_token的值',
  `token` blob NULL COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
  `authentication_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'MD5加密过的username,client_id,scope',
  `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录的用户名',
  `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端ID',
  `authentication` blob NULL COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据',
  `refresh_token` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'MD5加密果的refresh_token的值'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '访问令牌表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_access_token
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals`  (
  `userid` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录的用户名',
  `clientid` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端ID',
  `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '申请的权限',
  `status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '状态(Approve或Deny)',
  `expiresat` datetime NULL DEFAULT NULL COMMENT '过期时间',
  `lastmodifiedat` datetime NULL DEFAULT NULL COMMENT '最终修改时间'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '授权记录表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_approvals
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客户端ID',
  `resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源ID集合,多个资源时用逗号(,)分隔',
  `client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端密匙',
  `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端申请的权限范围',
  `authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端支持的grant_type',
  `web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '重定向URI',
  `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端所拥有的Spring Security的权限值,多个用逗号(,)分隔',
  `access_token_validity` int NULL DEFAULT NULL COMMENT '访问令牌有效时间值(单位:秒)',
  `refresh_token_validity` int NULL DEFAULT NULL COMMENT '更新令牌有效时间值(单位:秒)',
  `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '预留字段',
  `autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户是否自动Approval操作',
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客户端信息' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('account''account''$2a$10$y3MyUsS4YLUCdDo52nMller3ngQa6Eonml.I5.RQ.MD4ii5M1RsJG''web''implicit,client_credentials,authorization_code,refresh_token,password''http://www.baidu.com''ROLE_ADMIN', 7200, 108000, NULL, 'false');
INSERT INTO `oauth_client_details` VALUES ('order''order''$2a$10$M1D5s5a8z9Zv8FkXdnX8S.qMm7/xRY1r7cOw3BPjoV.eC7qBxYUlG''read''client_credentials,authorization_code,mobile,password,refresh_token''http://www.baidu.com''ROLE_USER', 7200, 108000, NULL, 'false');

-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token`  (
  `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'MD5加密的access_token值',
  `token` blob NULL COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
  `authentication_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'MD5加密过的username,client_id,scope',
  `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录的用户名',
  `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端ID'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客户端授权令牌表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_client_token
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code`  (
  `code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '授权码(未加密)',
  `authentication` blob NULL COMMENT 'AuthorizationRequestHolder.java对象序列化后的二进制数据'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '授权码表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_code
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token`  (
  `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'MD5加密过的refresh_token的值',
  `token` blob NULL COMMENT 'OAuth2RefreshToken.java对象序列化后的二进制数据',
  `authentication` blob NULL COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '更新令牌表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_refresh_token
-- ----------------------------


-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '密码',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
  `status` tinyint NULL DEFAULT NULL COMMENT '状态(1:正常 0:停用)',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_username`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'yianweilai''$2a$10$gExKdT3nkoFKfW1cFlqQUuFji3azHG.W4Pe3/WxHKANg3TpkSJRfW''一安未来''致力于Java,大数据;心得交流,技术分享', 1);


SET FOREIGN_KEY_CHECKS = 1;

表结构说明:

  1. oauth_client_details:客户端信息表,存储OAuth2客户端的详细信息,包括客户端ID、客户端密钥、授权类型、重定向URI等。
  2. oauth_access_token:访问令牌表,存储OAuth2访问令牌的详细信息,包括访问令牌值、客户端ID、用户ID、过期时间等。
  3. oauth_refresh_token:刷新令牌表,存储OAuth2刷新令牌的详细信息,包括刷新令牌值、客户端ID、用户ID、过期时间等。
  4. oauth_code:授权码表,存储OAuth2授权码的详细信息,包括授权码值、客户端ID、用户ID、过期时间等。
  5. oauth_approvals:授权表,存储OAuth2授权的详细信息,包括客户端ID、用户ID、授权范围等。
  6. oauth_client_token:客户端令牌表,存储OAuth2客户端的访问令牌信息,包括客户端ID、访问令牌值、过期时间等。
  7. sys_user:用户信息表。

新建授权模块

引入依赖:

<?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>springcloud-parent</artifactId>
        <groupId>org.yian</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>oauth2-servcie</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--SpringBoot2.4.x之后默认不加载bootstrap.yml文件,需要在pom里加上依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-oauth2 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>8.16</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.23</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>javax.validation</groupId>
                    <artifactId>validation-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</project>

生成RSA证书:

# 使用keytool生成RSA证书jwt.jks,复制到resource目录下
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

核心代码:

@EnableAuthorizationServer 注解是Spring Security打开OAuth认证服务的基础注解,可以在启动类或者任意一个@Configuration声明的类中打开这个注释,往常配置Spring Security时,我们利用WebSecurityConfigurerAdapter注入一个配置对象来完成对基础认证授权功能的配置,在使用OAuth2时,Spring Security也提供了一个类似的适配器AuthorizationServerConfigurerAdapter来帮助我们完成配置。

授权认证服务中最核心的配置:

  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
  • AuthorizationServerEndpointsConfifigurer:用来配置令牌(token)的访问端点和令牌服务(tokenservices)。
  • AuthorizationServerSecurityConfifigurer:用来配置令牌端点的安全约束。
/**
 * 认证服务器配置
 * @author yian
 * @since 2023-03-30
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;
    @Autowired

    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    // 此对象是将security认证对象注入到oauth2框架中
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * access_token存储器
     * 大家可以结合自己的业务场景考虑将access_token存入数据库还是redis
     * RedisTokenStore JdbcTokenStore
     */
    @Bean
    public TokenStore tokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    //客户端(第三方应用)信息来源
    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService(){
        return new JdbcClientDetailsService(dataSource);
    }


    //授权信息保存策略
    @Bean
    public ApprovalStore approvalStore(){
        return new JdbcApprovalStore(dataSource);
    }

    //授权码模式数据来源
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(){
        return new JdbcAuthorizationCodeServices(dataSource);
    }


    /**
     * 用来配置客户端详情服务(ClientDetailsService)
     *
     * 此方法主要是用来配置Oauth2中第三方应用的,什么是第三方应用呢,就是请求用微信、微博账号登录的程序
     * 取相关配置,有InMemoryClientDetailsService 和 JdbcClientDetailsService (oauth_client_details)两种方式选择
     *
     * ClientDetailsServiceConfigurer能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),
     * ClientDetailsService负责查找ClientDetails,一个ClientDetails代表一个需要接入的第三方应用,例如
     * 我们上面提到的OAuth流程中的百度。ClientDetails中有几个重要的属性如下:
     * - clientId: 用来标识客户的ID。必须。
     * - secret: 客户端安全码,如果有的话。在微信登录中就是必须的。
     * - scope: 用来限制客户端的访问范围,如果是空(默认)的话,那么客户端拥有全部的访问范围。
     * - authrizedGrantTypes:此客户端可以使用的授权类型,默认为空。在微信登录中,只支持authorization_code这一种。
     * - authorities:此客户端可以使用的权限(基于Spring Security authorities)
     * - redirectUris:回调地址。授权服务会往该回调地址推送此客户端相关的信息。
     *
     * Client Details客户端详情,能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如
     * 访问mysql,就提供了JdbcClientDetailsService)或者通过自己实现ClientRegisterationService接口(同
     * 时也可以实现ClientDetailsService接口)来进行定制
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

//    @Override
//    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        clients.inMemory()
//                .withClient("test_client")//配置client_id
//                .secret(passwordEncoder.encode("test_client"))//配置client_secret
//                .accessTokenValiditySeconds(3600)//配置访问token的有效期
//                .refreshTokenValiditySeconds(864000)//配置刷新token的有效期
//                .redirectUris("http://www.baidu.com")//配置redirect_uri,用于授权成功后跳转
//                .scopes("all")//配置申请的权限范围
//                .resourceIds("test_client")
//                .authorizedGrantTypes("authorization_code""password""client_credentials""implicit""refresh_token");//配置grant_type,表示授权类型
//    }


    /**
     * 用来配置令牌端点的安全约束
     *
     * 认证服务器相关接口权限管理
     * 对oauth/check_token,oauth/token_key访问控制,可以设置isAuthenticated()、permitAll()等权限
     * 这块的权限控制是针对应用的,而非用户,比如当设置了isAuthenticated(),必须在请求头中添加应用的id和密钥才能访问
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients() //如果使用表单认证则需要加上
                .passwordEncoder(passwordEncoder)
                .tokenKeyAccess("permitAll()") // oauth/token_key公开
                .checkTokenAccess("isAuthenticated()"); // oauth/check_token公开
    }


    /**
     此配置方法有以下几个用处:
     不同的授权类型(Grant Types)需要设置不同的类:
        authenticationManager:当授权类型为密码模式(password)时,需要设置此类
        AuthorizationCodeServices: 授权码模式(authorization_code) 下需要设置此类,用于实现授权码逻辑
        implicitGrantService:隐式授权模式设置此类。
        tokenGranter:自定义授权模式逻辑

     通过pathMapping<默认链接,自定义链接> 方法修改默认的端点URL
        /oauth/authorize:授权端点。
        /oauth/token:令牌端点。
        /oauth/confirm_access:用户确认授权提交端点。
        /oauth/error:授权服务错误信息端点。
        /oauth/check_token:用于资源服务访问的令牌解析端点。
        /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。


     通过tokenStore来定义Token的存储方式和生成方式:
        InMemoryTokenStore
        JdbcTokenStore
        JwtTokenStore
        RedisTokenStore
     */


    /**
     * 用来配置令牌(token)的访问端点和令牌服务
     *
     *  OAuth2的主配置信息,这个方法相当于把前面的所有配置到装配到endpoints中让其生效
     *
     * 配置授权模式也可以添加自定义模式(不写也有默认的)
     * 具体可查看AuthorizationServerEndpointsConfigurer中的getDefaultTokenGranters方法,以后添加一个手机验证码的功能
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //增加转换链路,以增加自定义属性(配置JWT的内容增强器)
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(accessTokenConverter());
        enhancerChain.setTokenEnhancers(delegates);

        endpoints.approvalStore(approvalStore())
                //授权码服务
                .authorizationCodeServices(authorizationCodeServices())
                //认证管理器
                .authenticationManager(authenticationManager)
                //如果需要使用refresh_token模式则需要注入userDetailService
                //配置tokenStore管理、配置加载用户信息的服务
                .tokenStore(tokenStore()).userDetailsService(userDetailsService)
                .accessTokenConverter(accessTokenConverter())
                .tokenEnhancer(enhancerChain)
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)//支持GET,POST请求
                .exceptionTranslator(new WebResponseTranslator());
    }


    //token生成方式
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyPair());
        return jwtAccessTokenConverter;
    }

    @Bean
    public KeyPair keyPair() {
        //从classpath下的证书中获取秘钥对
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt""123456".toCharArray());
    }
}
/**
 * JWT内容增强器
 * @author yian
 * @since 2023-03-30
 */
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Object principal = authentication.getPrincipal();
        Map<String, Object> info = new HashMap<>();
        if(principal instanceof  CustomUser){
            SysUser sysUser = ((CustomUser) principal).getSysUser();
            //把用户name设置到JWT中
            info.put("desc", sysUser.getName());
        }else if(principal instanceof SysUser){
            info.put("desc", ((SysUser) principal).getName());
        }else if(principal instanceof String){
            info.put("desc", principal);
        }

        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private SuccessHandler successHandler;

    @Autowired
    private FailureHandler failureHandler;

    @Autowired
    private LogoutHandler logoutHandler;


    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());

    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().formLogin()
                .loginProcessingUrl("/login").permitAll()
//                .successHandler(successHandler).permitAll()
                .failureHandler(failureHandler).permitAll().and()
                .logout().logoutSuccessHandler(logoutHandler).and()
                .authorizeRequests()
                .anyRequest().authenticated();
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/*.html""/favicon.ico","/swagger-resources/**",
                "/webjars/**""/v2/**""/swagger-ui.html/**""/doc.html");
    }

}

流程测试

客户端模式

这种模式是最简单的模式,流程如下:SpringCloud Alibaba微服务实战之整合Spring Security OAuth2这种模式是最方便但是也最不安全的模式,代表了授权服务器对客户端的完全互信。因此,这种模式一般可以用在授权服务器对客户端完全信任的场景,例如内部系统或者协议合作方系统对接。SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

密码模式

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

这种模式用户会把用户名和密码直接泄漏给客户端,代表了资源拥有者和授权服务器对客户端的绝对互信,相信客户端不会做坏事。一般适用于内部开发的客户端的场景。SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

简化模式

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2这种方案下,一般redirect uri会配置成客户端自己的一个相应地址。这个相应地址接收到授权服务器推送过来的访问令牌后,就可以将访问令牌在本地进行保存,然后在需要调用资源服务时,再拿出来通过资源服务的认证。

这种模式下,oauth三方的数据已经进行了隔离。这种模式一般用于没有服务端的第三方单页面应用,这样可以在JS里直接相应access_token

http://localhost:9000/auth/oauth/authorize?client_id=account&response_type=token&scope=web&redirect_uri=http://www.baidu.comSpringCloud Alibaba微服务实战之整合Spring Security OAuth2

授权码模式

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2这种模式是四种模式中最安全的一种。这种模式下,oauth2认证的三方可以在互不信任的情况下完成担保认证过程。而且,这种模式下,access_token是直接在后台服务端之间进行交互,这样也较小了令牌泄漏的风险。

http://localhost:9000/auth/oauth/authorize?client_id=account&response_type=code&scope=web&redirect_uri=http://www.baidu.comSpringCloud Alibaba微服务实战之整合Spring Security OAuth2


刷新令牌:SpringCloud Alibaba微服务实战之整合Spring Security OAuth2检查令牌:SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

优化网关服务

引入依赖:

  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
      <version>2.2.5.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
  <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.2</version>
  </dependency>
  <dependency>
      <groupId>com.alibaba.fastjson2</groupId>
      <artifactId>fastjson2</artifactId>
      <version>2.0.23</version>
  </dependency>

核心代码,两种方式,选择其一即可:

1.利用GlobalFilter

Spring-Cloud-Gatewayfilter包中吉接口有如下三个,GatewayFilter,GlobalFilter,GatewayFilterChain,GlobalGilter 全局过滤器接口与 GatewayFilter 网关过滤器接口具有相同的方法定义。全局过滤器是一系列特殊的过滤器,会根据条件应用到所有路由中。网关过滤器是更细粒度的过滤器,作用于指定的路由中。

我们可以配置多个GlobalFilter过滤器,通过指定getOrder()方法的优先级来配置过滤器的执行顺序。SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

@Component
@Slf4j
public class GatewayFilterConfig implements GlobalFilter, Ordered {


    @Autowired
    private TokenStore tokenStore;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestUrl = exchange.getRequest().getPath().value();
        AntPathMatcher pathMatcher = new AntPathMatcher();
        //1 认证服务所有放行
        if (pathMatcher.match("/auth/**", requestUrl)) {
            return chain.filter(exchange);
        }
        //2 检查token是否存在
        String token = getToken(exchange);
        if (StringUtils.isBlank(token)) {
            return noTokenMono(exchange);
        }
        //3 判断是否是有效的token
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = tokenStore.readAccessToken(token);
            Map<String, Object> additionalInformation = oAuth2AccessToken.getAdditionalInformation();
            //取出用户身份信息
            String principal = MapUtils.getString(additionalInformation, "user_name");
            //获取用户权限
            List<String> authorities = (List<String>) additionalInformation.get("authorities");
            JSONObject jsonObject=new JSONObject();
            jsonObject.put("principal",principal);
            jsonObject.put("authorities",authorities);
            //给header里面添加值
            String base64 = EncryptUtil.encodeUTF8StringBase64(jsonObject.toJSONString());
            ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("json-token", base64).build();
            ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
            return chain.filter(build);
        } catch (InvalidTokenException e) {
            log.info("无效的token: {}", token);
            return invalidTokenMono(exchange);
        }
    }


    /**
     * 获取token
     */
    private String getToken(ServerWebExchange exchange) {
        String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(tokenStr)) {
            return null;
        }
        String token = tokenStr.split(" ")[1];
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return token;
    }


    /**
     * 无效的token
     */
    private Mono<Void> invalidTokenMono(ServerWebExchange exchange) {
        JSONObject json = new JSONObject();
        json.put("status", HttpStatus.UNAUTHORIZED.value());
        json.put("data""无效的token");
        return buildReturnMono(json, exchange);
    }

    private Mono<Void> noTokenMono(ServerWebExchange exchange) {
        JSONObject json = new JSONObject();
        json.put("status", HttpStatus.UNAUTHORIZED.value());
        json.put("data""没有token");
        return buildReturnMono(json, exchange);
    }


    private Mono<Void> buildReturnMono(JSONObject json, ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        byte[] bits = json.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type""text/plain;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }


    @Override
    public int getOrder() {
        return 0;
    }
}

2.利用ReactiveAuthenticationManager

@Slf4j
public class ReactiveJwtAuthenticationManager implements ReactiveAuthenticationManager {

    private TokenStore tokenStore;
    public ReactiveJwtAuthenticationManager(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        return Mono.justOrEmpty(authentication)
                .filter(a -> a instanceof BearerTokenAuthenticationToken)
                .cast(BearerTokenAuthenticationToken.class)
                .map(BearerTokenAuthenticationToken::getToken)
                .flatMap((accessToken -> {
                    log.info("accessToken is :{}", accessToken);
                    OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
                    //根据access_token从数据库获取不到OAuth2AccessToken
                    if (oAuth2AccessToken == null) {
                        return Mono.error(new InvalidTokenException("invalid access token,please check"));
                    }
                    else if(oAuth2AccessToken.isExpired()) {
                        return Mono.error(new InvalidTokenException("access token has expired,please reacquire token"));
                    }

                    OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
                    if (oAuth2Authentication == null) {
                        return Mono.error(new InvalidTokenException("Access Token 无效!"));
                    } else {
                        return Mono.just(oAuth2Authentication);
                    }
                })).cast(Authentication.class);
    }

    /**
     * 获取token
     */
    private String getToken(ServerWebExchange exchange) {
        String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(tokenStr)) {
            return null;
        }
        String token = tokenStr.split(" ")[1];
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return token;
    }
}

安全配置:

/**
 * 由于 SpringCloud Gateway 基于WebFlux 并且不兼容SpringMVC,因此对于Security的配置方式也跟普通SpringBoot项目中的配置方式不同。
 *
 * 在Gateway项目中使用的WebFlux,是不能和Spring-Web混合使用的。
 *
 *
 * 在Gateway项目中使用的WebFlux,是不能和Spring-Web混合使用的
 */
@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法级的权限认证
public class SecurityConfig {
    private static final String MAX_AGE = "18000L";

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private AccessManager accessManager;

    /**
     * 方式1:GlobalFilter 配置方式要换成 WebFlux的方式
     */
//    @Bean
//    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
//        return httpSecurity.authorizeExchange()
//                .pathMatchers("/**").permitAll()
//                .and().csrf().disable().build();
//    }



    /**
     * 方式2:ReactiveAuthenticationManager 配置方式要换成 WebFlux的方式
     */
    @Bean
    public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
        //token管理器
        ReactiveAuthenticationManager tokenAuthenticationManager = new ReactiveJwtAuthenticationManager(tokenStore);
        //认证过滤器
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());

        http.httpBasic().disable()
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers(HttpMethod.OPTIONS).permitAll()
            .anyExchange().access(accessManager)
            .and()
            //oauth2认证过滤器
            .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }
}

权限管理:

/**
 * 方式2:ReactiveAuthenticationManager
 */
@Slf4j
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private Set<String> permitAll = new ConcurrentHashSet<>();
    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();


    public AccessManager (){
        permitAll.add("/");
        permitAll.add("/error");
        permitAll.add("/favicon.ico");
        permitAll.add("/**/v2/api-docs/**");
        permitAll.add("/**/swagger-resources/**");
        permitAll.add("/webjars/**");
        permitAll.add("/doc.html");
        permitAll.add("/swagger-ui.html");
        permitAll.add("/**/oauth/**");
    }

    /**
     * 实现权限验证判断
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        //请求资源
        String requestPath = exchange.getRequest().getURI().getPath();
        // 是否直接放行
        if (permitAll(requestPath)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        return authenticationMono.map(auth -> {
            return new AuthorizationDecision(checkAuthorities(exchange, auth, requestPath));
        }).defaultIfEmpty(new AuthorizationDecision(false));

    }

    /**
     * 校验是否属于静态资源
     * @param requestPath 请求路径
     * @return
     */
    private boolean permitAll(String requestPath) {
        return permitAll.stream()
                .filter(r -> antPathMatcher.match(r, requestPath)).findFirst().isPresent();
    }

    //权限校验
    private boolean checkAuthorities(ServerWebExchange exchange, Authentication auth, String requestPath) {
        if(auth instanceof OAuth2Authentication){
            OAuth2Authentication athentication = (OAuth2Authentication) auth;
            String clientId = athentication.getOAuth2Request().getClientId();
            log.info("clientId is {}",clientId);
        }

        Object principal = auth.getPrincipal();
        log.info("用户信息:{}",principal.toString());
        return true;
    }
}

跨域处理:

@Configuration
public class GatewayCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOriginPattern("*");
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}

ReactiveAuthenticationManagerReactiveAuthorizationManagerSpring Security中的两个重要接口,用于认证和授权:

  • ReactiveAuthenticationManager是用于认证用户身份的接口,它接收一个Authentication对象作为参数,并返回一个Mono<Authentication>对象,表示认证成功后的身份信息。它的主要作用是验证用户的身份信息是否正确,并将用户的身份信息封装成一个Authentication对象返回给调用方。
  • ReactiveAuthorizationManager是用于授权的接口,它接收一个AuthorizationContext对象作为参数,并返回一个Mono<AuthorizationDecision>对象,表示授权决策结果。它的主要作用是根据用户的身份信息和请求的资源信息,判断用户是否有访问该资源的权限,并返回授权决策结果。

因此,ReactiveAuthenticationManagerReactiveAuthorizationManager的主要区别在于它们的作用不同,前者用于认证用户身份,后者用于授权用户访问资源。

最后记得在网关加上认证的路由。

优化用户模块(其他模块类似)

引入依赖:

  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
      <version>2.2.5.RELEASE</version>
  </dependency>

核心代码:

/**
 * 方式1:GlobalFilter
 */
@Component
public class AuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
         String token = request.getHeader("json-token");
        if (StringUtils.isNotEmpty(token)){
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            JSONObject jsonObject = JSON.parseObject(json);
            //获取用户身份信息、权限信息
            String principal = jsonObject.getString("principal");
            String authorities = jsonObject.getString("authorities");
            if(!StringUtils.isEmpty(principal)){
                //身份信息、权限信息填充到用户身份token对象中
                UsernamePasswordAuthenticationToken authenticationToken = null;
                if(!StringUtils.isEmpty(authorities)) {
                    List<Map> maplist = JSON.parseArray(authorities, Map.class);
                    List<SimpleGrantedAuthority> authList = new ArrayList<>();
                    for (Map map:maplist) {
                        String authority = (String)map.get("authority");
                        authList.add(new SimpleGrantedAuthority(authority));
                    }
                    authenticationToken = new UsernamePasswordAuthenticationToken(principal,null, authList);
                }else{
                    authenticationToken = new UsernamePasswordAuthenticationToken(principal,null, new ArrayList<>());
                }
                //创建details
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                //将authenticationToken填充到安全上下文
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                filterChain.doFilter(request,response);
            }else{
                JSONObject res =new JSONObject();
                res.put("code",403);
                res.put("msg","没有权限");
                response.setHeader("Content-type""text/html;charset=UTF-8");
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write(JSONObject.toJSONString(res));
            }
        }else{
            JSONObject res =new JSONObject();
            res.put("code",403);
            res.put("msg","没有权限");
            response.setHeader("Content-type""text/html;charset=UTF-8");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write(JSONObject.toJSONString(res));
        }
    }
}
/**
 * 配置资源服务器
 */
@Configuration
@EnableResourceServer //相当于加上OAuth2AuthenticationProcessingFilter过滤器
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Autowired
    private TokenStore tokenStore;


    /**
     * 资源ID
     */
    private static final String RESOURCE_ID = "test";


    /**
     *  资源配置
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                .stateless(true)
                .accessDeniedHandler(new CustomAccessDeniedHandler());
    }

   /**
     * 方式1:GlobalFilter
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers("/**").permitAll()
                .and()
                //统一自定义异常
                .exceptionHandling()
                .and()
                .csrf().disable();
    }
    /**
     * 方式2:ReactiveAuthenticationManager
     */

//    @Override
//    public void configure(HttpSecurity http) throws Exception {
//        http.authorizeRequests()
//            .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
//            .antMatchers(
//                    "/v2/api-docs/**",
//                    "/swagger-resources/**",
//                    "/swagger-ui.html",
//                    "/webjars/**"
//            ).permitAll()
//            .anyRequest().authenticated()
//            .and()
//            //统一自定义异常
//            .exceptionHandling()
//            .and()
//            .csrf().disable();
//    }
}

网关演示

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2报错了,不要慌,测试使用的RESOURCE_ID (test),修改代码里RESOURCE_IDaccount,与oauth_client_details中保持一致。SpringCloud Alibaba微服务实战之整合Spring Security OAuth2重新测试:SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

至此我们将SpringCloud Gateway整合好了Oauth2.0,实现微服务统一认证。


如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

Quartz基于配置实现动态定时任务执行


聊聊保证线程安全的几个小技巧


一文教你如何实现接口的幂等性

SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

原文始发于微信公众号(一安未来):SpringCloud Alibaba微服务实战之整合Spring Security OAuth2

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145202.html

(1)
青莲明月的头像青莲明月

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!