OAuth2+JWT 实现权限验证

Java
352
0
0
2023-06-04

前言

微服务架构下统⼀认证思路主要有两种形式:

1、基于 Session 的认证⽅式在分布式的环境下,基于 session 的认证会出现⼀个问题,每个应⽤服务都需要在session中存储⽤户身份信息,通过负载均衡将本地的请求分配到另⼀个应⽤服务需要将 session 信息带过去,否则会重新认证。我们可以使⽤ Session 共享、Session 黏贴等⽅案。Session ⽅案也有缺点,⽐如基于 cookie ,移动端不能有效使⽤等

2、基于 token 的认证⽅式。基于token的认证⽅式,服务端不⽤存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地⽅,并且可以实现 web 和 app 统⼀认证机制。其缺点也很明显,token 由于⾃包含信息,因此⼀般数据量较⼤,⽽且每次请求 都需要传递,因此⽐较占带宽。另外,token 的签名验签操作也会给 cpu 带来额外的处理负担。

下面我们就基于 token 的认证⽅式。采用 OAuth 2 框架来实现。

oauth 2 开放授权协议/标准

OAuth(开放授权)是⼀个开放协议/标准,允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容。允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容。

OAuth2 协议流程图如下:

1、客户端请求用户授权

2、用户确认授权

3、客户端收到授权许可后,向认证服务器申请令牌

4、认证服务器验证授权许可,向客户端返回有效令牌

5、客户端携带有效令牌访问资源服务器

6、资源服务器从认证服务器中验证有效令牌。

7、验证通过后,返回对应的资源给客户端。

什么情况下需要使⽤ OAuth2 ?

第三⽅授权登录的场景:⽐如,我们经常登录⼀些⽹站或者应⽤的时候,可以选择使⽤第三⽅授权登录的⽅式,⽐如: 微信 授权登录、QQ授权登录、 微博 授权登录等,这是典型的 OAuth2 使⽤场景。单点登录的场景:如果项⽬中有很多微服务或者公司内部有很多服务,可以专⻔做⼀个认证中⼼(充当认证平台⻆⾊),所有的服务都要到这个认证中⼼做认证,只做⼀次登录,就可以在多个授权范围内的服务中⾃由串⾏。

Spring Cloud OAuth2 + JWT 实现

Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多个微服务的统⼀认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务(统⼀认证授权服务)发送某个类型的 grant_type 进⾏集中认证和授权,从⽽获得 access_token(访问令牌),⽽这个 token 是受其他微服务信任的。

使⽤ OAuth2 解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了资源的拥有者,在授权层⾥⾯,资源的拥有者可以给第三⽅应⽤授权去访问我们的某些受保护资源。

搭建认证服务器

创建一个新的的模块,service-oauth-hw-9900。

依赖

pom 文件中依赖如下:

 <dependency>
    <groupId> org .springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.11. RELEASE </version>
</dependency>
<!--引入security对oauth2的支持-->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>  

配置文件

 server:
  port: 9900
spring:
  application:
    name: service-oauth-hw
  zipkin:
    base-url:  # zipkin server的请求地址
    sender:
      # web 客户端将踪迹日志数据通过网络请求的方式传送到服务端,另外还有配置
      # kafka/rabbit 客户端将踪迹日志数据传递到mq进行中转
      type: web
    sleuth:
      sampler:
        # 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集
        # 生产环境下,请求量非常大,没有必要所有请求的踪迹数据都采集分析,对于网络包括server端压力都是比较大的,可以配置采样率采集一定比例的请求的踪迹数据进行分析即可
        probability: 1
eureka:
  client:
    serviceUrl: # eureka server的路径
      defaultZone:  #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
    instance:
      prefer-ip-address: true #使用ip注册
#分布式链路追踪
logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: debug
    org.springframework.cloud.sleuth: debug  

启动类

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

config

自定义一个 OauthServerConfiger。当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)

 @Configuration
@EnableAuthorizationServer  //开启认证服务器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
     @Autowired 
    private AuthenticationManager authenticationManager;
    /**
     * 客户端详情配置,
     *  比如client_id,secret
     *  当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
     *  颁发client_id等必要参数,表明客户端是谁
     * @param clients
     * @throws Exception
     */
    @ Override 
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);
        // 客户端信息存储在什么地方,可以在内存中,可以在数据库里
        clients.inMemory()
                // 添加一个client配置,指定其client_id
                .withClient("quellanan")
                //指定客户端的密码/安全码
                .secret("abcdefg")
                //指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
                .redirectUris("*")
                //认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
                .authorizedGrantTypes("password","refresh_token")
                //客户端的权限范围,此处配置为all全部即可
                .scopes("all");
    }
    /**
     * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
     * 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
        // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
        security
                // 允许客户端表单认证
                .allowFormAuthenticationForClients()
                // 开启端口/oauth/token_key的访问权限(允许)
                .tokenKeyAccess("permitAll()")
                // 开启端口/oauth/check_token的访问权限(允许)
                .checkTokenAccess("permitAll()");
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
        endpoints
                // 指定token的存储方法
                .tokenStore(tokenStore())
                // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
                .tokenServices(authorizationServerTokenServices())
                // 指定认证管理器,随后注入一个到当前类使用即可
                .authenticationManager(authenticationManager)
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }
    /*
        该方法用于创建tokenStore对象(令牌存储对象)
        token以什么形式存储
     */
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
        // 使用jwt令牌
        //return new JwtTokenStore(jwtAccessTokenConverter());
    }
    /**
     * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
     */
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        // 使用默认实现
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
        defaultTokenServices.setTokenStore(tokenStore());
        // 针对jwt令牌的添加
        //defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        // 设置令牌有效时间(一般设置为2个小时)
        defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
        return defaultTokenServices;
    }
}  

关于三个 configure ⽅法

configure(ClientDetailsServiceConfifigurer clients):⽤来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这⾥进⾏初始化,你能够把客户端详情信息写死在这⾥或者是通过数据库来存储调取详情信息

confifigure(AuthorizationServerEndpointsConfifigurer endpoints):⽤来配置令牌(token)的访问端点和令牌服务(token services)

confifigure(AuthorizationServerSecurityConfifigurer oauthServer):⽤来配置令牌端点的安全约束.

关于 TokenStore

InMemoryTokenStore默认采⽤,它可以完美的⼯作在单服务器上(即访问并发量 压⼒不⼤的情况下,并且它在失败的时候不会进⾏备份),⼤多数的项⽬都可以使⽤这个版本的实现来进⾏ 尝试,你可以在开发的时候使⽤它来进⾏管理,因为不会被保存到磁盘中,所以更易于调试。

JdbcTokenStore这是⼀个基于JDBC的实现版本,令牌会被保存进关系型数据库。使⽤这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤这个版本的时候请注意把”springjdbc”这个依赖加⼊到你的 classpath当中。JwtTokenStore 这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进⾏编码(因此对于后端服务来说,它不需要进⾏存储,这将是⼀个重⼤优势),缺点就是这个令牌占⽤的空间会⽐较⼤,如果你加⼊了⽐较多⽤户凭证信息,JwtTokenStore 不会保存任何数据。

然后再自定义有一个配置类,主要处理用户名和密码的校验等事宜。

 @Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    //@Autowired
    //private JdbcUserDetailsService jdbcUserDetailsService;
    /**
     * 注册一个认证管理器对象到容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    /**
     * 密码编码对象(密码不进行加密处理)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    /**
     * 处理用户名和密码验证事宜
     * 1)客户端传递username和password参数到认证服务器
     * 2)一般来说,username和password会存储在数据库中的用户表中
     * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
        // 实例化一个用户对象(相当于数据表中的一条用户记录)
        UserDetails user = new User("admin","123456",new ArrayList<>());
        auth.inMemoryAuthentication()
                .withUser(user).passwordEncoder(passwordEncoder);
        //auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
    }
}  

JWT 改造统⼀认证授权中⼼的令牌存储机制

JWT 令牌介绍

通过上边的测试我们发现,当资源服务和授权服务不在⼀起时资源服务使⽤RemoteTokenServices 远程请求授权 服务验证token,如果访问量较⼤将会影响系统的性能。

解决上边问题: 令牌采⽤JWT格式即可解决上边的问题,⽤户认证通过会得到⼀个JWT令牌,JWT令牌中已经包括了⽤户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证 服务完成授权。

什么是JWT?

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

JWT令牌结构

JWT 令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz

header 。头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA),例如

 {
 "alg": "HS256",
 "typ": "JWT"
}  

将上边的内容使⽤Base64Url编码,得到⼀个 字符串 就是JWT令牌的第⼀部分。

Payload。第⼆部分是负载,内容也是⼀个json对象,它是存放有效信息的地⽅,它可以存放jwt提供的现成字段,⽐ 如:iss(签发者),exp(过期时间戳), sub(⾯向的⽤户)等,也可⾃定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第⼆部分负载使⽤Base64Url编码,得到⼀个字符串就是JWT令牌的第⼆部分。 ⼀个例⼦:

 {
 "sub": "1234567890",
 "name": "John Doe",
 "iat": 1516239022
}  

Signature。第三部分是签名,此部分⽤于防⽌jwt内容被篡改。 这个部分使⽤base64url将前两部分进⾏编码,编码后使⽤点(.)连接组成字符串,最后使⽤header中声明 签名算法进⾏签名。

 HMACSHA256(
 base64UrlEncode(header) + "." +
 base64UrlEncode(payload),
 secret)  
  1. base64UrlEncode(header):jwt令牌的第⼀部分。
  2. base64UrlEncode(payload):jwt令牌的第⼆部分。

secret:签名所使⽤的密钥。

认证服务器端JWT改造(改造主配置类)

 /*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
    //return new InMemoryTokenStore();
    // 使用jwt令牌
    return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
    jwtAccessTokenConverter.setSigningKey(sign_key);  // 签名密钥
    jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));  // 验证时使用的密钥,和签名密钥保持一致
    jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}  

修改 JWT 令牌服务⽅法

 /**
 * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
 */
public AuthorizationServerTokenServices authorizationServerTokenServices() {
    // 使用默认实现
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
    defaultTokenServices.setTokenStore(tokenStore());
    // 针对jwt令牌的添加
    defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
    // 设置令牌有效时间(一般设置为2个小时)
    defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
    // 设置刷新令牌的有效时间
    defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
    return defaultTokenServices;
}  

总结

我们在实际工作中,token 鉴权的方式是很常见的现在,这一套解决方案也可以直接使用到项目中,小伙伴们赶紧学习起来吧。