1. Oauth2认证

1.1 Oauth2简介

1.1.1 简介

  第三方认证技术方案最主要的解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。

  Oauth协议为用户资源的授权提供了一个安全的、开放而又简介的标准。同时,任何第三方都可以使用Oauth认证服务,任何服务提供商都可以实现自身的Oauth认证服务,因而Oauth是开放的。业界提供了Oauth的多种实现如PHP、JavaScript、Java、Ruby等各种语言开发包,大大节约了程序员的时间,因而Oauth是简易的。互联网很多服务的Open API,很多大公司如Google、Yahoo、Microsoft等都提供了Oauth认证服务,这些都足以说明Oauth标准主键成为开放资源授权的标准。

  Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。

  参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin

  Oauth协议:https://tools.ietf.org/html/rfc6749

  下边分析一个Oauth2认证的例子,网站使用微信认证的过程:

java dummy 认证 java oauth认证_服务器

 

  1. 用户进入网站的登录页面,点击微信的图标以微信账号登录系统,用户是自己的微信里的信息资源拥有者。

  点击“微信”出现一个二维码,此时用户用微信扫描二维码,开始给网站授权。

  2. 资源拥有者同意给客户端授权

  资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证,验证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到网站。

  3. 客户端获取到授权码,请求认证服务器申请令牌

  此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。

  4. 认证服务器向客户端响应令牌

  认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。

  5. 客户端请求资源服务器的资源

  客户端携带令牌访问资源服务器的资源。网站携带令牌请求访问微信服务器获取用户的基本信息。

  6. 资源服务器返回受保护资源

  资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

  注意:资源服务器和认证服务器可以是一个服务业可以分开的服务,如果是分开的服务资源服务器通常请求认证服务器来校验令牌的合法性。

 

  Oauth2.0认证流程如下:

  引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749

java dummy 认证 java oauth认证_spring_02

 

1.1.2 角色

  客户端:本身不存在资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器)、微信客户端等。

  资源拥有者:通常为用户,也可以是应用程序,即该资源的拥有者。

  授权服务器(也称认证服务器):用来对资源拥有者身份认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授权后可以访问。

  资源服务器:存储资源的服务器。比如:网站用户管理服务器存储了网站用户信息,网站相册服务器存储了用户的相册信息,微信的资源服务存储了微信的用户信息等。客户端最终访问的资源服务器获取资源信息。

 

1.1.3 常用术语

  客户凭证(client Credentials):客户端的clientId和密码用于认证客户。

  令牌(tokens):授权服务器在接收到客户请求后,颁发的访问令牌。

  作用域(scopes):客户请求访问令牌时,由资源拥有者额外指定的细分权限(Permission)

 

1.1.4 令牌类型

  授权码:仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌。

  访问令牌:用于代表一个用户或服务直接去访问受保护的资源。

  刷新令牌:用于取授权服务器获取一个刷新访问令牌。

  BearerToken:不管谁拿到Token都可以访问资源,类似现金。

  Proof of Possession(Pop)Token:可以校验client是否对Token有明确的拥有权。

 

1.1.5 特点

  优点:

  更安全,客户端不接触用于密码,服务器端更易集中保护;

  广泛传播并被持续采用;

  短寿命和封装的Token;

  资源服务器和授权服务器解耦;

  集中式授权,简化客户端;

  HTTP/JSON友好,易于请求和传递token;

  考虑多种客户端架构场景;

  客户可以拥有不同的信任级别;

  缺点:

  协议框架太宽泛,造成各种实现的兼容性和互操作性差;

  不是一个认证协议,本身并不能告诉你任何用户信息。

 

1.2 授权模式

  授权码模式(Authorization code)

  简化授权模式(Implicit)

  密码模式(Resource Owner PasswordCredentials)

  客户端模式(Client Credentials)

  刷新令牌

 

1.3 Spring Security Oauth2

1.3.1 授权服务器

java dummy 认证 java oauth认证_spring_03

  Authorize Endpoint:授权端点,进行授权

  Token Endpoint:令牌端点,经过授权拿到对应的Token

  Introspection Endpoint:校验端点,校验Token的合法性

  Revocation Endpoint:撤销端点,撤销授权

 

1.3.1 Spring Security Oauth2 架构

java dummy 认证 java oauth认证_客户端_04

 

 

 

  流程:

  1、用户访问,此时没有Token,Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter捕获并重定向到认证服务器。

  2、认证服务器通过Authorization EndPoint进行授权,并通过Authorization·ServerTokenServices生成授权码并返回给客户端。

  3、客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端。

  4、客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验,检验通过可以获取资源。

 

2 SpringSecurity Oauth2样例

2.1 授权码模式

2.1.1 实例代码

  我们新建一个Maven项目,pom文件如下:

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
        <spring-boot.version>2.1.3.RELEASE</spring-boot.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

  启动类

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

  SpringSecurity配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**", "/login/**", "/logout/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
}

  授权服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
   private PasswordEncoder passwordEncoder;
@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  // 测试需要,放到内存里
                // 客户端ID
                .withClient("client")
                // 秘钥
                .secret(passwordEncoder.encode("112233"))
                // 重定向地址
                .redirectUris("http://www.baidu.com")
                // 授权范围
                .scopes("all")
                // 授权类型:authorization_code 授权码模式
                .authorizedGrantTypes("authorization_code");
    }
}

  资源服务器配置

注意:正常的微服务项目,授权服务器和资源服务器是分别在不同的微服务里的,不应该放在一起。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .antMatchers("/user/**")
                .permitAll();
    }
}

  User实体类

public class User implements UserDetails {

    private String username;
    private String password;
    private List<GrantedAuthority> authorities;

    public User(String username, String password, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

  Service类

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String password = passwordEncoder.encode("123456");
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

  Controller,这里的Controller就是需要被授权的资源了。

@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 返回当前用户
     */
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication){
        return authentication.getPrincipal();
    }

}

 

2.1.2 功能演示

  我们访问这个地址:http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all

  会跳转到登录页,因为上面的配置我们只验证了密码123456,用户名可以随便输入就能进来。

  登录成功后,调整到认证服务器页面

java dummy 认证 java oauth认证_spring_05

 

  提示让我们选择是否允许“client” 授权访问被保护的资源?选择 Approve后,跳转到我们配置的www.baidu.com 页面。 baidu.com 地址后面的code参数就是授权码。

  我们需要使用授权码获取令牌。

 

2.1.3 获取令牌

  我们需要使用Postman访问  http://localhost:8080/oauth/token 获取令牌

  先设置Authorization页面。这里的username是clientId,password是秘钥

java dummy 认证 java oauth认证_spring_06

 

 

 

   设置Body页面。code填写的是刚才登陆成功后baidu.com页面的code授权码 AuthorizationServerConfig 类里配置的。

java dummy 认证 java oauth认证_客户端_07

 

  请求成功后,我们会在结果里看到 access_token,这就是我们要的令牌。

 

2.1.4 访问资源  

  我们依然使用Postman访问接口资源:http://localhost:8080/user/getCurrentUser

  在Authorization标签页中,Type选择Bearer Token,并且把刚才生成的token复制进去访问。

 

2.2 密码模式

2.2.1 实例代码

  在 SpringSecurity 配置类上,声明一个Bean

@Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

   把 AuthorizationServerConfig 改造成这样,新增了一个 configure方法,并且把 authorizedGrantTypes 改成了同时支持 令牌和密码模式。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserService userService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager).userDetailsService(userService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  // 测试需要,放到内存里
                // 客户端ID
                .withClient("client")
                // 秘钥
                .secret(passwordEncoder.encode("112233"))
                // 重定向地址
                .redirectUris("http://www.baidu.com")
                // 授权范围
                .scopes("all")
                /*
                 * 授权类型:
                 * authorization_code 授权码模式
                 * password  密码模式
                 */
                .authorizedGrantTypes("authorization_code", "password");
    }
    
}

2.2.2 获取令牌

   Postman中的 Authorization 页面不用修改,把 body 页面修改如下:

java dummy 认证 java oauth认证_spring_08

 

   发送请求后获得Token,还是可以访问资源接口。

 

2.3 使用Redis存储Token

  在之前的代码中,我们将生成的Token存到内存中,但在生成环境中是不合理的,在分布式环境的场景下,需要放到Redis里。

2.3.1 添加依赖

  修改 pom.xml 文件

<!-- redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 连接池依赖 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

  配置 application.properties 文件

# Redis 配置
spring.redis.host=192.168.10.100

 

 2.3.2 代码实现

  新建一个Redis配置类,创建 redisTokenStore 对象。

@Configuration
public class RedisConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }

}

  在 AuthorizationServerConfig 配置类中,改造如下

@Autowired
    @Qualifier(value = "redisTokenStore")
    private TokenStore tokenStore;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                .tokenStore(tokenStore);
    }

  我们还继续使用密码模式访问获取token,发现 Redis 中自动保存了刚才返回的Token。

 

3. JWT

3.1 常见的认证机制

3.1.1 HTTP Basic Auth

  HTTP Basic Auth 简单点说明就是每次请求API时都提供用户的username 和 password,简言之,Basic Auth 是配合RESTful API 使用的最简单的认证方式,只需要提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生成环境下被使用的越来越少。因此,在开发以外的RESTful API时,尽量避免采用HTTP Basic Auth.

 

3.1.2 Cookie Auth

  Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建一个Cookie对象;通过客户端带上Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie的expire time使cookie在一定时间内有效。

 

3.1.3 OAuth

  OAuth(开发授权,Open Authorization) 是一个开发的授权标准,允许用户让第三方应用访问该用户在某一web服务器上存储的私密的资源(如照片、视频、联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。

  OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定的服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时间(例如,接下来的2小时)访问特定的资源(例如仅仅是某一个相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

  这种基于OAuth的认证机制适用于个人消费者类的互联网应用,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

  缺点:比较重。

 

3.1.4 Token Auth

  使用基于Token的身份验证方法,在服务器端不需要存储用户的登录记录,大概流程是这样的:

  (1)客户端使用用户名和密码请求登录;

  (2)服务端收到请求,去验证用户名和密码;

  (3)验证成功后,服务端会签发一个Token,再把这个Token发给客户端;

  (4)客户端收到Token以后可以把它存储起来,比如放在Cookie里;

  (5)客户端每次向服务端请求资源的时候需要待着服务端签发的Token;

  (6)服务端收到请求,会去验证客户端请求里带着的Token,如果验证成功,就向客户端返回请求的数据;

  这种方式比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方便更加轻量。

  具体,Token Auth的优点(Token机制相对于Cookie机制又有什么好处呢?)

  (1)支持跨域访问:Cookie是不允许跨域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输;

  (2)无状态(也称:服务端可扩展):Token机制在服务端不需要存储session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息;

  (3)更实用CDN:可以通过内容分发网络请求服务端的所有资料(如:JavaScript,HTML,图片等),而你的服务端只要提供API即可;

  (4)去偶:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可;

  (5)更适用于移动应用:当你的客户端是一个原生平台(IOS,Android,Windows 10等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多;

  (6)CSRS:因为不再依赖于Cookie,所有你就不需要考虑CSRF(跨域请求伪造)的防范;

  (7)性能:一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多;

  (8)不需要为登陆页面做特殊处理:如果你使用 Protractor 做功能测试的时候,不再需要为登陆页面做特殊处理;

  (9)基于标准化:你的API可以采用标准化的JSON Web Token(JWT)这个标准已经存在多个后端库(.Net,Ruby,Java,Python,PHP)和多家公司的支持(如:Firebase,Google,Microsoft)

3.2 JWT简介

3.2.1 什么是JWT

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

  官网:https://jwt.io/

  标准:https://tools.ietf.org/html/rfc7519

  JWT令牌的特点:

  (1)JWT基于json,非常方便解析;

  (2)可以在令牌中自定义丰富的内容,易扩展;

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

  (4)资源服务使用JWT可不依赖认证服务即可完成授权;

  缺点:

  (1)JWT令牌较长,占存储空间比较大;

 

3.2.2 JWT组成

  一个JWT实际上就是一个字符串,它由三部分组成:头部、负载、签名。

3.2.2.1 头部(Header)

  头部用于描述该JWT的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC、SHA256或RSA)等。这也可以被表示成一个JSON对象。

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

  typ:是类型

  alg:签名的算法,这里使用的算法是HS256算法

  我们对头部的json字符串进行Base64编码(网上有很多在线编码的网站),编码后的字符串如下:*****

  Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK中提供了非常方便的 BASE64Encoder 和 BASE64Decoder ,用它们可以非常方便的完成基于BASE64的编码和解码。

3.2.2.2 负载(Payload,荷载)

  负载是存放有效信息的地方。这个名字像是特指飞机上承载的货物,这些有效信息包含三个部分:

  (1)标准中注册的声明(建议但不强制使用)

  iss:jwt芊发者

  sub:jwt所面向的用户

  aud:接收jwt的一方

  exp:jwt的过期时间,这个过期时间必须要大于签发时间

  nbf:定义在什么时间之前,该jwt都是不可用的

  iat:jwt的签发时间

  jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  (2)公共的声明

   公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可加密。

  (3)私有的声明

  私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

  这个指的是自定义的 claim。比如下面那个举例中的name都属于自定义的claim。这些 claim 是JWT标准规定的 claim 区别在于:JWT规定的 claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的 claim 进行验证(还不知道是否能够验证);而 private claims 不会验证,除非明确告诉接收方要对这些 claim 进行验证以及规则才行。

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

  其中 sub 是标注的声明, name 是自定义的声明(公共的或私有的)

  然后将其进行 base64 编码,得到jwt的第二部分:*****

  提示:声明中不要放一些敏感信息。

3.2.2.3 签名(signature)

  Jwt的第三部分是一个签证信息,这个签证信息由三个部分组成:

  (1)header(base64加密后的)

  (2)payload(base64加密后的)

  (3)secret(盐、一定要保密)

  这个部分需要base64 加密后的header和base64加密后的 payload 使用,连接组成的字符串,然后通过header中声明的加密方式进行加盐secret 组合加密,然后就构成了jwt的第三部分:*****

  将这三部分用.连接成一个完成的字符串,构成了最终的jwt:*****.*****.*****

  注意:secret 是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt 的签发和 jwt的验证。所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知 secret,那就意味着客户端是可以自我签发jwt 了。

 

3.3 JJWT简介

3.3.1 什么是JJWT

  JJWT 是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

  规范官网:https://jwt.io/

 

3.3.2 快速入门

3.3.2.1 token的创建

  创建SpingBoot工程,引入依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <mavne.compiler.target>1.8</mavne.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

  Token创建方法

@Test
    public void testJwt(){
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("888")     // 标识
                .setSubject("Rose") // 用户
                .setIssuedAt(new Date())  // 创建时间
                // 签名手段,参数:算法、盐
                .signWith(SignatureAlgorithm.HS256, "xxxx");
        // 签发token
        String token = jwtBuilder.compact();
        System.out.println(token);

        System.out.println(" ==================== 下面演示的对token的字符串解密");

        String[] split = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
        // 会乱码,因为经过HS256的加密串是不能解密的。
        System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
    }

  打印结果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjQ4NjI5MTMxfQ.XIC5S_AeV0uuoWAx_jW7Gfy8UuxYQx922LPa8QMofmo
==================== 下面演示的对token的字符串解密
{"alg":"HS256"}
{"jti":"888","sub":"Rose","iat":1648629131
\��H���Xc[����.ń1�m�=�2��

 

3.3.2.2 token的验证解析

  我们已经创建了Token,在web应用中这个操作是由服务端进行并发给客户端的,客户端在下次向服务端发送请求时需要携带这个token,服务端接收到token之后,应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回的响应结果。

  注意:签发时的秘钥必须要和加密时保持一致。

/**
     * 解析token
     */
    @Test
    public void testParseToken(){
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjQ4NjI5MTMxfQ.XIC5S_AeV0uuoWAx_jW7Gfy8UuxYQx922LPa8QMofmo";
        // 解析Token,获取claims,jwt中荷载声明的对象
        Claims claims = (Claims) Jwts.parser()
                .setSigningKey("xxxx") // 签发的秘钥
                .parse(token)
                .getBody();
        System.out.println("id = " + claims.getId());
        System.out.println("sub = " + claims.getSubject());
        System.out.println("iat = " + claims.getIssuedAt());
    }

  打印结果

id = 888
sub = Rose
iat = Wed Mar 30 16:32:11 CST 2022

 

3.3.2.3 token的验证解析

  在一般场景中,我们并不希望签发的token是永久生效的(上面我们生成的token就是永久的),所以我们可以为token添加一个过期时间。原因:从服务器签发出的token,服务器自己并不做记录,就存在一个弊端,服务端无法主动控制某token的立刻失效。

  在生成Token的方法中加上如下参数:

.setExpiration(new Date(System.currentTimeMillis() + 600000))

  参数值是Date类型,传入的是要过期的时间。当Token过期后,使用解析方法会抛出异常。

 

3.3.3 使用JwtToken

  创建一个JwtToken的配置类

@Configuration
public class JwtTokenStoreConfig {

    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 设置jwt秘钥
        jwtAccessTokenConverter.setSigningKey("test_key");
        return jwtAccessTokenConverter;
    }

}

  改造之前写过的 AuthorizationServerConfig 配置类。其他方法保持不变

@Autowired
    @Qualifier(value = "jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                // accessToken 转 jwtToken
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter);
    }

 

3.3.4 JJWT与Oauth2集成,增强拓展

  我们先定义一个拓展类

public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> map = new HashMap<>();
        map.put("enhance", "enhancer info");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
        return accessToken;
    }

}

  在 JwtTokenStoreConfig 里配置上这个类。

@Bean
    public JwtTokenEnhancer jwtTokenEnhancer(){
        return new JwtTokenEnhancer();
    }

  改造 AuthorizationServerConfig 类的 configure方法

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 定义增强内容
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);
        // 设置增强内容
        TokenEnhancerChain chain = new TokenEnhancerChain();
        chain.setTokenEnhancers(delegates);
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                // accessToken 转 jwtToken
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(chain);
    }

 

3.3.5 Jwt的Token解析

  首先我们确保有jjwt的jar包。

<dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

  我们直接在Controller层中,在方法中增加参数 HttpServletRequest,我们再从 request 中拿到Token,并解析 

@RequestMapping("/getCurrentUser")
    public Object getCurrentUser(HttpServletRequest request, Authentication authentication){
        String header = request.getHeader("Authorization");
        String token = header.substring(header.lastIndexOf("bearer") + 7);
        return Jwts.parser()
                .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
    }

 

3.3.6 Token的有效期验证

  我们上面的例子中生成的Token,都是没有设置有效期的。下面我们要生成一个带有效期的token。

  在 AuthorizationServerConfig 配置类中,修改第二个配置方法

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  // 测试需要,放到内存里
                // 客户端ID
                .withClient("client")
                // 秘钥
                .secret(passwordEncoder.encode("112233"))
                // 重定向地址
                .redirectUris("http://www.baidu.com")
                // 授权范围
                .scopes("all")
                // 设置Token失效时间
                .accessTokenValiditySeconds(60)
                /*
                 * 授权类型:
                 * authorization_code 授权码模式
                 * password  密码模式
                 */
                .authorizedGrantTypes("authorization_code", "password");
    }

  token设置的时间为是秒,当token失效之后,用户请求会失败。

  我们可以设置Token刷新机制,改造如下:

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  // 测试需要,放到内存里
                // 客户端ID
                .withClient("client")
                // 秘钥
                .secret(passwordEncoder.encode("112233"))
                // 重定向地址
                .redirectUris("http://www.baidu.com")
                // 授权范围
                .scopes("all")
                // 设置Token失效时间,单位s
                .accessTokenValiditySeconds(60)
                // 刷新后,令牌有效期,单位s
                .refreshTokenValiditySeconds(36400)
                /*
                 * 授权类型:
                 * authorization_code 授权码模式
                 * password  密码模式
                 * refresh_token  刷新令牌
                 */
                .authorizedGrantTypes("authorization_code", "password", "refresh_token");
    }