当前位置:   article > 正文

OAuth2+JWT

oauth2+jwt

简介

1、什么是 OAuth2

OAuth2 是针对一些特定问题的解决方案。之前使用 SpringSecurity,也可以实现登录的权限校验,但是它是通过 Session 来存储用户信息的,要在用户的浏览器客户端存储 Cookie。但是在分布式项目中不能实现单点登录,并且可能用户的浏览器客户端关闭了 Cookie功能,直接不让你存 Cookie 了。

而且继续使用 Session,就算解决了 Session 共享的问题,仍然是多点登录,即每个微服务模块都需要有验证模块进行权限校验,这是冗余的,应该将其抽离出公共模块进行共享。

1.1、主要解决的问题

并且使得登录功能的登录校验并不是应用本身在进行,而是交给了第三方去进行登录,然后再从第三方拉去登录的校验信息。

1.1.1、开放系统间的授权问题

比如照片拥有者将照片上传到云存储中,然后想进行打印,但是这个打印的服务是第三方的,无法直接访问云存储中的照片,这时候就需要 OAuth2 给第三方打印服务授权。

1.1.2、单点登录SSO 问题

单点登录就是说一个项目的多个服务中,本来都需要用户进行登录才可访问,但现在只需要在某个服务中登录一次,就可以访问该项目下的任何一个服务。

1.1.3、继续使用 Session 的情况 - Session 从客户端存储到 Redis 存储

以前是存储 Session,然后就比如把 Session 复制一份给别人,但是既浪费时间又浪费内存。所以可以考虑把权限校验信息进行统一存储,比如放在 Redis 中。

但是在微服务中使用 RestTemplate、OpenFeign 等远程调用,就无法携带对应 Session信息的 Cookie。

1.1.3.1、依赖

SpringSecurity 配合 Redis 存储 Session。

<!--  SpringSession Redis支持  -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--  添加Redis的Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
​
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.1.3.2、配置
spring:
  session:
    # 存储类型改为 Redis
    store-type: redis
  redis:
    # Redis服务器的信息,该咋写咋写
    host: 1.14.121.107

1.2、三种授权思路

1.2.1、用户名密码复制(不安全)

把用户的用户名和密码给第三方服务,但是很不安全,很容易导致泄露。

1.2.2、通用开发者 key

适用于合作商或授信的不同业务部门间使用。

有一把共用的万能钥匙,然后用万能钥匙进行访问,但是这把钥匙需要 app方公司和云存储方公司双方进行合作,现实中很难实现。

1.2.3、颁发令牌(OAuth2方式)

云存储方颁发令牌,然后第三方服务根据令牌去访问。OAuth2 只会提供颁发令牌的接口,具体的字符串生成规则需要自己整合,例如 JWT。

1.3、OAuth2 的四种授权模式

1.3.1、客户端模式(Client Credentials)

我们可以直接向验证服务器请求一个 Token令牌,我们需要在验证服务器(User Account And Authentication)服务拿到令牌之后,才能去访问资源。这样资源服务器才能知道我们是谁以及是否成功登录了。

并且此处是未加密的,虽然这种模式比较简便,但是已经失去了用户验证的意义,所以就不是给用户校验准备的,而是更适用于服务内部调用的场景。

1.3.2、密码模式(Resource Owner Password Credentials)

密码模式相比客户端模式,就多了用户名和密码的信息,用户需要提供对应账号的用户名和密码,才能获取到Token。

但是会直接将账号和密码泄露给客户端,需要后台完全信任客户端不会拿账号密码去干其他坏事,这肯定是不可取的。

1.3.3、隐式授权模式(Implicit Grant

它适用于没有服务端的第三方应用页面,并且相比前面一种形式,验证都是在验证服务器进行的,敏感信息不会轻易泄露,但是因为携带 Token 进行访问,所以 Token 依然存在泄露的风险。 

1.3.4、授权码模式(Authrization Code

这种模式是最安全的一种模式,也是推荐使用的一种模式。

相比隐式授权模式,它并不会直接返回 Token,而是返回授权码,真正的 Token 是通过应用服务器访问验证服务器获得的。

应用服务器(客户端通过访问自己的应用服务器来访问其他服务)和验证服务器之间会共享一个 secret,这个 secret 没有其他人知道含义,而验证服务器在用户验证完成之后,会返回一个授权码,应用服务器最后将 授权码和 secret 一起交给验证服务器进行验证,并且 Token 也是在服务端之间传递,不会直接给到客户端。

就算有人中途窃取了授权码,也毫无意义,因为 Token 的获取必须同时携带授权码和 secret,但是 secret 是第三方无法得知的,并且 Token 不会直接丢给客户端,而是给应用服务器,这样大大减少了泄露的风险。 

2、简单使用

2.1、实例1

新建一个模块 auth-server

2.1.1、搭建授权服务器

2.1.1.1、引入依赖
<!-- 父项目依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>2021.0.1</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
​
​
​
<!-- 子项目依赖 -->
<dependencies>
    <!--  OAuth2.0 依赖,新版本不再内置了,所以得我们自己指定一下版本  -->
    <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-web</artifactId>
    </dependency>
​
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>
2.1.1.2、配置文件
server:
  port: 8500
  servlet:
    # 为了防止会在服务之间跳转导致 Cookie 打架(因为所有服务地址都是 localhost,都会存 JSESSIONID)
    # 指定 Cookie 的保存路径,验证服务器需要单独存,防止覆盖,即防止和其他服务打架,但之后的请求都得在最前面加上此路径
    context-path: /sso
2.1.1.3、两个配置类
@EnableAuthorizationServer   //开启验证服务器
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
  @Resource
  private AuthenticationManager manager;
  @Resource // 刷新 token 的时候需要用到
  UserDetailsService userDetailsService;
​
  private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
​
  /**
   * 这个方法是对客户端进行配置,一个验证服务器可以预设很多个客户端,
   * 之后这些指定的客户端就可以按照下面指定的方式进行验证
   * @param clients 客户端配置工具
   */
  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients
      .inMemory()   //这里我们直接硬编码创建,当然也可以像Security那样自定义或是使用JDBC从数据库读取
      .withClient("web")   //客户端名称,随便起就行
      .secret(encoder.encode("654321"))      //只与客户端分享的secret,随便写,但是注意要加密
      .autoApprove(true)    //自动审批,这里关闭,要的就是一会体验那种感觉
      .scopes("book", "user", "borrow")     //授权范围,这里我们使用全部all
      //登录后的回调地址。可以写多个,当有多个时需要在验证请求中指定使用哪个地址进行回调
      .redirectUris("http://localhost:8101/login", "http://localhost:8201/login", "http://localhost:8301/login")
      .authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");
      //授权模式,一共支持5种,除了之前我们介绍的四种之外,还有一个刷新Token的模式
      //这里我们直接把五种都写上,方便一会实验,当然各位也可以单独只写一种一个一个进行测试
      //现在我们指定的客户端就支持这五种类型的授权方式了
  }
​
  @Override
  public void configure(AuthorizationServerSecurityConfigurer security) {
    security
      .passwordEncoder(encoder)    //编码器设定为BCryptPasswordEncoder
      .allowFormAuthenticationForClients()  //允许客户端使用表单验证,一会我们POST请求中会携带表单信息
      .checkTokenAccess("permitAll()");     //允许所有的Token查询请求
  }
​
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints
            // 刷新 token 的时候需要用到
            .userDetailsService(userDetailsService)
            .authenticationManager(manager);
    //由于SpringSecurity新版本的一些底层改动,这里需要配置一下authenticationManager,才能正常使用password模式
  }
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests()
          .anyRequest().authenticated()  //
          .and()
          .formLogin().permitAll();    //使用表单登录
    }
  
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
      auth
        .inMemoryAuthentication()   //直接创建一个用户,懒得搞数据库了
        .passwordEncoder(encoder)
        .withUser("test").password(encoder.encode("123456")).roles("USER");
    }
  
    @Bean   //这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
​
  @Bean // 刷新 token 的时候需要用到
  @Override
  public UserDetailsService userDetailsServiceBean() throws Exception {
      return super.userDetailsServiceBean();
  }
}

2.1.2、测试获取 token

默认的请求路径是:http://localhost:8500/sso/oauth/token

默认的 token校验路径是:http://localhost:8500/sso/oauth/check_token?token=token

除了客户端模式,通过其他模式登录的都可以在进行 token校验之后得到用户的登录信息。

2.1.2.1、测试客户端模式

客户端模式只需要提供 id 和 secret 即可直接拿到 Token,这个 id 和 secret 定义在了配置类里面。grant_type 指定授权方式。

将得到的 token 进行校验。

2.1.2.2、测试密码模式

我们还需要提供具体的用户名和密码,这个用户信息定义在了配置类里面。授权模式定义为 password,然后前端还需要在请求头中添加 Basic Auth 验证信息,这里我们直接写 id 和 secret 即可。

2.1.2.3、测试隐式授权模式

隐式授权模式需要在验证服务器上进行登录操作,而不是直接请求 Token,验证登录请求地址是:http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=token,注意 response_type 一定要是 token类型,这样才会直接返回 Token。访问该地址之后因为使用了 SpringSecurity,所以需要登录,登录后会需要进行一个授权,就像登录王者需要 QQ 的授权一样。然后就会调用回调地址,会以 GET方式传 token 等参数。

2.1.2.4、测试授权码模式

授权码模式其实和隐式授权模式是差不多的,但是请求的是code类型:http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=code。就也需要进行登录和授权,然后调用回调地址,但是只会传输一个 code 给回调地址,然后应用服务器通过这个 code 去请求验证服务器得到 token。

所以这个回调地址一般就是应用服务器的地址,应用服务器拿到 code 后,通过 code授权码和 secret 去请求 token,然后验证服务器把 token 返回给应用服务器。说白了就是通过验证码服务器得到验证码,然后再由应用服务器根据 code验证码和 secret 获取到 token。然后这个 token 浏览器是看不到的,因为是在内部进行请求的。

去请求的路径依旧是 http://localhost:8500/sso/oauth/token,并且 code 只能使用一次。

2.1.2.5、获取新的 token

这个是刷新令牌使用的,当我们的Token过期时,我们就可以使用这个 refresh_token 来申请一个新的 Token。

需要单独配置一个 UserDetailsService。

2.1.3、单点登录客户端 - 基于 @EnableOAuth2Sso 实现

SpringCloud 为我们提供了客户端的直接实现,我们只需添加一个注解和少量配置即可将我们的服务作为一个单点登录应用,使用的是最安全的授权码模式。登录是在我们自己搭建的验证码服务器上去进行登录的。

这个注解是添加在我们的微服务上,将其作为一个单点登录的客户端。

2.1.3.1、引入依赖
<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-security</artifactId>
</dependency>
2.1.3.2、配置文件添加配置
security:
  oauth2:
    client:
      #不多说了
      client-id: web
      client-secret: 654321
      # Token获取地址
      access-token-uri: http://localhost:8500/sso/oauth/token
      #验证页面地址
      user-authorization-uri: http://localhost:8500/sso/oauth/authorize
    resource:
      #Token信息获取和校验地址
      token-info-uri: http://localhost:8500/sso/oauth/check_token
2.1.3.3、启动类添加注解
@EnableOAuth2Sso
@SpringBootApplication
public class BookApplication {
  public static void main(String[] args) {
    SpringApplication.run(BookApplication.class, args);
  }
}
2.1.3.4、测试

访问对应的 API,就需要进行登录,然后可以通过 SecurityContextHolder 取得保存在验证服务器上的用户登录信息。

这里使用的不是之前的 UsernamePasswordAuthenticationToken,也不是 RememberMeAuthenticationToken,而是新的 OAuth2Authentication,它保存了验证服务器的一些信息,以及经过我们之前的登录流程之后,验证服务器发放给客户端的 Token信息,并通过 Token信息在验证服务器进行验证获取用户信息,最后保存到 Session 中,表示用户已验证,所以本质上还是要依赖浏览器存 Cookie 的。

@RequestMapping("/book/{bid}")
Book findBookById(@PathVariable("bid") int bid){
    //通过SecurityContextHolder将用户信息取出
    SecurityContext context = SecurityContextHolder.getContext();
    System.out.println(context.getAuthentication());
    return service.getBookById(bid);
}
2.1.3.5、存在的问题

因为 SESSION 不同步,所以每次切换不同的服务进行访问,都会重新去验证服务器去验证一次,就相当于登录验证的方式改成了去验证服务器去做验证。那么这样其实就不算是单点登录了,因为有重复的登录验证。

并且还是解决不了服务间调用的问题。

解决方法:

  • 做 SESSION 统一存储

  • 设置 context-path路径,每个服务单独设置,就不会覆盖了

2.1.4、资源服务器 - 基于 @EnableResourceServer 实现

@EnableOAuth2Sso 将我们的服务作为单点登录应用直接实现单点登录,如果是以第三方应用进行访问的话,这时就需要将我们的服务作为资源服务了,作为资源服务的话就不会再提供验证的过程,而是直接要求请求时携带 Token。而验证过程获取 token 我们这里就继续用 Postman 来完成,这才是常见的模式。

即我们只需要携带 Token 就能访问这些资源服务器了,客户端被独立了出来,用于携带 Token 去访问这些服务。

资源服务器完全没有必要将 Security 的信息保存在 Session 中了,因为现在只需要将 Token 告诉资源服务器,那么资源服务器就可以联系验证服务器,得到用户信息,就不需要使用之前的 Session 存储机制了,所以你会发现 HttpSession 中没有 SPRING_SECURITY_CONTEXT,现在 Security信息都是通过连接资源服务器获取。

// 这样拿不到
SecurityContext context = (SecurityContext)httpSession.getAttribute("SPRING_SECURITY_CONTEXT");
// 这样才可以拿到
SecurityContext context = SecurityContextHolder.getContext();

将我们的服务作为资源服务器,这样和我们上面的作为客户端是不同的,将服务作为客户端相当于只需要验证通过即可,但还是要保存 Session信息的,相当于只是将登录流程换到统一的验证服务器上进行罢了。

而将其作为资源服务器,那么就需要另外找客户端(可以是浏览器、小程序、App、第三方服务等)来访问,并且也是需要先进行验证然后再通过携带 Token 进行访问,这种模式是我们比较常见的模式。

2.1.4.1、修改配置文件
security:
  oauth2:
    client:
      client-id: web
      client-secret: 654321
    resource:
#资源服务器得验证你的 Token 是否有访问此资源的权限和用户信息,所以只需要一个验证地址
      token-info-uri: http://localhost:8500/sso/oauth/check_token
2.1.4.2、修改启动类
@EnableResourceServer
@SpringBootApplication
public class BookApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookApplication.class, args);
    }
}
2.1.4.3、每个微服务定义一个配置类

这样是对资源服务器进行深度自定义,我们可以为每个微服务编写一个配置类,比如我们现在希望用户授权了某个 Scope 才可以访问此服务。

@Configuration
public class ResourceConfiguration extends ResourceServerConfigurerAdapter { //继承此类进行高度自定义
  @Override
  public void configure(HttpSecurity http) throws Exception {  //这里也有HttpSecurity对象,方便我们配置SpringSecurity
    http
      .authorizeRequests()
      .anyRequest().access("#oauth2.hasScope('book')");//添加自定义规则
    //Token必须要有我们自定义scope授权才可以访问此资源
  }
}
2.1.4.4、使用一种方式获取 token

2.1.4.5、两种使用 token 进行登录验证的方式
  • 在 url 后面以 GET方式添加 access_token=token值

  • 在请求头中添加 Authorization,值为 Bearer Token值。添加后,请求头中会自动多出来一段信息。

2.1.4.5、存在的问题

因为 SESSION 不同步,所以每次切换不同的服务进行访问,都会重新去验证服务器去验证一次,就相当于登录验证的方式改成了去验证服务器去做验证。那么这样其实就不算是单点登录了,因为有重复的登录验证。

并且还是解决不了服务间调用的问题。所以仅靠 OAuth2 是无法解决的,最推荐的解决方案就是配合使用 JWT。

2.1.5、解决服务间调用未携带 token 的问题

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2.1.5.1、RestTemplate

替换 template 即可。

@Configuration
public class WebConfiguration {
  @Resource
  OAuth2ClientContext context;
​
  @LoadBalanced // 配置负载均衡
  @Bean
  public OAuth2RestTemplate restTemplate(){
    return new OAuth2RestTemplate(new ClientCredentialsResourceDetails(), context);
  }
}
@Service
public class BorrowServiceImpl implements BorrowService {
  @Resource
  BorrowMapper mapper;
  @Resource
  OAuth2RestTemplate template;
​
  @Override
  public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
    List<Borrow> borrow = mapper.getBorrowsByUid(uid);
    User user = template.getForObject("http://localhost:8101/user/"+uid, User.class);
    //获取每一本书的详细信息
    List<Book> bookList = borrow.stream()
      .map(b -> template.getForObject("http://localhost:8201/book/"+b.getBid(), Book.class))
      .collect(Collectors.toList());
    return new UserBorrowDetail(user, bookList);
  }
}
2.1.5.2、OpenFeign
feign:
  oauth2:
    # feign 开启 Oauth2 的支持,这样就会在请求头中携带Token了
    enabled: true
    #同时开启负载均衡支持
    load-balanced: true
@FeignClient("user-service")
public interface UserClient {
    @RequestMapping("/user/{uid}")
    User getUserById(@PathVariable("uid") int uid);
}

3、使用 JWT 存储 token

官网:JSON Web Tokens - jwt.io

3.1、JWT简介

3.1.1、什么是 JWT

传统的 Web应用中,使用 session 来存在用户的信息,每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中。随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大,由于 Session 是在内存中的,这就带来一些扩展性的问题,servlet 依赖于 web容器,即依赖 tomcat服务器。当然也可以在浏览器中存 Cookie,让它携带 Session信息去访问,但是也不一定所有的用户都允许你存 Cookie。

JSON Web Token ,是 token 的一种,是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。token 是被加密的。JWT 可以使用密钥(使用 HMAC算法)或使用 RSA 或 ECDSA 进行公钥/私钥对进行签名。

JWT 存放在客户端(前端),每次请求的请求头中,携带此 JWT 发送给服务器,服务器端负责接收和验证。服务器端可以不用存储 JWT,这样可以降低服务器的内存的开销。

并且 JWT 和语言无关,扩展起来非常方便,无论是 PC端还是移动端,都可以很容易的使用,不受 cookie 的限制。

session 和 JWT 的主要区别就是保存的位置,session 是保存在服务端的,而 JWT 是保存在客户端的。而且 JWT 就是一个固定格式的字符串。

3.1.2、为什么使用 JWT

之前使用 OAuth2,每个请求都需要携带 token 向资源服务器发起请求,然后由于资源服务器不知道此 token 对应的用户信息,所以就需要向验证服务器获得此 token 对应的用户认证信息。那么在大量请求下,会对验证服务器造成极大的压力。

而使用 JWT 之后,会在 token 中直接保存用户的信息,这样资源服务器就不需要去访问验证服务器了,可以自行完成 token 解析获取到用户认证信息。

3.1.3、JWT格式

JWT令牌字符串,由三部分组成:头部(Header)、载荷(Payload)、签名(Signature)。在传输的时候,会将 JWT 的 3部分分别进行 Base64编码后用 . 进行连接形成最终需要传输的字符串。

并且此 JWT 是被加密的。但是不要在 JWT 的 payload 或 header 中放置敏感信息,除非它们是经过加密的。因为不安全。

3.1.3.1、标头

包含一些元数据信息,比如 JWT签名所使用的加密算法,还有类型,这里统一都是JWT。header 一般的由两部分组成:token 的类型(JWT)和算法名称(比如:HMAC SHA256 或者 RSA 等等)。alg 和 typ。

3.1.3.2、载荷

包括用户名称、令牌发布时间、过期时间、JWT ID 等,当然我们也可以自定义添加字段,我们的用户信息一般都在这里存放。

payload 主要用来包含声明(claims),这个声明一般是关于实体(通常是用户)和其他数据的声明。

声明有三种类型:registered(注册声明)、public(公有)、private(私有)

具体的字段信息如下:

  • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。

  • iss:jwt签发者

  • sub:jwt 所面向的用户

  • aud:接收 jwt 的一方

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

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

  • iat:jwt 的签发时间

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

  • Public claims:可以随意定义

  • 自定义数据:在 token 中存放的 key-value值

  • Private claims:用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明

3.1.3.3、签名

首先需要指定一个密钥,该密钥仅仅保存在服务器中,保证不能让其他用户知道。然后使用 Header 中指定的算法对 Header 和 Payload 进行 base64加密之后会得到两个字符串,把两个字符串用 . 连接后形成新的字符串,再然后通过密钥和算法(比如 HS256算法)进行加密计算哈希,然后就得出一个签名哈希字符串,这个字符串结果就是签名。这个会用于之后验证内容是否被篡改。

3.1.4、base64

Base64 就是包括小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"一共64个字符的字符集,任何的符号都可以转换成这个字符集中的字符(如果结果不够 64 就用 = 来凑够字节数),这个转换过程就叫做 Base64编码,编码之后会生成只包含上述 64个字符的字符串。相反,如果需要原本的内容,我们也可以进行 Base64解码,回到原有的样子。 

public void test(){
  String str = "我是一个测试字符串";
    //把东西变成 Byte 后就可以进行 Base64编码了,返回结果可以是 String或Byte []
  String encodeStr = Base64.getEncoder().encodeToString(str.getBytes());
  System.out.println("Base64编码后的字符串:"+encodeStr);
​
  System.out.println("解码后的字符串:"+new String(Base64.getDecoder().decode(encodeStr)));
}

3.1.4、加密算法

Base64 不是加密算法,只是一种信息的编码方式。

加密算法分为对称加密和非对称加密。

3.1.4.1、对称加密(Symmetric Cryptography)

就像一把锁配了两把钥匙,这两把钥匙你和别人一人一把,然后你们传递数据,都会把数据用锁给锁上,就算传递的途中有人把数据窃取了,也没办法解密,因为钥匙只有你和对方有,没有钥匙无法进行解密。

但是这样有个问题,既然解密的关键在于钥匙本身,那么如果有人不仅窃取了数据,而且还把发送方的钥匙也拿到了,那你们这个数据就被窃取了。

对称加密的算法:DES、IDEA、RC2

3.1.4.2、非对称加密(Asymmetric Cryptography)

对于一把锁,它并不是直接生成一把钥匙,而是生成一个公钥和一个私钥,公钥和私钥是一一对应的,私钥只能由你保管,而公钥交给对方或是你要发送的任何人都行,现在你需要把数据传给对方,那么就需要使用私钥进行加密,但是,这个数据只能使用对应的公钥进行解密;相反,如果对方需要给你发送数据,那么就需要用公钥进行加密,而数据只能使用私钥进行解密,这样的话就算对方的公钥被窃取,那么别人发给你的数据也没办法解密出来,因为需要私钥才能解密,而只有你才有私钥。

因此,非对称加密的安全性会更高一些,包括 HTTPS 的隐私信息正是使用非对称加密来保障传输数据的安全(当然 HTTPS 并不是单纯地使用非对称加密完成的)。

非对称加密的算法:RSA、DAS、ECC。

3.1.4.3、不可逆加密(Asymmetric Cryptography)

常见的不可逆加密算法有 MD5, HMAC, SHA-1, SHA-224, SHA-256, SHA-384 和 SHA-512,

其中 SHA-224、SHA-256、SHA-384,和 SHA-512 我们可以统称为 SHA2 加密算法,SHA加密算法的安全性要比 MD5更高,而 SHA2加密算法比 SHA1的要高,

其中 SHA 后面的数字表示的是加密后的字符串长度,SHA1 默认会产生一个 160位的信息摘要。

经过不可逆加密算法得到的加密结果,是无法解密回去的,也就是说加密出来是什么就是什么了。本质上,不可逆加密算法就是一种哈希函数,用于对一段信息产生摘要,以防止被篡改。

实际上这种算法就常常被用作信息摘要计算,同样的数据通过同样的算法计算得到的结果肯定也一样,而如果数据被修改,那么计算的结果肯定就不一样了。

3.2.2、JWT工具类

3.2.2.1、配套工具类1
/**
 * 相当于 spring-mvc.xml 这个mvc自动配置文件
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    //添加一个拦截器 mvc:interceptors
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInteceptor())
                .addPathPatterns("/auth/**");
    }
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")//映射所有路径
                .allowedOrigins("*")//运行所有客户端访问,与下面的cookie是冲突的,是*则为false,是true则不能为*
                .allowCredentials(false)//不允许携带cookie
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")//支持的方法
                .allowedHeaders("*")//运行所有请求头字段
                .maxAge(3600);//允许客户端缓存“预检请求”中获取的信息,3600秒
    }
}
// 拦截认证资源,例如:拦截 /auth/** 下的资源
// 可以implements HandlerInterceptor,也可以 extends HandlerInterceptorAdapter
// 以前是通过 <mvc:interceptors> 让拦截器生效,现在是使用 config 进行配置生效
public class JwtInteceptor implements HandlerInterceptor {
    /**
     * 功能描述 前置拦截器
     * @param handler 方法对象
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /*
         * 从请求头中取出token
         * 先判断 token 是否为空,若为空则代表没有登录
         * 判断 token 是否合法、是否过期、是否被篡改
         * 取出 token 资源(如果需要)
         * 最后放行资源
         */
        // class org.springframework.web.method.HandlerMethod 表示是拦截器方法。
        // 如果这个方法是拦截器方法,那么就说明这个方法是需要被拦截的
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        // 从请求头中获取token
        String token = request.getHeader("token");
        // 判断token是否为空 空直接抛异常
        if (token == null) {
            throw new YyghException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
        }
        // 校验token
        JwtUtil.checkSign(token);
        // 取出token中的数据
        Map<String, Object> info = JwtUtil.getInfo(token);
        System.out.println("取出 token 中的数据");
        info.forEach((k, v) -> {
            System.out.println(k + " : " + v);
        });
        // 放行
        return true;
    }
}
public class JwtUtil {
    //过期时间5分钟
    private static final long EXPIRE_TIME = 5 * 60 * 1000;
    //jwt 签名密钥
    private static final String SECRET = "jwt_secret";
    /**
     * 生成签名,五分钟后过期
     *
     * @param userId
     * @param info,Map的value只能存放的值的类型为:Map, List, Boolean, Integer, Long, Double, String and Date
     * @return
     */
    public static String sign(String userId, Map<String, Object> info) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            return JWT.create()
                    // 将 user id 保存到 token 里面
                    .withAudience(userId)
                    // 存放自定义数据
                    .withClaim("info", info)
                    // 五分钟后token过期
                    .withExpiresAt(date)
                    // token 的密钥
                    .sign(algorithm);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据token获取userId
     *
     * @param token
     * @return
     */
    public static String getUserId(String token) {
        try {
            String userId = JWT.decode(token).getAudience().get(0);
            return userId;
        } catch (JWTDecodeException e) {
            return null;
        }
    }
    /**
     * 根据token获取自定义数据info
     *
     * @param token
     * @return
     */
    public static Map<String, Object> getInfo(String token) {
        try {
            return JWT.decode(token).getClaim("info").asMap();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
    /**
     * 校验token
     *
     * @param token
     * @return
     */
    public static boolean checkSign(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    // .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            throw new RuntimeException("token 无效,请重新获取");
        }
    }
}
3.2.2.2、工具类2
public class JwtHelper {
    //token过期时间
    private static long tokenExpiration = 24 * 60 * 60 * 1000;
    //token签名密钥
    private static String tokenSignKey = "123456";
    //根据参数生成 token
    public static String createToken(Long userId, String userName) {
        String token = Jwts.builder()
                .setSubject("INTERNETHOSPITAL-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("userId", userId)
                .claim("userName", userName)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }
    //根据 token得到 Id
    public static Long getUserId(String token) {
        if (StringUtils.isEmpty(token)) return null;
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        Integer userId = (Integer) claims.get("userId");
        return userId.longValue();
    }
    //根据 token得到 Username
    public static String getUserName(String token) {
        if (StringUtils.isEmpty(token)) return "";
        Jws<Claims> claimsJws
                = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return (String) claims.get("userName");
    }
    public static void main(String[] args) {
        String token = JwtHelper.createToken(1L, "tom");
        System.out.println(token);
        System.out.println(JwtHelper.getUserId(token));
        System.out.println(JwtHelper.getUserName(token));
    }
}
/**
 * 获取当前用户信息工具类
 */
public class AuthContextHolder {
    // 获取当前用户id
    public static Long getUserId(HttpServletRequest request) {
        // 从header获取token
        String token = request.getHeader("token");
        // jwt从token获取userid
        Long userId = JwtHelper.getUserId(token);
        return userId;
    }
    // 获取当前用户名称
    public static String getUserName(HttpServletRequest request) {
        // 从header获取token
        String token = request.getHeader("token");
        // jwt从token获取userid
        String userName = JwtHelper.getUserName(token);
        return userName;
    }
}

3.2、简单使用 JWT

3.2.1、实例1 - 对称加密

在 OAuth2 的情况下,从获取 token 变成获取 JWT。

3.2.1.1、通过验证服务器(即第三方应用)获取 JWT
// 写在随便一个 Configuration 里面即可
@Bean
//Token转换器,将其转换为 JWT
public JwtAccessTokenConverter tokenConverter(){
  JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  converter.setSigningKey("lbwnb");//这是对称密钥,资源服务器那边也要指定为这个
  return converter;
}
​
@Bean
//Token存储方式从在内存中存储改为 JWT存储
public TokenStore tokenStore(JwtAccessTokenConverter converter){
    return new JwtTokenStore(converter);  //传入刚刚定义好的转换器
}
// 写在 OAuth2 的 Configuration 里面
@Resource
TokenStore store;
@Resource
JwtAccessTokenConverter converter;
​
//这里对 AuthorizationServerTokenServices 进行一下配置
private AuthorizationServerTokenServices serverTokenServices(){
    DefaultTokenServices services = new DefaultTokenServices();
    services.setSupportRefreshToken(true);   //允许Token刷新
    services.setTokenStore(store);   //添加刚刚的TokenStore
    services.setTokenEnhancer(converter);   //添加Token增强,其实就是JwtAccessTokenConverter,增强是添加一些自定义的数据到JWT中
    return services;
}
​
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints
      // 设定为刚刚配置好的 AuthorizationServerTokenServices
      .tokenServices(serverTokenServices())
      .userDetailsService(service)
      .authenticationManager(manager);
}

3.2.1.2、微服务资源服务器拿到第三方的 token

资源服务器拿到 token 后直接进行校验,然后获取用户信息。就不需要去验证服务器去通过 token 验证再得到用户信息了。

security:
  oauth2:
    resource:
      jwt:
        key-value: lbwnb #注意这里要跟验证服务器的密钥一致,这样算出来的签名才会一致

还是老样子,两种请求方式,放在路径里面或者放在请求头里面。

在访问的路径后面直接拼接 token 即可。

token 放在请求头里面即可。

实例2

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
</dependency>
​
​
​
Long userId = 123L;
String token = JWT.create()
    .withHeader(headerClaims)
    .withSubject("auth token for pinlor authentication")
    .withIssuer("pinlor")
    .withAudience("pinlor")
    //签发时间
    .withIssuedAt(new Date())
    //uuid
    .withJWTId(UUID.fastUUID().toString())
    //过期时间
    .withExpiresAt(cal.getTime())
    .withClaim("org", "pinlor")
    .withClaim("contact", "pinlor")
    .withClaim("userId", userId)
    .sign(Algorithm.HMAC512("1234qwer"));

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/932654
推荐阅读
相关标签
  

闽ICP备14008679号