🚀SpringSecurity+OAuth2+JWT认证服务

4,814 阅读14分钟

OAuth2

什么是OAuth2?

OAuth 2.0(Open Authorization 2.0)是一种开放标准的授权协议,用于在不透露用户凭据(例如用户名和密码)的情况下,让用户授权第三方应用程序访问其受保护的资源。

简单来说,OAuth 2.0 提供了一种安全的机制,允许用户使用其已有的认证凭据(例如社交媒体账户)来登录和授权其他应用程序访问其个人信息。

OAuth 2.0 的核心角色包括:

  1. 用户(resource owner):拥有受保护资源的实体,是授权的主体。
  2. 客户端(client):代表用户请求访问受保护资源的应用程序,例如一个第三方应用或者服务。
  3. 授权服务器(authorization server):负责验证用户身份并授予客户端访问令牌的服务器。
  4. 资源服务器(resource server):存储用户的受保护资源,并负责根据访问令牌提供授权的访问。

OAuth2认证过程

首先先来快速认识一下Oauth2的认证流程图

流程图内容解释

Clinet(客户端)、ResourceOwner(资源拥有者)
Authorization Server(授权服务器),ResourceServer(资源服务器)
AuthorizationRequest(授权请求)、AuthorizationGrant(授权许可)
AccessToken(访问令牌)
Portected Resource(受保护的资源)

Client(客户端)本身不用来存储资源,需要通过请求资源服务器的资源来获取资源。

而要获取到资源服务器的资源,需要经过ABCDEF这些过程。总结来说有4个流程

第一步:向资源拥有者发起授权请求,通俗来讲,就类似你微信授权登录某小程序,小程序就是客户端,而你微信就资源拥有者,用户的意思。

第二步:资源拥有者返回客户端一个授权许可,此时等同你已经同意授权了。

第三步:客户端拿到授权许可后会向授权服务器申请访问令牌,然后授权服务器会认证你这个用户或者客户端并颁发令牌

其中:认证用户是通过验证用户提供的凭据,包括匹配用户名密码或者授权码等。 认证客户端是通过提供的客户端标识和客户端密钥等。

注意:因为OAuth2的授权模式有授权码、密码、简化、客户端模式等,选择哪个模式对应认证哪个方面。

第四步:客户端拿到访问令牌就可以向资源服务器请求受保护的资源了。这些受保护的资源类似你登录成功后展示给你看的商品图片,数据等都是。

JWT

OAuth2令牌可以分为透明令牌(Transparent Token)和不透明令牌(Opaque Token)。

不透明令牌:指令牌本身并不包含令牌的具体信息,仅包含一个令牌标识(Token Identifier)

透明令牌:指令牌本身包含了令牌的所有信息,例如访问令牌的有效期、权限范围等

JWT分为三部分,分别是头部载荷签名。

  1. 头部(Header): JWT的头部通常由两部分组成,令牌的类型(即"typ"字段,一般都是JWT)和所使用的签名算法(即"alg"字段)。例如: { "alg": "HS256", "typ": "JWT" }。
  2. 载荷(Payload):载荷是JWT的第二部分,它包含了一些称为声明(Claims)的数据。有三种类型的声明:注册声明(Registered claims)、公共声明(Public claims)和私有声明(Private claims)。注册声明是预定义的声明类型,如iss(签发者)、exp(过期时间)、sub(主题)等。公共声明是自定义的声明,但不建议重复使用。私有声明定义了用户自定义的声明,用于在通信的各方之间共享信息。
  3. 签名(Signature):签名是JWT的第三部分,它由前两部分(头部和载荷)及一个密钥进行签名生成。签名的目的是为了验证消息的完整性和真实性。签名通常使用 HMAC(基于哈希的消息认证码)或 RSA(非对称加密算法)等算法进行生成。签名通过对头部、载荷和密钥进行加密生成一个哈希值,并将其作为签名部分的内容。

JWT令牌的优点

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

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

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

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

JWT令牌的缺点

1、JWT令牌较长,占存储空间比较大。

下面通过一个构建认证服务让了解更加深刻。

配置类

TokenEnhanceConfig JWT声明内容加强配置

WebSecurityConfig 安全配置

AuthorizationServerConfig OAuth2授权服务器配置

注意事项

不是所有配置类都可以作为OAuth2.0认证中心的配置类, 需要满足以下两点:

1、继承AuthorizationServerConfigurerAdapter

2、 标注 @EnableAuthorizationServer 注解

AuthorizationServerConfigurerAdapter需要实现的三个方法如下

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
	//配置令牌端点安全约束
   @Override
   public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {
      
   }
    	//用来配置客户端详情服务
   @Override
   public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {

   }

   @Override
   public void configure(AuthorizationServerEndpointsConfigurer authorizationServerEndpointsConfigurer) throws Exception {
    	//配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
   }

}

TokenEnhanceConfig

它是用来配置JWT声明信息达到增强令牌的一个配置类。

@Configuration
@RequiredArgsConstructor
public class TokenEnhanceConfig {

    private final RedisTemplate redisTemplate;

    @Bean
    // 根据authentication对象中的principal属性的类型,将不同的用户信息存放到additionalInfo对象中,
    // 并将additionalInfo设置为accessToken的附加信息
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            // 实现TokenEnhancer接口的匿名内部类,增强访问令牌的信息
            Object principal = authentication.getUserAuthentication().getPrincipal();

            Map<String, Object> additionalInfo = MapUtil.newHashMap();
            if (principal instanceof SysUserDetails) {
                SysUserDetails sysUserDetails = (SysUserDetails) principal;
                additionalInfo.put("userId", sysUserDetails.getUserId());
                additionalInfo.put("username", sysUserDetails.getUsername());
                additionalInfo.put("deptId", sysUserDetails.getDeptId());
                additionalInfo.put("dataScope",sysUserDetails.getDataScope());

                /**
                 * 系统用户按钮权限标识数据量多存放至redis
                 *
                 * key:AUTH:USER_PERMS:2
                 * value:['sys:user:add',...]
                 */
              	//这样可以方便地将用户的权限信息存储在分布式缓存Redis中,以供后续的权限验证等操作使用。
                redisTemplate.opsForValue().set("AUTH:USER_PERMS:" + sysUserDetails.getUserId(), sysUserDetails.getPerms());

            } else if (principal instanceof MemberUserDetails) {
                MemberUserDetails memberUserDetails = (MemberUserDetails) principal;
                additionalInfo.put("memberId", memberUserDetails.getMemberId());
                additionalInfo.put("username", memberUserDetails.getUsername());
            }
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
            return accessToken;
        };
    }
}

增强访问令牌的信息

TokenEnhanceConfig里实现了tokenEnhancer的匿名内部类,目的是增强访问令牌的信息。将授权主体(指用户)的相关信息存放到addtionalInfo中,然后设置到accessToken(访问令牌)的附加信息中。

可能你会问,配置了声明信息如何达到增强的效果呢?

:通过配置了上述代码的声明,令牌声明里多了用户的权限信息,

服务端就可以利用这些信息进行身份验证、授权等操作。这样可以减少数据库的压力,简化跨域通信的流程,简化系统的实现等。

WebSecurityConfig

安全配置

@ConfigurationProperties(prefix = "security")
@Configuration
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService sysUserDetailsService;
    private final UserDetailsService memberUserDetailsService;
    private final WxMaService wxMaService;
    private final MemberFeignClient memberFeignClient;
    private final StringRedisTemplate redisTemplate;

    @Setter
    private List<String> ignoreUrls;

    @Override
    //配置安全过滤器
    protected void configure(HttpSecurity http) throws Exception {

        if (CollectionUtil.isEmpty(ignoreUrls)) {
            //将默认的白名单路径添加到ignoreUrls列表
            ignoreUrls = Arrays.asList("/webjars/**", "/doc.html", "/swagger-resources/**", "/v2/api-docs");
        }

        log.info("whitelist path:{}", JSONUtil.toJsonStr(ignoreUrls));

        http
                .authorizeRequests()
                //允许ignoreUrls中的所有URL进行无需身份验证的访问
                .antMatchers(Convert.toStrArray(ignoreUrls)).permitAll()
                //其他所有请求则需要进行身份验证
                .anyRequest().authenticated()
                .and()
                //禁用了跨站请求伪造(CSRF)保护
                .csrf().disable();
    }

    /**
     * 认证管理对象
     *
     * @return
     * @throws Exception
     */
    @Bean
    // 这个方法暴露了AuthenticationManager bean,用于处理身份验证和授权过程。
    //AuthenticationManager在密码授权模式下会用到,这里提前注入,如果你用的不是密码模式,可以不注入.
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    // 这个方法通过指定认证提供者来配置AuthenticationManagerBuilder。
    // 它注册了微信、用户名密码和短信验证码认证提供者。
    public void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(wechatAuthenticationProvider()).
                authenticationProvider(daoAuthenticationProvider()).
                authenticationProvider(smsCodeAuthenticationProvider());
    }

    /**
     * 手机验证码认证授权提供者
     *
     * @return
     */
    @Bean
    public SmsCodeAuthenticationProvider smsCodeAuthenticationProvider() {
        SmsCodeAuthenticationProvider provider = new SmsCodeAuthenticationProvider();
        provider.setUserDetailsService(memberUserDetailsService);
        provider.setRedisTemplate(redisTemplate);
        return provider;
    }

    /**
     * 微信认证授权提供者
     *
     * @return
     */
    @Bean
    public WechatAuthenticationProvider wechatAuthenticationProvider() {
        WechatAuthenticationProvider provider = new WechatAuthenticationProvider();
        provider.setUserDetailsService(memberUserDetailsService);
        provider.setWxMaService(wxMaService);
        provider.setMemberFeignClient(memberFeignClient);
        return provider;
    }


    /**
     * 用户名密码认证授权提供者
     *
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(sysUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
        return provider;
    }


    /**
     * 密码编码器
     * <p>
     * 委托方式,根据密码的前缀选择对应的encoder,例如:{bcypt}前缀->标识BCYPT算法加密;{noop}->标识不使用任何加密即明文的方式
     * 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks
     */
    @Bean
    // 这个方法配置了用户名密码认证所使用的密码编码器。
    // 它使用Spring Security的DelegatingPasswordEncoder创建一个密码编码器,根据密码的前缀委派给不同的编码器
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


}

这个类整合的内容有

1、加密方式

PasswordEncoderFactories.createDelegatingPasswordEncoder()返回的是一个委托密码编码器,

默认使用的是 BCryptPasswordEncoder 加密方式来加密和验证密码。

2、配置用户,

它注册了微信、用户名密码和短信验证码认证提供者。

3、注入认证管理器

因为上述代码使用的是密码授权模式,在这里提前注入,如果用的不是密码模式,可以不注入。

4、配置安全拦截策略

允许ignoreUrls中的所有URL进行无需身份验证的访问,禁用了跨站请求伪造(CSRF)保护等。


AuthorizationServerConfig

它是 OAuth2授权服务器配置

@Configuration
@EnableAuthorizationServer//启用授权服务器功能,将当前应用程序作为一个OAuth2授权服务器
@RequiredArgsConstructor
//这个类用于配置和初始化授权服务器的各种参数和组件
//AuthorizationServerConfig用 @EnableAuthorizationServer 注解标识并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器。
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;//用于进行身份验证
    private final SysUserDetailsServiceImpl sysUserDetailsService;
    private final MemberUserDetailsServiceImpl memberUserDetailsService;
    private final StringRedisTemplate stringRedisTemplate;
    private final DataSource dataSource;

    private final TokenEnhancer tokenEnhancer;//注入TokenEnhancer对象,用于增强令牌的内容。

    /**
     * OAuth2客户端
     */
    @Override
    //用来配置客户端详情服务,基于 JDBC 的客户端详情服务配置为当前的 Spring Security OAuth2 客户端配置
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    //用来配置令牌(token)的访问端点和令牌服务(token services)
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        //tokenEnhancer里有附加信息为用户的信息
        tokenEnhancers.add(tokenEnhancer);
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        //token存储模式设定 默认为InMemoryTokenStore模式存储到内存中
        endpoints.tokenStore(jwtTokenStore());

        // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
        //,endpoints.getTokenGranter() 方法返回一个 TokenGranter 数组,通过使用 Arrays.asList(...) 方法将其转换为一个 List
        List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));

        // 添加验证码授权模式授权者
        granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager, stringRedisTemplate
        ));

        // 添加手机短信验证码授权模式的授权者
        granterList.add(new SmsCodeTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        // 添加微信授权模式的授权者
        granterList.add(new WechatTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));
        // 用于组合多个 TokenGranter 实例,以实现多种授权模式的支持。
        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .tokenGranter(compositeTokenGranter)
                .tokenServices(tokenServices(endpoints))
        ;
    }

    /**
     * jwt token存储模式
     */
    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        // 用于组合多个令牌增强器
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        // 用于存储令牌增强器。
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer);
        tokenEnhancers.add(jwtAccessTokenConverter());
        //将 tokenEnhancers 设置为 tokenEnhancerChain 的令牌增强器列表。
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
        //用于管理令牌相关的业务逻辑
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        // 设置 endpoints 中的令牌存储方式为 tokenServices 的令牌存储方式。即上面的JwtTokenStore这个方法
        tokenServices.setTokenStore(endpoints.getTokenStore());
        // 设置 tokenServices 支持刷新令牌。
        tokenServices.setSupportRefreshToken(true);
        // 设置 tokenServices 使用的客户端详情服务。
        tokenServices.setClientDetailsService(jdbcClientDetailsService());
        //设置 tokenServices 使用的令牌增强器。
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        // 多用户体系下,刷新token再次认证客户端ID和 UserDetailService 的映射Map
        //用于存储客户端ID和对应的用户详情服务
        Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
        clientUserDetailsServiceMap.put(SecurityConstants.ADMIN_CLIENT_ID, sysUserDetailsService); // 系统管理客户端
        clientUserDetailsServiceMap.put(SecurityConstants.APP_CLIENT_ID, memberUserDetailsService); // Android、IOS、H5 移动客户端
        clientUserDetailsServiceMap.put(SecurityConstants.WEAPP_CLIENT_ID, memberUserDetailsService); // 微信小程序客户端

        // 刷新token模式下,重写预认证提供者替换其AuthenticationManager,可自定义根据客户端ID和认证方式区分用户体系获取认证用户信息
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();//用于进行预认证。
        //设置 provider 的预认证用户详情服务为 PreAuthenticatedUserDetailsService,并传入 clientUserDetailsServiceMap
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
        // 设置 tokenServices 使用的身份验证管理器为 ProviderManager,并传入包含 provider 的集合。
        tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));

        /**
         * refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
         *  1 重复使用:access_token过期刷新时, refresh_token过期时间未改变,仍以初次生成的时间为准
         *  2 非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新便永不失效达到无需再次登录的目的
         */
        //设置tokenServices 的刷新令牌是否可重复使用。
        tokenServices.setReuseRefreshToken(true);
        return tokenServices;

    }

    /**
     * 使用非对称加密算法对token签名
     */
    //认证服务和资源服务使用相同的密钥,这叫对称加密,对称加密效率高,如果一旦密钥泄露可以伪造jwt令牌。
    // JWT还可以使用非对称加密,认证服务自己保留私钥,将公钥下发给受信任的客户端、资源服务,公钥和私钥是配对的,
    // 成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密非对称加密更安全一些。
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * 密钥库中获取密钥对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        //通过传递一个ClassPathResource对象和密码字符串,该工厂负责加载密钥库文件并准备密钥对。
        // 这里的keystore.jks是密钥库的文件名,它位于类路径下。
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "123456".toCharArray());
        // 通过getKeyPair方法获取密钥对:
        // 调用KeyStoreKeyFactory对象的getKeyPair方法,使用密钥库密码和密钥的别名(这里是jwt)来获取密钥对。
        // 密钥别名用于标识密钥对,而密钥库密码用于保护密钥库的访问。
        KeyPair keyPair = factory.getKeyPair("jwt", "123456".toCharArray());
        return keyPair;
    }

    /**
     * 自定义认证异常响应数据
     */
    @Bean
    //用于在用户尚未进行身份认证或者认证失败时处理请求。
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            response.setStatus(HttpStatus.HTTP_OK);
            response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Cache-Control", "no-cache");
            Result result = Result.failed(ResultCode.CLIENT_AUTHENTICATION_FAILED);
            response.getWriter().print(JSONUtil.toJsonStr(result));
            response.getWriter().flush();
        };
    }

    /**
     * 可自定义实现
     *
     * @return
     */
    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }
}

这个类整合了

1、 令牌相关的配置

JwtAccessTokenConverter令牌增强,用于JWT令牌和OAuth身份进行转换。

JwtTokenStore存储模式(这里使用的是JWTTokenStore存储模式,此外还有RedisTokenStore和JdbcTokenStore), JwtTokenStore 使用 JWT(JSON Web Token)作为存储模式,它将令牌作为简单的字符串进行存储, 每当用户登录并成功认证后,系统会生成一个 JWT Token 返回给客户端。这个 JWT Token 包含一些关键信息,如用户身份标识和访问权限等。

这里使用的是非对称加密,通过Keypair获取密钥对。

非对称加密指:通过密钥对获取公钥和私钥,认证服务自己保留私钥,将公钥下发给受信任的客户端、资源服务,公钥和私钥是配对的, 成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密非对称加密更安全一些。

2、 令牌管理服务的配置

使用的是DefaultTokenServices这个实现类,通过上述代码的注释可以了解清楚, 至于final修饰的那部分内容,可以去源码查看。

3、 令牌访问端点的配置

给端点设置认证管理器,JWT令牌和OAuth身份转换,令牌增强器,令牌授权者,令牌管理服务。

4、 客户端配置

基于 JDBC 的客户端详情服务配置为当前的 Spring Security OAuth2 客户端配置。

此外还设置了自定义认证异常响应数据。

代码摘自

youlai-mall: youlai-mall是基于Spring Boot 2.7、Spring Cloud & Alibaba 、vue3、element-plus、uni-app 构建的开源全栈微服务商城项目。 (gitee.com)