SpringCloud系列 (十二) 微服务统⼀认证⽅案 Spring Cloud OAuth2 + JWT

595 阅读16分钟

微服务统⼀认证⽅案 Spring Cloud OAuth2 +JWT

1. 微服务架构下统⼀认证思路

基于Session的认证⽅式

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

Session⽅案也有缺点,⽐如基于cookie,移动端不能有效使⽤等

基于token的认证⽅式 基于token的认证⽅式,服务端不⽤存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地⽅,并且可以实现web和app统⼀认证机制。

其缺点也很明显,token由于⾃包含信息,因此⼀般数据量较⼤,⽽且每次请求 都需要传递,因此⽐较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。

2. OAuth2开放授权协议/标准

OAuth2介绍

OAuth(开放授权)是⼀个开放协议/标准

允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容

结合“使⽤QQ登录拉勾”这个场景拆分理解上述那句话

  • ⽤户:我们⾃⼰
  • 第三⽅应⽤:拉勾⽹
  • 另外的服务提供者:QQ

OAuth2是OAuth协议的延续版本,但不向后兼容OAuth1即完全废⽌了OAuth1。

OAuth2协议⻆⾊和流程

拉勾⽹要开发使⽤QQ登录这个功能的话,那么拉勾⽹是需要提前到QQ平台进⾏登记的(否则QQ凭什么陪着拉勾⽹玩授权登录这件事)

1)拉勾⽹——登记——>QQ平台

2)QQ 平台会颁发⼀些参数给拉勾⽹,后续上线进⾏授权登录的时候(刚才打开授权⻚⾯)需要携带这些参数

  • client_id:客户端id(QQ最终相当于⼀个认证授权服务器,拉勾⽹就相当于⼀个客户端了,所以会给⼀个客户端id),相当于账号

  • secret:相当于密码

  • 资源所有者(Resource Owner):可以理解为⽤户⾃⼰
  • 客户端(Client):我们想登陆的⽹站或应⽤,⽐如拉勾⽹
  • 认证服务器(Authorization Server):可以理解为微信或者QQ
  • 资源服务器(Resource Server):可以理解为微信或者QQ

什么情况下需要使⽤OAuth2?

第三⽅授权登录的场景:⽐如,我们经常登录⼀些⽹站或者应⽤的时候,可以选择使⽤第三⽅授权登录的⽅式,⽐如:微信授权登录、QQ授权登录、微博授权登录等,这是典型的 OAuth2 使⽤场景。

单点登录的场景:如果项⽬中有很多微服务或者公司内部有很多服务,可以专⻔做⼀个认证中⼼(充当认证平台⻆⾊),所有的服务都要到这个认证中⼼做认证,只做⼀次登录,就可以在多个授权范围内的服务中⾃由串⾏。

OAuth2的颁发Token授权⽅式

  • 1)授权码(authorization-code)
  • 2)密码式(password)提供⽤户名+密码换取token令牌
  • 3)隐藏式(implicit)
  • 4)客户端凭证(client credentials)

授权码模式使⽤到了回调地址,是最复杂的授权⽅式,微博、微信、QQ等第三⽅登录就是这种模式。我们重点讲解接⼝对接中常使⽤的password密码模式(提供⽤户名+密码换取token)。

3. Spring Cloud OAuth2 + JWT 实现

Spring Cloud OAuth2介绍

Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多个微服务的统⼀认证(验证身份合法性)授权(验证权限)。

通过向OAuth2服务(统⼀认证授权服务)发送某个类型的grant_type进⾏集中认证和授权,从⽽获得access_token(访问令牌),⽽这个token是受其他微服务信任的。

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

Spring Cloud OAuth2构建微服务统⼀认证服务思路

注意:在我们统⼀认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接⼝就是资源,发起http请求的浏览器就是Client客户端(对应为第三⽅应⽤)

搭建认证服务器(Authorization Server)

  • 1 - 新建项⽬lagou-cloud-oauth-server-9999

  • 2 - pom

<!--导入Eureka Client依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>


<!--导入spring cloud oauth2依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </exclusion>
    </exclusions>
</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>
  • 3 - application.yml(构建认证服务器,配置⽂件⽆特别之处)
server:
  port: 9999
Spring:
  application:
    name: lagou-cloud-oauth-server
eureka:
  client:
    serviceUrl: # eureka server的路径
      defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
  instance:
    #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
    prefer-ip-address: true
    #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
  • 4 - ⼊⼝类⽆特殊之处
@SpringBootApplication
@EnableDiscoveryClient
public class OauthServerApplication9999 {

    public static void main(String[] args) {
        SpringApplication.run(OauthServerApplication9999.class,args);
    }
}
  • 5 - 认证服务器配置类

@Configuration
@EnableAuthorizationServer// 开启认证服务器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {

    /**
     * 客户端信息
     * ⽤来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这
     * ⾥进⾏初始化,你能够把客户端详情信息写死在这⾥或者是通过数据库来存
     * 储调取详情信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        super.configure(clients);

        clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
                .withClient("client_lagou")// 添加一个client配置,指定其client_id
                .secret("123456")// 指定客户端的密码/安全码
                .resourceIds("autodeliver") // 指定客户端所能访问资源id清单,
                                            // 此处的资源id是需要在具体的资源服务器上也配置一样

                // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,
                // 具体使用哪种方式颁发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);

        security
                .allowFormAuthenticationForClients()
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()");
    }

    /**
     * 认证服务器是玩转token的,那么这⾥配置token令牌管理相关(token此时就是⼀个字符串,
     * 当下的token需要在服务器端存储,
     * 那么存储在哪⾥呢?都是在这⾥配置)
     *
     * ⽤来配置令牌(token)的访问端点和令牌服务(token services)
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);

        endpoints
                .tokenStore(tokenStore()) // 指定token的存储⽅法
                .tokenServices(authorizationServerTokenServices()) // token服务的⼀个描述,可以认为是token⽣成细节的描述,⽐如有效时间多少等
                .authenticationManager(authenticationManager) //指定认证管理器,随后注⼊⼀个到当前类使⽤即可
        .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     该⽅法⽤于创建tokenStore对象(令牌存储对象)
     token以什么形式存储
     */
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }

    /**
     * 该⽅法⽤户获取⼀个token服务对象(该对象描述了token有效期等信息)
     */
    public AuthorizationServerTokenServices authorizationServerTokenServices(){

        //使用默认实现
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

        // 是否开启令牌刷新
        defaultTokenServices.setSupportRefreshToken(true);

        defaultTokenServices.setTokenStore(tokenStore());

        // 设置令牌有效时间(⼀般设置为2个⼩时),这里设置为20秒
        defaultTokenServices.setAccessTokenValiditySeconds(20);

        // 设置刷新令牌的有效时间
        defaultTokenServices.setRefreshTokenValiditySeconds(259200);

        return defaultTokenServices;
    }
 
}

===>关于三个configure⽅法

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

  • configure(AuthorizationServerEndpointsConfigurer endpoints) ⽤来配置令牌(token)的访问端点和令牌服务(token services)

  • configure(AuthorizationServerSecurityConfigureroauthServer) ⽤来配置令牌端点的安全约束.

===>关于 TokenStore

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

  • JdbcTokenStore 这是⼀个基于JDBC的实现版本,令牌会被保存进关系型数据库。使⽤这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤这个版本的时候请注意把"spring-jdbc"这个依赖加⼊到你的 classpath当中。

  • JwtTokenStore 这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进⾏编码(因此对于后端服务来说,它不需要进⾏存储,这将是⼀个重⼤优势),缺点就是这个令牌占⽤的空间会⽐较⼤,如果你加⼊了⽐较多⽤户凭证信息,JwtTokenStore 不会保存任何数据

  • 6 - 认证服务器安全配置类

@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {

    /**
     * 注册⼀个认证管理器对象到容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 处理⽤户名和密码验证事宜
     * 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);
    }
}
  • 测试

1、获取token:http://localhost:9999/oauth/token?client_secret=123456&grant_type=password&username=admin&password=123456&client_id=client_lagou

  • 参数信息
    • endpoint:/oauth/token
    • 获取token携带的参数
      • client_id:客户端id
      • client_secret:客户单密码
      • grant_type:指定使⽤哪种颁发类型,password
      • username:⽤户名
      • password:密码

2、校验token:http://localhost:9999/oauth/check_token?token=e62f4fef-3756-439e-b206-46b125124278

3、刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=123456&refresh_token=f9eb2cce-a082-401d-abc1-9ce2af2c096c

配置资源服务器

这里我们对自动投递微服务进行改造

  • pom 文件引入 oauth2的依赖
<!--导入spring cloud oauth2依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </exclusion>
    </exclusions>
</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>
  • ResourceServerConfiger
    • 配置了校验
    • 这里配置了autodeliver开头的API都要进行验证
    • demo大头的API要进行验证
    • 其他API不需要验证
    • 配置了校验接口地址

@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {

    /**
     * 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进⾏token校验等事宜
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

        // 设置当前资源服务的资源id
        resources.resourceId("autodeliver");

        // 定义token服务对象(token校验就应该靠token服务对象)
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();

        // 校验端点/接⼝设置
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");

        // 携带客户端id和客户端安全码
        remoteTokenServices.setClientId("client_lagou");
        remoteTokenServices.setClientSecret("123456");

        // 别忘了这一步
        resources.tokenServices(remoteTokenServices);
    }

    /**
     * 场景:⼀个服务中可能有很多资源(API接⼝)
     * 某⼀些API接⼝,需要先认证,才能访问
     * 某⼀些API接⼝,压根就不需要认证,本来就是对外开放的接⼝
     * 我们就需要对不同特点的接⼝区分对待(在当前configure⽅法中完成),
     * 设置是否需要经过认证
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()

                //autodeliver为前缀的请求需要认证
                .antMatchers("/autodeliver/**").authenticated()

                //demo为前缀的请求需要认证
                .antMatchers("/demo/**").authenticated()
                .anyRequest().permitAll();
    }
}
  • 添加两个测试的Controller
@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("/test")
    public String findResumeOpenState() {
        Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
        return "demo/test!";
    }
}

@RestController
@RequestMapping("/others")
public class OthersController {
    @GetMapping("/test")
    public String findResumeOpenState() {
        return "others/test!";
    }
}
  • 测试 1、自动投递微服务在进行配置后,如果不传递access_Token参数,则会返回未授权的错误

2、刚刚新建的demoController,如果不传递access_Token参数,则会返回未授权的错误

3、刚刚新建的demoController,如果不传递access_Token参数,也能返回成功信息

4、针对未授权的地址,在url中,带上access_token参数,即可完成调用

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

JWT令牌介绍

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

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

1)什么是JWT?

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

2)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)
 
 
# base64UrlEncode(header):jwt令牌的第⼀部分。

# base64UrlEncode(payload):jwt令牌的第⼆部分。

# secret:签名所使⽤的密钥。

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

改造Token的存储方式

修改 JWT 令牌服务⽅法

接口测试-使用Jwt方式的Token

资源服务器校验JWT令牌

不需要和远程认证服务器交互,添加本地tokenStore。原本的remote方式,每次校验都需要请求认证授权服务器的token校验接口,这样会导致 认证服务器压力过大,特别是当下这种大规模的微服务架构,压力更是大,所以,资源服务器这里,使用jwt后,就能本地就进行校验了。

这里增加的配置和服务端的配置一样,配置Token的相关秘钥

接口测试-使用Jwt方式的Token-校验授权服务器

从数据库加载Oauth2客户端信息

一般来说,我们需要将客户端的信息放在数据库中进行维护。为此需要使用jdbc去连接数据库进行客户端信息的获取

  • 1、创建数据表并初始化数据(表名及字段保持固定)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
 `client_id` varchar(48) NOT NULL,
 `resource_ids` varchar(256) DEFAULT NULL,
 `client_secret` varchar(256) DEFAULT NULL,
 `scope` varchar(256) DEFAULT NULL,
 `authorized_grant_types` varchar(256) DEFAULT NULL,
 `web_server_redirect_uri` varchar(256) DEFAULT NULL,
 `authorities` varchar(256) DEFAULT NULL,
 `access_token_validity` int(11) DEFAULT NULL,
 `refresh_token_validity` int(11) DEFAULT NULL,
 `additional_information` varchar(4096) DEFAULT NULL,
 `autoapprove` varchar(256) DEFAULT NULL,
 PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('client_lagou123',
'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token',
NULL, NULL, 7200, 259200, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
  • pom文件引入相关依赖,引入druid starter后,就能自动注入datasource了
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
<!--操作数据库需要事务控制-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>
  • 配置文件增加数据库配置,重点看druid datasource的相关配置
server:
  port: 9999
Spring:
  application:
    name: lagou-cloud-oauth-server
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2B8
    username: root
    password: root
    druid:
      initialSize: 10
      minIdle: 10
      maxActive: 30
      maxWait: 50000 
  • 认证服务器主配置类改造,对ClientDetailsServiceConfigurer进行配置
 @Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    super.configure(clients);
    //现在改成读取数据库
    clients.withClientDetails(createJdbcClientDetailsService());
}

@Autowired
private DataSource dataSource;

public JdbcClientDetailsService createJdbcClientDetailsService(){
    JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
    return jdbcClientDetailsService;
}

image.png

从数据库验证⽤户合法性

  • 创建数据表users(表名不需固定),初始化数据
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `username` char(10) DEFAULT NULL,
 `password` char(100) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (4, 'lagou-user', 'iuxyzds');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
  • 在common moudle中,增加一个JPA的User表配置
@Data
@Entity
@Table(name = "users")
public class Users{
    @Id
    private Long id;
    private String userName;
    private String password;
}
  • 在pom文件中,引入 common模块
<dependency>
    <groupId>com.lagou.edu</groupId>
    <artifactId>lagou-service-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  • 新建用户的仓储层 UsersRepository
public interface UsersRepository extends JpaRepository<Users,Long> {
    Users findByUsername(String username);
}
  • 新建服务 JdbcUserDetailService
@Service
public class JdbcUserDetailService implements UserDetailsService {

    @Autowired
    private UsersRepository usersRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Users users = usersRepository.findByUsername(username);

        return new User(users.getUsername(),users.getPassword(),new ArrayList<>());
    }
}
  • 改造 SecurityConfiger,从数据库中读取用户信息
    @Autowired
    private JdbcUserDetailService jdbcUserDetailService;
    
     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(jdbcUserDetailService).passwordEncoder(passwordEncoder);
    }

这样,就完成了从数据库中读取用户的改造,下面进行测试。请求的URL参数都是从数据库中读取的 image.png

基于Oauth2的 JWT 令牌信息扩展

OAuth2帮我们⽣成的JWT令牌载荷部分信息有限,关于⽤户信息只有⼀个 user_name,有些场景下我们希望放⼊⼀些扩展信息项,⽐如,之前我们经常向 session中存⼊userId,或者现在我希望在JWT的载荷部分存⼊当时请求令牌的客户 端IP,客户端携带令牌访问资源服务时,可以对⽐当前请求的客户端真实IP和令牌中 存放的客户端IP是否匹配,不匹配拒绝请求,以此进⼀步提⾼安全性。那么如何在 OAuth2环境下向JWT令牌中存如扩展信息?

  • 认证服务器⽣成JWT令牌时存⼊扩展信息(⽐如clientIp) 继承DefaultAccessTokenConverter类,重写convertAccessToken⽅法存⼊扩展信息

@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {

    @Override
    public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {

        //框架默认返回的Map
        Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        String remoteAddr = request.getRemoteAddr();

        stringMap.put("clientIp", remoteAddr);

        return stringMap;
    }
}

将⾃定义的转换器对象注⼊

    @Autowired
    private LagouAccessTokenConvertor lagouAccessTokenConvertor;
    
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sing_key);
        jwtAccessTokenConverter.setVerifier(new MacSigner(sing_key));

        ####==================>>>>
        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
        ####==================<<<<
        return jwtAccessTokenConverter;
    }

重新发布服务,获取jwt,验证下jwt中是否有clientIP属性

image.png

  • 资源服务器取出 JWT 令牌扩展信息 资源服务器也需要⾃定义⼀个转换器类,继承DefaultAccessTokenConverter,重 写extractAuthentication提取⽅法,把载荷信息设置到认证对象的details属性中
package com.lagou.edu.config;

import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
        oAuth2Authentication.setDetails(map);
        return  oAuth2Authentication;
    }
}

将⾃定义的转换器对象注⼊

  @Autowired
    private LagouAccessTokenConvertor lagouAccessTokenConvertor;

    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(sing_key);
        jwtAccessTokenConverter.setVerifier(new MacSigner(sing_key));

        jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);

        return jwtAccessTokenConverter;
    }

业务类⽐如Controller类中,可以通过 SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进⼀步 获取到扩展信息

@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("/test")
    public String findResumeOpenState() {
        Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();

        Object decodedDetails = ((OAuth2AuthenticationDetails) details).getDecodedDetails();

        Map<String, String> stringMap = (Map<String, String>) decodedDetails;

        String clientIp = stringMap.get("clientIp");

        System.out.println(clientIp);

        return "demo/test!";
    }
}

关于JWT令牌我们需要注意

  • JWT令牌就是⼀种可以被验证的数据组织格式,它的玩法很灵活,我们这⾥是基于Spring Cloud Oauth2 创建、校验JWT令牌

  • 我们也可以⾃⼰写⼯具类⽣成、校验JWT令牌

  • JWT令牌中不要存放过于敏感的信息,因为我们知道拿到令牌后,我们可以解码看到载荷部分的信息

  • JWT令牌每次请求都会携带,内容过多,会增加⽹络带宽占⽤