【网课平台】Day8.Spring Security实现统一认证授权(OAuth2.0与JWT)

有时候,不是因为你没有能力,也不是因为你缺少勇气,只是因为你付出的努力还太少,所以,成功便不会走向你。而你所需要做的,就是坚定你的梦想,你的目标,你的未来,然后以不达目的誓不罢休的那股劲,去付出你的努力,成功就会慢慢向你靠近。

导读:本篇文章讲解 【网课平台】Day8.Spring Security实现统一认证授权(OAuth2.0与JWT),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

一、认证授权模块

认证,即验证用户身份受否合法,告诉系统你是谁。常见形式如:用户名密码登录、微信扫码登录

在这里插入图片描述

授权,即认证通过后,系统判断用户对某资源是否有权访问。

在这里插入图片描述

1、统一认证

统一认证,即各种角色使用统一的认证入口,如下图中:

在这里插入图片描述

  • 项目由 统一认证服务 来受理用户的认证请求
  • 认证通过,则认证服务向用户颁发令牌,相当于通行证

在这里插入图片描述

2、单点登录

单点登录(Single Sign On),SSO,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

在这里插入图片描述

在本项目中,微服务包括:内容管理服务、媒资管理服务、学习中心服务、系统管理服务。可以理解为,登录一次,就能访问所有你拥有权限的系统。

3、第三方认证

借助第三方,如微信扫码登录

在这里插入图片描述
如此,就省去了用户注册成本。不然一个新网站,注册还要一大堆信息,没人愿意鸟你根本。

二、Spring Security认证

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。

1、认证授权入门案例

测试工程准备:

创建一个测试工程,取名auth:

  • 工程nacos配置:
server:
  servlet:
    context-path: /auth
  port: 63070
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xc1010_users?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql

  • 所连数据库
    在这里插入图片描述
  • 生成PO类和POMapper接口后,写测试Controller:
@Slf4j
@RestController
public class LoginController {

	  @Autowired
	  XcUserMapper userMapper;
	
	  @RequestMapping("/login-success")
	  public String loginSuccess(){
	
	      return "登录成功";
	  }
	
	
	  @RequestMapping("/user/{id}")
	  public XcUser getuser(@PathVariable("id") String id){
	    XcUser xcUser = userMapper.selectById(id);
	    return xcUser;
	  }
	
	  @RequestMapping("/r/r1")
	  public String r1(){
	    return "访问r1资源";
	  }
	
	  @RequestMapping("/r/r2")
	  public String r2(){
	    return "访问r2资源";
	  }
	


}

  • 此时访问http://localhost:63070/auth/r/r1,正常返回,无身份校验
    在这里插入图片描述

集成Spring security:

  • 在auth工程pom文件中引入Spring security依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

  • 重启auth,此时访问http://localhost:63070/auth/r/r1,进入/login页面,/login是spring security提供的
    在这里插入图片描述
  • 在config目录下添加WebSecurityConfig.class,注意这里对拦截机制的设置
package com.xuecheng.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @description 安全管理配置
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


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

    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中,p1即该用户拥有p1权限
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        //密码为明文方式
        return NoOpPasswordEncoder.getInstance();
        //return new BCryptPasswordEncoder();
    }
	
	//!!!!!!!!!!!!!!核心
    //配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
                .anyRequest().permitAll()//其它请求全部放行
                .and()
                .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
                http.logout().logoutUrl("/logout");//退出地址
    }



}

  • @PreAuthorize注解指定资源与权限
@RestController
public class LoginController {
    ....
    @RequestMapping("/r/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
    public String r1(){
    
      return "访问r1资源";
    }

此时登录无p1权限的用户,访问/r/r1,则访问失败:

在这里插入图片描述

到此,整个工作流程为:

在这里插入图片描述

2、Spring Security工作原理

Spring Security是做安全访问控制,对所有进入系统的请求进行拦截,并做校验,这可以通过Filter或者AOP实现,Spring Security靠Filter。

在这里插入图片描述

  • 初始化Spring Security时,创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类
  • FilterChainProxy是一个代理
  • SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理
  • 这些Filter不负责认证或者授权,而是交给认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理

Spring Security功能的实现就是一系列过滤器链相互配合。

在这里插入图片描述

  • SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口,也就是第一个和最后一个拦截器
  • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证,该表单必须提供对应的用户名和密码

Spring Security的执行流程如下

在这里插入图片描述

  • 用户提交用户名密码,被安全过滤器链中的UsernamePasswordAuthenticationFilter过滤器拿到,并封装为请求Authentication
  • 过滤器将Authentication提交至认证管理器(AuthenticationManager)
  • 认证管理器通过后面一系列实现类拿到数据进行认证
  • 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例
  • SecurityContextHolder安全上下文容器将上一步的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中

3、OAuth2.0

认识Oauth2.0协议

OAUTH协议是为用户资源的授权提供的一个标准,先看一个网站扫码登录的例子,时序图

在这里插入图片描述
以上:

  • 点击微信图标,获取微信扫码页面
    在这里插入图片描述

  • 此时,资源是用户信息,资源拥有者是用户自己。扫码后出现授权页面,点击确定即同意当前网站从微信拿你的信息
    在这里插入图片描述

  • 此时客户端是当前网站,服务端是微信。用户同意,微信下发授权码(对用户不可见)

  • 网站拿授权码向微信申请令牌(对用户不可见)

  • 微信下发令牌(用户不可见)

  • 网站拿着令牌去请求微信资源服务器,获取用户信息(用户不可见)

  • 资源服务器返回用户信息,网站收到即登陆成功(用户收到登录成功提示)

==整个Oauth2.0的示意图==

在这里插入图片描述
以上,角色包括:

  • 客户端:没资源,要求拿资源的一方
  • 资源拥有者:数据持有者,用户自己。图中A过程即客户端请求资源拥有者授权,B过程即权限授予
  • 授权服务器,即认证服务器,对资源拥有者进行认证,还会对客户端进行认证并颁发令牌。C即网站携带授权码请求认证。D即认证通过颁发令牌
  • 资源服务器:存有请求方想要的资源,E客户端携带令牌请求。F即校验令牌通过后提供资源

Oauth2.0的授权模式

Oauth2.0提供四种授权模式:

  • 授权码模式
  • 密码模式
  • 简化模式
  • 客户端模式
  • 授权码模式

该模式最终通过令牌去获取资源,而令牌的获取需要授权码,授权码的获取需要资源拥有者亲自授权

在这里插入图片描述
代码实现

  • 在config包下建AuthorizationServer.java,加@EnableAuthorizationServer 注解标识并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器
package com.xuecheng.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;

import javax.annotation.Resource;

/**
 * @description 授权服务器配置
 */
 @Configuration
 @EnableAuthorizationServer
 public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

	 @Resource(name="authorizationServerTokenServicesCustom")
	 private AuthorizationServerTokenServices authorizationServerTokenServices;
	
	 @Autowired
	 private AuthenticationManager authenticationManager;
	
	  //客户端详情服务
	  @Override
	  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	          
	        clients.inMemory()// 使用in-memory存储
	                .withClient("XcWebApp")// client_id
	                .secret("XcWebApp")//客户端密钥
	                //.secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
	                .resourceIds("xuecheng-plus")//资源列表
	                .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
	                .scopes("all")// 允许的授权范围
	                .autoApprove(false)//false跳转到授权页面
	                //客户端接收授权码的重定向地址
	                .redirectUris("http://www.51xuecheng.cn") ;
	  
	  }
	
	
	  //令牌端点的访问配置
	  @Override
	  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
	   endpoints
	           .authenticationManager(authenticationManager)//认证管理器
	           .tokenServices(authorizationServerTokenServices)//令牌管理服务
	           .allowedTokenEndpointRequestMethods(HttpMethod.POST);
	  }
	
	  //令牌端点的安全配置
	  @Override
	  public void configure(AuthorizationServerSecurityConfigurer security){
	   security
	           .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
	           .checkTokenAccess("permitAll()")                  //oauth/check_token公开
	           .allowFormAuthenticationForClients()				//表单认证(申请令牌)
	   ;
	  }
	


 }

ps:继承的AuthorizationServerConfigurerAdapter要求配置以下三项:

//源码:

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {}
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}


//AuthorizationServerEndpointsConfigurer:
//用来配置令牌(token)的访问端点和令牌服务(token services)

//AuthorizationServerSecurityConfigurer:
//用来配置令牌端点的安全约束.

ClientDetailsServiceConfigurer:不能允许所有客户端都可以接入认证,这里配置哪个客户端可以接入到认证服务。服务提供者给客户端一个client_id和客户端密钥

  • 在config下建令牌配置策略类TokenConfig.java
package com.xuecheng.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.util.Arrays;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    @Autowired
    TokenStore tokenStore;

    @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }

    //令牌管理服务
    @Bean(name="authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略
        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }


}

  • 修改WebSecurityConfig.class,配置认证管理Bean
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

  • 重启auth服务,get请求授权码
http://localhost:63070/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://localhost:8080


• client_id: 客户端准许接入的标识
• response_type: 授权码模式固定传code
• scope: 客户端权限
• redirect_uri: 跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上刚拿到的授权码
  • 授权(Client_id)“XcWebApp”访问自己的受保护资源?
    在这里插入图片描述
  • 成功拿到授权码
    在这里插入图片描述
  • POST获取token
    在这里插入图片描述

参数列表:

•	client_id: 客户端准入标识
•	client_secret: 客户端秘钥
•	grant_type: 授权类型,填写authorization_code,表示授权码模式
•	code: 授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请
•	redirect_uri: 申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致

  • 密码模式

在这里插入图片描述

和授权码模式不一样,密码模式不需要用户亲自授权,只要密码账户校验通过就发token

  • 资源拥有者提供账号和密码
  • 客户端向认证服务申请令牌,请求中携带账号和密码
  • 认证服务校验账户和密码,正确则颁发令牌

在这里插入图片描述

传参: 
•	client_id: 客户端准入标识
•	client_secret: 客户端秘钥
•	grant_type: 授权类型,填写password表示密码模式
•	username: 资源拥有者用户名
•	password: 资源拥有者密码

以上:

  • 授权码模式适合用于客户端和认证服务不在同一个系统,如微信扫码登录
  • 密码模式用作前端请求微服务的认证方式

4、JWT

JWT与普通令牌的对比

当前,资源服务校验令牌得远程请求认证服务,性能较低:

在这里插入图片描述

如果资源服务可以自行校验令牌,则上面问题解决。而JWT就是来实现这个效果的。

在这里插入图片描述

JWT,即Json Web Token,是一种使用JSON格式传递数据的网络令牌技术

  • JWT基于json,方便解析
  • 可以在令牌中自定义需要的内容
  • 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高
  • 资源服务使用JWT可不依赖认证服务即可完成授权
  • 缺点是JWT较长,存储占用空间大

JWT示例

JWT由三部分组成,每部分之间用点隔开,格式Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQyNTQ2NzIsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijg4OTEyYjJkLTVkMDUtNGMxNC1iYmMzLWZkZTk5NzdmZWJjNiIsImNsaWVudF9pZCI6ImMxIn0
.wkDBL7roLrvdBG2oGnXeoXq-zZRgE9IVV2nxd-ez_oA
  • Header:头部包括令牌的类型和使用的哈希算法
    在这里插入图片描述

  • Payload:负载,也是一个json对象,这里存信息字段,如iss(签发者),exp(过期时间戳), sub(面向的用户)或其他自定义字段

{
    "sub": "1234567890",
    "name": "456",
    "admin": true,
    "exp": 1682465665,
    "client_id": "XcWebApp"
  }

  • Signature:签名,校验JWT是否被篡改。这里将前两个json用base64编码,然后用.点连接成字符串,最后使用Header中声明的签名算法进行签名
HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

//secret即签名所使用的密钥

JWT防篡改的原理

  • 签名算法需要使用密钥进行签名
  • 密钥不对外公开,并且签名是不可逆的

在这里插入图片描述
资源服务使用密钥得到的结果和传过来的JWT不一样时,即被篡改。

认证服务和资源服务使用相同的密钥,这叫对称加密,对称加密效率高,如果一旦密钥泄露可以伪造jwt令牌

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

代码实现

在认证服务中配置jwt令牌的配置类:


package com.xuecheng.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.util.Arrays;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {
	//密钥
    private String SIGNING_KEY = "mq123";

    @Autowired
    TokenStore tokenStore;

	  //@Bean
	  //public TokenStore tokenStore() {
          //使用内存存储令牌(普通令牌)
         //return new InMemoryTokenStore();
      //}

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

    //令牌管理服务
    @Bean(name="authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }
}

密码模式申请令牌:
在这里插入图片描述

access_token: 生成的jwt令牌,用于访问资源使用。
token_type: bearer是在RFC6750中定义的一种token类型,在携带jwt访问资源时需要在head中加入bearer jwt令牌内容
refresh_token: 当jwt令牌快过期时使用刷新令牌可以再次生成jwt令牌
expires_in: 过期时间(秒)
scope: 令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权
jti: 令牌的唯一标识

调用check_token接口来校验令牌:

在这里插入图片描述

资源服务校验令牌

有了JWT令牌,接下来要携带JWT访问资源服务。本项目中,认证服务即auth,资源即其他微服务,如内容管理服务。内容管理服务要对JWT进行校验,合法则允许访问:

在这里插入图片描述

资源服务集成JWT

  • 在内容管理服务的content-api工程中添加依赖
<!--认证相关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

  • 写TokenConfig,这里配置的密钥要和认证服务中的密钥一致(对称加密)

package com.xuecheng.content.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    //jwt签名密钥,与认证服务保持一致
    private String SIGNING_KEY = "mq123";

    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

   /* @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }*/
}

  • 写资源服务配置,注意资源服务标识要和认证服务中resourcesIds(“xxx”)一致

package com.xuecheng.content.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {


    //资源服务标识
    public static final String RESOURCE_ID = "xuecheng-plus";

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll() //其余的全部放行
        ;
    }

}

  • 重启资源服务(例子中的内容管理服务),此时访问/course开头的资源,需要携带token,否则unauthorized
{
  "error": "unauthorized",
  "error_description": "Full authentication is required to access this resource"
}

获取用户身份

认证服务生成jwt令牌将用户身份信息写入令牌,当客户端携带jwt访问资源服务,资源服务验签通过后将前两部分的内容还原即可取出用户的身份信息,并将用户身份信息放在了SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份

@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
    //取出当前用户身份
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    System.out.println(principal);

    return courseBaseInfoService.getCourseBaseInfo(courseId);
}

5、网关认证

所有访问微服务的请求都要经过网关,所以不用在每个微服务里加校验,而是在网关进行用户身份的认证,这样就可以将很多非法的请求拦截到微服务以外 ===> 网关认证

在这里插入图片描述
到此,网关的作用:

  • 路由转发
  • 白名单维护:对不用认证校验JWT的URL全部放行
  • 校验JWT合法性:除了白名单中的URL,校验其余的JWT,不合法则拒绝继续访问

注意:网关负责认证,但不负责授权。不同的用户有权访问哪些资源或接口,各个微服务自己最清楚(就是代码中业务校验里的身份校验)

实现网关认证:

  • 在网关集成Spring-security,添加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>

  • gateway.config下写TokenConfig类
package com.xuecheng.gateway.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
 
/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {
 
    String SIGNING_KEY = "mq123";
 
 
//    @Bean
//    public TokenStore tokenStore() {
//        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
//    }
 
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
 
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
 
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
 
 
}
  • gateway.config下写安全配置类
package com.xuecheng.gateway.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
 
/**
 * @description 安全配置类
 */
 @EnableWebFluxSecurity
 @Configuration
 public class SecurityConfig {
 
 
  //安全拦截配置
  @Bean
  public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
 
   return http.authorizeExchange()
           .pathMatchers("/**").permitAll()
           .anyExchange().authenticated()
           .and().csrf().disable().build();
  }
 
 
 }
  • gateway.config下写错误响应的包装结果类
package com.xuecheng.gateway.config;
 
import java.io.Serializable;
 
/**
 * 错误响应参数包装
 */
public class RestErrorResponse implements Serializable {
 
    private String errMessage;
 
    public RestErrorResponse(String errMessage){
        this.errMessage= errMessage;
    }
 
    public String getErrMessage() {
        return errMessage;
    }
 
    public void setErrMessage(String errMessage) {
        this.errMessage = errMessage;
    }
}
  • 配置网关认证过滤器
package com.xuecheng.gateway.config;
 
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
 
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
 
/**
 * @description 网关认证过虑器
 */
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
 
 
    //白名单
    private static List<String> whitelist = null;
 
    static {
        //加载白名单
        try (
                InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
        ) {
            Properties properties = new Properties();
            properties.load(resourceAsStream);
            Set<String> strings = properties.stringPropertyNames();
            whitelist= new ArrayList<>(strings);
 
        } catch (Exception e) {
            log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
            e.printStackTrace();
        }
 
 
    }
 
    @Autowired
    private TokenStore tokenStore;
 
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestUrl = exchange.getRequest().getPath().value();
        AntPathMatcher pathMatcher = new AntPathMatcher();
        //白名单放行
        for (String url : whitelist) {
            if (pathMatcher.match(url, requestUrl)) {
                return chain.filter(exchange);
            }
        }
 
        //检查token是否存在
        String token = getToken(exchange);
        if (StringUtils.isBlank(token)) {
            return buildReturnMono("没有认证",exchange);
        }
        //判断是否是有效的token
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = tokenStore.readAccessToken(token);
 
            boolean expired = oAuth2AccessToken.isExpired();
            if (expired) {
                return buildReturnMono("认证令牌已过期",exchange);
            }
            return chain.filter(exchange);
        } catch (InvalidTokenException e) {
            log.info("认证令牌无效: {}", token);
            return buildReturnMono("认证令牌无效",exchange);
        }
 
    }
 
    /**
     * 获取token
     */
    private String getToken(ServerWebExchange exchange) {
        String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(tokenStr)) {
            return null;
        }
        String token = tokenStr.split(" ")[1];
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return token;
    }
 
 
 
 
    private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        String jsonString = JSON.toJSONString(new RestErrorResponse(error));
        byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
 
 
    @Override
    public int getOrder() {
        return 0;
    }
}

  • 配置白名单文件
/**=临时全部放行
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口

也可在gateway的nacos中配置:

在这里插入图片描述

  • 重启网关,此时资源服务中不用再单独加JWT校验

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/146053.html

(1)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!