SpringCould整合oauth2

一、创建认证微服务端

创建mengxuegu-blog-oauth2微服务工程,做认证使用

认证服务器表结构默认如下

 最主要表就是这个oauth_client_details

 生成 client_sercet密码

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAuthApplication {
 @Test
 public void testPwd() {
 System.out.println(new BCryptPasswordEncoder().encode("123456"));
 }
}

主要添加依赖

<!-- Spring Security、OAuth2 和JWT等 -->
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-oauth2</artifactId>
 </dependency>

因为登录要调用其他微服务模块的接口,所以先封装接口

feign封装的接口

1、 findUserByUsername (通过用户名查询用户信息)

2、findMenuByUserId(通过用户的ID查询用户所有权限)

@ApiImplicitParam(name="username", value="用户名", required=true)
 @ApiOperation("Feign接口-通过用户名查询用户信息")
 @GetMapping("/api/feign/user/{username}") 
 SysUser findUserByUsername(@PathVariable("username") String username);
 @ApiImplicitParam(name="username", value="用户ID", required=true)
 @ApiOperation("Feign接口-通过用户id查询拥有权限")
 @GetMapping("/api/feign/menu/{userId}")
 List<SysMenu> findMenuByUserId(@PathVariable("userId") String userId);

(1)、实现 UserDetailsService

逻辑 1. 因为 UserDetailsService 接口中有一个 UserDetails loadUserByUsername(String username) 抽象方法, 它的返回值 UserDetails 接口,我们要创建一个 JwtUser 类实现这个接口。

注意:isAccountNonExpired 声明了 boolean 类型,但是在构造器是 Integer 类型接收, 原因是 数据库 sys_user 表中存储的是整型,所以我们然后转 boolean,即 : this.isAccountNonExpired = isAccountNonExpired == 1 ? true: false;

@JSONField(serialize = false) // 忽略转json ,因为 后面我们要将这个类对象转成json。

package com.jhj.blog.oauth2.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.alibaba.fastjson.annotation.JSONField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;

/**
 * @program: jhj-blog
 * @ClassName JwtUser
 * @description:
* @create: 2022-01-04 20:45
 * @Version 1.0
 **/

@Data
public class JwtUser implements UserDetails {

    @ApiModelProperty(value = "用户ID")
    private String uid;
    @ApiModelProperty(value = "用户名")
    private String username;
    @JSONField(serialize = false) // 忽略转json
    @ApiModelProperty(value = "密码,加密存储, admin/1234")
    private String password;
    @ApiModelProperty(value = "昵称")
    private String nickName;
    @ApiModelProperty(value = "头像url")
    private String imageUrl;
    @ApiModelProperty(value = "注册手机号")
    private String mobile;
    @ApiModelProperty(value = "注册邮箱")
    private String email;
    // 1 true 0 false
    @JSONField(serialize = false) // 忽略转json
    @ApiModelProperty(value = "帐户是否过期(1 未过期,0已过期)")
    private boolean isAccountNonExpired; // 不要写小写 boolean
    @JSONField(serialize = false) // 忽略转json
    @ApiModelProperty(value = "帐户是否被锁定(1 未过期,0已过期)")
    private boolean isAccountNonLocked;
    @JSONField(serialize = false) // 忽略转json
    @ApiModelProperty(value = "密码是否过期(1 未过期,0已过期)")
    private boolean isCredentialsNonExpired;
    @JSONField(serialize = false) // 忽略转json
    @ApiModelProperty(value = "帐户是否可用(1 可用,0 删除用户)")
    private boolean isEnabled;
    /**
     * 封装用户拥有的菜单权限标识
     */
    @JSONField(serialize = false) // 忽略转json
    private List<GrantedAuthority> authorities;
    // isAccountNonExpired 是 Integer 类型接收,然后转 boolean
    public JwtUser(String uid, String username, String password,
                   String nickName, String imageUrl, String mobile, String email,
                   Integer isAccountNonExpired, Integer isAccountNonLocked,
                   Integer isCredentialsNonExpired, Integer isEnabled,
                   List<GrantedAuthority> authorities) {
        this.uid = uid;
        this.username = username;
        this.password = password;
        this.nickName = nickName;
        this.imageUrl = imageUrl;
        this.mobile = mobile;
        this.email = email;
        this.isAccountNonExpired = isAccountNonExpired == 1 ? true: false;
        this.isAccountNonLocked = isAccountNonLocked == 1 ? true: false;
        this.isCredentialsNonExpired = isCredentialsNonExpired == 1 ? true: false;
        this.isEnabled = isEnabled == 1 ? true: false;
        this.authorities = authorities;
    }

    }

(2)、创建UserDetailsServiceImpl 实现 UserDetailsService接口。

调用feign接口,查看用户信息并把用户信息封装到UserDetails 中

package com.jhj.blog.oauth2.service;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.jhj.blog.entities.SysMenu;
import com.jhj.blog.entities.SysUser;
import org.apache.commons.lang.StringUtils;

import com.jhj.blog.feign.IFeignSystemController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @program: jhj-blog
 * @ClassName UserDetailsServiceImpl
 * @description:
 * @create: 2022-01-04 20:44
 * @Version 1.0
 **/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {


    @Autowired // 检查启动类注解 @EnableFeignClients
    private IFeignSystemController feignSystemController;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 判断用户名是否为空
        if(StringUtils.isEmpty(username)) {
            throw new BadCredentialsException("用户名不能为空");
        }
        SysUser sysUser = feignSystemController.findByUsername(username);
        if(sysUser == null) {
            throw new BadCredentialsException("用户名或密码错误");
        }
        // 3. 通过用户id去查询数据库的拥有的权限信息
        List<SysMenu> menuList =
                feignSystemController.findMenuByUserId(sysUser.getId());
        // 4. 封装权限信息(权限标识符code)
        List<GrantedAuthority> authorities = null;
        if(CollectionUtils.isNotEmpty(menuList)) {
            authorities = new ArrayList<>();
            for(SysMenu menu: menuList) {
                // 权限标识
                String code = menu.getCode();
//                将权限标识封装起来比如 article:delete 文章删除权限
                authorities.add(new SimpleGrantedAuthority(code));
            }
        }
        // 5. 构建UserDetails接口的实现类JwtUser对象
        JwtUser jwtUser = new JwtUser(
                sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(),
                sysUser.getNickName(), sysUser.getImageUrl(), sysUser.getMobile(),
                sysUser.getEmail(),
                sysUser.getIsAccountNonExpired(), sysUser.getIsAccountNonLocked(),
                sysUser.getIsCredentialsNonExpired(), sysUser.getIsEnabled(),
                authorities );
        return jwtUser;

    }
}

(3)、添加密码配置类到Spring容器中

@Configuration //标注配置类
public class PasswordEncoderConfig {

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

(4)、生成私钥,并配置 Jwt 管理令牌

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之 间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用密码(使用HMAC算 法)或使用RSA或ECDSA的公钥/私钥对进行签名 ,防止被篡改。

JWT令牌生成采用非对称加密算法.

别名为 oauth2,秘钥算法为 RSA,秘钥口令为 oauth2,秘钥库(文件)名称为 oauth2.jks,秘钥库(文 件)口令为 oauth2。输入命令回车后,后面还问题需要回答,最后输入 y 表示确定 :

keytool -genkeypair -alias oauth2 -keyalg RSA -keypass oauth2 -keystore oauth2.jks -storepass oauth2

将生成的 oauth2.jks 文件 拷贝到认证微服务服务器 mengxuegu-blog-oauth2 的 resources 文件夹下:

使用JWT管理令牌


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
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;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

/**
 * @program: jhj-blog
 * @ClassName JwtTokenStoreConfig
 * @description:
 * @create: 2022-01-05 10:08
 * @Version 1.0
 * 第三步
 * JWT管理信息类配置
 *
 **/
@Configuration
public class JwtTokenStoreConfig {
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 采用非对称加密jwt
        // 第1个参数就是密钥证书文件,第2个参数 密钥库口令, 私钥进行签名
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
                new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());
        converter.setKeyPair(factory.getKeyPair("oauth2"));
        return converter;
    }
    @Bean
    public TokenStore tokenStore() {
        // Jwt管理令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

}

  (5)、扩展认证的响应数据(将用户信息userinfo响应给前端)

@Component // 不要少了
public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
// 扩展令牌内容
        JwtUser user = (JwtUser) oAuth2Authentication.getPrincipal();
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("userInfo", JSON.toJSON(user));

        //设置附加信息
        ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);

        return oAuth2AccessToken;
    }
}

(6)、加载中文响应信息

     全局搜索messages_zh_CN,找到springscurity的jar包解压,拿到messages_zh_CN.properties文件,放到resource目录下面,因为底层会加上properties,所以不加上后缀。

@Configuration
public class ReloadMessageConfig {
    @Bean // 加载中文的认证提示信息
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new
                ReloadableResourceBundleMessageSource();
        //.properties 不要加到后面
// messageSource.setBasename("classpath:org/springframework/security/messages_zh_CN");
        messageSource.setBasename("classpath:messages_zh_CN");//不要.properties
        return messageSource;
    }
}

(7)、创建认证服务器配置类

1、配置数据源,2、配置密码模式,3配置扩展响应数据等

@Configuration
@EnableAuthorizationServer // 开启了认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired // 1. 数据源
    private DataSource dataSource;

    @Bean // 1. 客户端使用 jdbc 管理
    public ClientDetailsService jdbcClientDetailsService() {

        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 1. 配置被允许访问此认证服务器的客户端信息: 数据库方式
     * 如:门户客户端,后台客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // jdbc 管理客户端
        clients.withClientDetails(jdbcClientDetailsService());
    }

    // 2. 在 SpringSecurityConfig 中添加到容器了, 密码模式需要
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired // token管理方式,引用 JwtTokenStoreConfig 配置的
    private TokenStore tokenStore;
    @Autowired // jwt 转换器
    private JwtAccessTokenConverter jwtAccessTokenConverter;


    /**
     * 注入扩展器
     */
    @Autowired
    private TokenEnhancer jwtTokenEnhancer;

    /**
     * 关于认证服务器端点配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
        // 刷新令牌时需要使用
        endpoints.userDetailsService(userDetailsService);
        // 令牌的管理方式
        endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);

        // 添加扩展器 +++++++ ++++++++++++++++++++++++++++
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancerList = new ArrayList<>();
        enhancerList.add(jwtTokenEnhancer);
        enhancerList.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancerList);
        endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // /oauth/check_token 解析令牌,默认情况 下拒绝访问
        security.checkTokenAccess("permitAll()");
    }

}

1、密码模式响应用户信息

localhost:7001/auth/oauth/token

密码模式响应用户信息 

 通过刷新令牌获取用户信息

(8)、封装接口放回restful风格接口

自定义返回值,遵循其他接口一样的restful风格

1、创建service层,

通过 loadBalancerClient 调用调用认证中心的接口,拼接成localhost:7001/auth/oauth/token,前端传递refreshToken ,通过刷新令牌拿到用户信息

service层如下‘


import com.arronlong.httpclientutil.HttpClientUtil;
import com.arronlong.httpclientutil.common.HttpConfig;
import com.arronlong.httpclientutil.common.HttpHeader;
import com.arronlong.httpclientutil.exception.HttpProcessException;

import com.jhj.blog.utils.base.Result;
import com.jhj.blog.utils.enums.ResultEnum;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;

import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.util.HashMap;

@Service
public class AuthService {

    @Autowired
    LoadBalancerClient loadBalancerClient;//开启负载均衡

    public Result refreshToken(String header, String refreshToken) throws HttpProcessException {
        ServiceInstance serviceInstance = loadBalancerClient.choose("auth-server");
        if (serviceInstance == null) {
            return Result.error("未找到有效认证服务器");
        }
        // 请求刷新令牌 url,拿到分布式微服务的基础路径
        System.out.println("serviceInstance.getUri():"+serviceInstance.getUri().toString());
        String refreshTokenUrl = serviceInstance.getUri().toString() + "/auth/oauth/token";

        // 封装刷新令牌请求参数
        HashMap<String , Object> map = new HashMap<>(2);
        map.put("grant_type", "refresh_token");
        map.put("refresh_token",refreshToken);


        // 构建配置请求参数(网址、请求参数、编码、client)
        Header[] headers = HttpHeader.custom() // 自定义请求
                .contentType(HttpHeader.Headers.APP_FORM_URLENCODED) // 数据类型
                .authorization(header) // 认证请求头
                .build();
        HttpConfig config = HttpConfig.custom().headers(headers)
                .url(refreshTokenUrl)
                .map(map);
        // 发送请求, 响应令牌
        String token = HttpClientUtil.post(config);
        JSONObject jsonToken = JSON.parseObject(token);
        if(StringUtils.isNotEmpty(jsonToken.getString("error")) ) {
            return Result.build(ResultEnum.TOKEN_PAST);
        }
        // 响应新令牌对象
        return Result.ok(jsonToken);
    }

}

2、controller层如下


import com.google.common.base.Preconditions;
import com.jhj.blog.utils.base.Result;
import com.jhj.blog.utils.tools.RequestUtil;
import com.jhj.blog.web.service.AuthService;
import com.sun.net.httpserver.HttpHandler;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class AuthController {

    @Autowired
    private AuthService authService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private ClientDetailsService clientDetailsService;
    private static final String HEADER_TYPE = "Basic ";

 
    @GetMapping("/user/refreshToken")//相当于问号传参的那样HttpServletRequest
    public Result refreshToken(HttpServletRequest request) {
        try {
            // 获取请求中的刷新令牌
            String refreshToken = (String) request.getParameter("refreshToken");
            System.out.println(refreshToken);
            //如果请求头为空,则抛出异常
            Preconditions.checkArgument(StringUtils.isNotEmpty(refreshToken), "刷新令牌不能为空");
//           //如果请求头不为空,则开始解析请求头
            String header = request.getHeader(HttpHeaders.AUTHORIZATION);
            //判断请求头中的信息
            if (header == null || !header.startsWith(HEADER_TYPE)) {
                throw new UnapprovedClientAuthenticationException("请求头中无client信息");

            }
            //通过自定义的base64解码,那取到授权表的用户名和密码
            String[] tokens = RequestUtil.extractAndDecodeHeader(header);
            assert tokens.length == 2;
            /**
             *  assert(断言的用法)
             *             if(假设成立)
             *             {
             *                 程序正常运行;
             *             }
             *             else
             *             {
             *                 报错&&终止程序!(避免由程序运行引起更大的错误)
             *             }
             */
            String clientId = tokens[0];
            String clientSecret = tokens[1];
//            springCloud自定义的方法用于判断,通过ID来查寻一条记录
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
            if (clientDetails == null) {
                throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在:"
                        + clientId);
            }
            //拿到这一条中存储的密码
            String clientSecret1 = clientDetails.getClientSecret();
            //通过加密器match方法自定义效验,返回的是布尔值
            if (!passwordEncoder.matches(clientSecret, clientSecret1)) {
                throw new UnapprovedClientAuthenticationException("无效clientSecret");
            }
            /**
             *
             *
             * 前面终于效验完了请求头和刷新码refresh_token
             * 下面调用service封装的调用方法,放回reful风格验证信息
             */

            // 获取刷新令牌
            return authService.refreshToken(header, refreshToken);

        } catch (Exception e) {
            return Result.error("令牌刷新失败" + e.getMessage());
        }

    }
}

3、进行关闭 csrf 攻击,不然调用不到接口

 /**
     * 在安全配置类 com.mengxuegu.blog.oauth2.config.SpringSecurityConfig 覆盖#configure(HttpSecurity)
     * 进行关闭 csrf 攻击,不然调用不到接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
    }

4、调用接口

http://127.0.0.1:7001/auth/user/refreshToken

二、Redis管理JWT令牌-登录与退出

Redis 存储有效令牌:

1、生成Jwt 访问令牌的时候,将 Jwt Token 存入redis中

2、扩展Jwt的验证功能,验证redis中是否存在数据,如果存在则token有效,否则无效

3、退出系统时将Redis中的数据删除。

1、Redis 启动器

<!--redis-->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置 Redis 连接信息

# redis 配置
spring:
 redis:
 host: localhost # Redis服务器地址
 port: 6379 # Redis服务器连接端口
 password: # Redis服务器连接密码(默认为空),redis 不需要用户名的

3、在之前配置的JwtTokenStoreConfig下面配置tokenStore,方便在redis中存储和移除token值


    @Bean
    public TokenStore tokenStore() {
        // Jwt管理令牌,
        //将jti作为key值,存储token信息
        return new JwtTokenStore(jwtAccessTokenConverter()) {
            @Override
            public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {

                //判断map中是否含有jti
                if (token.getAdditionalInformation().containsKey("jti")) {
                    //token值
                    String value = token.getValue();
                    //超时时间
                    int expiresIn = token.getExpiresIn();
                    String jti = token.getAdditionalInformation().get("jti").toString();
//                    redis设置key value ,超时时间,时间单位秒
                    redisTemplate.opsForValue().set(jti, value, expiresIn, TimeUnit.SECONDS);
                }
                super.storeAccessToken(token, authentication);
            }

            @Override
            public void removeAccessToken(OAuth2AccessToken token) {
                if (token.getAdditionalInformation().containsKey("jti")) {
                    // 通过 Jwt 的唯一标识 jti 为 Key 删除 redis 中数据
                    String jti = token.getAdditionalInformation().get("jti").toString();
                    redisTemplate.delete(jti);
                }
                super.removeAccessToken(token);
            }
        };
    }

1、登录功能 /auth/login

在 com.mengxuegu.blog.oauth2.config.SpringSecurityConfig 配置表单登录方式: http.formLogin()

@Override
protected void configure(HttpSecurity http) throws Exception {
 // 关闭csrf攻击
 http.formLogin() // ++
 .and()
 .csrf().disable();
}

(1)、成功处理器获取 token 值响应

创建 com.mengxuegu.blog.oauth2.CustomAuthenticationSuccessHandler 注意不要放到与 SpringSecurityConfig 同级包,不然功能都会失效,要放到父包上。

@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private ClientDetailsService clientDetailsService;

    private static final String HEADER_TYPE = "Basic ";


    @Resource
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {

        /**
         * 开始效验请求头
         */
        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        //判断请求头中的信息
        if (header == null || !header.startsWith(HEADER_TYPE)) {
            throw new UnapprovedClientAuthenticationException("请求头中无client信息");

        }
        /**
         * 开始效验请求的信息,并与数据库中比对
         */
        //通过自定义的base64解码,那取到授权表的用户名和密码
        String[] tokens = RequestUtil.extractAndDecodeHeader(header);
        assert tokens.length == 2;

        String clientId = tokens[0];
        String clientSecret = tokens[1];
//            springCloud自定义的方法用于判断,通过ID来查寻一条记录
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在:"
                    + clientId);
        }
        //拿到这一条中存储的密码
        String clientSecret1 = clientDetails.getClientSecret();
        //通过加密器match方法自定义效验,返回的是布尔值
        if (!passwordEncoder.matches(clientSecret, clientSecret1)) {
            throw new UnapprovedClientAuthenticationException("无效clientSecret");
        }
        /**
         * 结束效验信息
         */
        Result result = null;
        try {

            // 构建 tokenRequest 和 oAuth2Request 组合成 oAuth2Authentication 去获取 accessToken
            TokenRequest tokenRequest =
                    new TokenRequest(MapUtils.EMPTY_MAP, clientId,
                            clientDetails.getScope(), "custom");
            OAuth2Request oAuth2Request =
                    tokenRequest.createOAuth2Request(clientDetails);
            OAuth2Authentication oAuth2Authentication =
                    new OAuth2Authentication(oAuth2Request, authentication);
            // 获取 accessToken
            OAuth2AccessToken token =
                    authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
            result = Result.ok(token);
        } catch (Exception e) {

            // 认证失败
            result = Result.build(ResultEnum.AUTH_FAIL.getCode(), e.getMessage());
        }
       // 响应结果
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(result));


    }
}

(2)、登录失败处理器

@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler  implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// 响应错误信息:json格式
        response.setContentType("application/json;charset=UTF-8");
        String result = objectMapper.writeValueAsString(Result.error(e.getMessage()));
        response.getWriter().write( result );

    }
}

(3)、注入失败与成功处理器

@Autowired
 private AuthenticationSuccessHandler authenticationSuccessHandler;
 @Autowired
 private AuthenticationFailureHandler authenticationFailureHandler;
 @Override
 protected void configure(HttpSecurity http) throws Exception {
 http.formLogin()
 // 成功处理器
 .successHandler(authenticationSuccessHandler)
 .failureHandler(authenticationFailureHandler)
 .and().csrf().disable();
 }

2、测试登录

访问 post 请求 http://localhost:7001/auth/login 登录,添加 Basic 请求头,和用户名密码. 登录后,查看 redis 中是否有数据。

3、退出登录处理器

package com.jhj.blog.oauth2;


import com.jhj.blog.utils.base.Result;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @program: jhj-blog
 * @ClassName CustomLogoutSuccessHandler
 * @description:
 * @author:蒋皓洁
 * @create: 2022-01-12 19:31
 * @Version 1.0
 **/
@Component("customLogoutSuccessHandler")
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    TokenStore tokenStore;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
// 获取 access_token
        String accessToken = request.getParameter("accessToken");
        if (StringUtils.isNotBlank(accessToken)) {
            // 转换token对象
            OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
            if (oAuth2AccessToken != null) {
                // 删除redis的访问令牌
                tokenStore.removeAccessToken(oAuth2AccessToken);
            }
        }
        // 退出成功, 响应结果
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write(Result.ok().toJsonString());
    }
}

 注入退出处理器

@Autowired // 1. 退出成功处理器 +++++++
 private LogoutSuccessHandler logoutSuccessHandler;

 protected void configure(HttpSecurity http) throws Exception {
     // 关闭csrf攻击
     http.formLogin()
 // 成功处理器
            .successHandler(authenticationSuccessHandler)
             .failureHandler(authenticationFailureHandler)
     .and()
         .logout() // 1. 退出成功处理器 +++++++
             .logoutSuccessHandler(logoutSuccessHandler)
     .and()
         .csrf().disable();
 }

三、资源服务器安全配置

1、创建公钥

根据密钥证书获取公钥 在请求资源服务器(文章、问答、系统微服务)接口时,都必须要求在请求头带上jwt令牌来访问服务接口,而认 证服务器生成的jwt令牌是通过非对称私钥进行加密了,资源服务收到请求后,要解析出jwt令牌就需要公钥进行解 析出来。所以下面要通过 密钥证书获取公钥,放到资源服务器中,这样资源服务器可以直接解析出有效信息。

1、安装 OpenSSL

2、配置 OpenSSL 的环境变量,即你所安装的目录\bin,如: D:\workInstall\OpenSSL-Win64\bin

1、重新打开 CMD 命令行窗口, 进入 oauth2.jks 文件所在目录执行如下命令

keytool -list -rfc --keystore oauth2.jks | openssl x509 -inform pem -pubkey

注意要在 oauth2.jks 文件所在目录

生成一个公钥,拷贝公钥,复制成一个public.txt文件,放在资源服务器对token解密

2. 复制打印出来的公钥, 

注意:-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----必须要带上 。

3. 在资源服务器的 resources 文件夹下面,新建一个 public.txt 文件,将公钥粘贴进去保存

因为当前的资源服务的所有接口,

不管是有身份还是没有身份的用户都可以访问到, 我们应该让请求在它的请求头中带着 有效 token 过来,才允许访问到对应有权限的接口。

2、添加oAuth2依赖

<!-- Spring Security、OAuth2 和JWT等-->
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

1. 添加依赖后,重启服务,默认会拦截所有请求。

2. 访问 http://localhost:8001/article/swagger-ui.html 接口文档,会被拦截要求登录,所以无法在这里测试接 口。

3. 简单测一个 get 请求,在浏览器直接访问 get 请求的 http://localhost:8001/article/label/1 查询标签id=1的标 签信息,会跳转到一个 Spring Security Oauth2 自带的一个登录页面,要求你先登录通过认证,已经不能直 接访问。

 3、创建 Jwt 管理令牌配置类,采用公钥进行解密

@Configuration
public class JwtTokenStoreConfig {
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 非对称加密,资源服务器使用公钥解密 public.txt
        ClassPathResource resource = new ClassPathResource("public.txt");
        String publicKey = null;
        try {
            publicKey = IOUtils.toString(resource.getInputStream(), "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        converter.setVerifierKey(publicKey);
        /**
         * 定制 AccessToken 转换器添加扩展内容到JWT的转换器中 ++++++++++++++++
         */

        converter.setAccessTokenConverter(new CustomAccessTokenConverter());
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        // Jwt管理令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
}

4、配置资源服务器

.1、 创建 com.mengxuegu.blog.oauth2.config.ResourceServerConfig 类,然后继承 ResourceServerConfigurerAdapter 资源服务器配置适配器

2. 在类上加上以下注解: @Configuration @EnbleResourceServer :标识为资源服务器,所有发往这个服务的请求,都会去请求头里找 token, 找不到或者通过认证服务器验证不合法,则不允许访问。 @EnableGlobalMethodSecurity(prePostEnabled = true) :开启方法级权限控制

3. 重写资源服务器相关配置方法 configure(ResourceServerSecurityConfigurer resources) 指定 Jwt 令牌管理方式

4. 重写 configure(HttpSecurity http) 进行权限规则配置,指定哪些请求接口需要认证后才可访问 ,哪些请 求接口不需要认证就可以访问。放行 swagger-ui.html 接口文档请求。

@Configuration  //标识为配置类
@EnableResourceServer // 标识为资源服务器,请求服务中的资源,就要带着token过来,找不到token或token是无效访问不了资源
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别权限控
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore); // JWT管理令牌
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                // 不使用也不会创建HttpSession实例,因为我们使用 token 方式
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests() // 授权规则配置
                // 放行 swagger-ui 相关请求
                .antMatchers("/v2/api-docs", "/v2/feign-docs",
                        "/swagger-resources/configuration/ui",
                        "/swagger-resources","/swagger-resources/configuration/security",
                        "/swagger-ui.html", "/webjars/**").permitAll()

                // 放行 /api 开头的请求
                .antMatchers("/api/**").permitAll()

                // 所有请求,都需要有all范围(scope)
                .antMatchers("/**").access("#oauth2.hasScope('all')")

                // 其他所有请求都要先通过认证
                .anyRequest().authenticated()
        ;
    }
}

四、Feign 请求拦截器

微服务加上安全配置后,微服务之间使用 Feign 进行远程调用也需要携带 JWT 令牌,通过 Feign 拦截器实现携带 JWT 远程调用。

@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
  if(attributes!=null){
      HttpServletRequest request = attributes.getRequest();
      String token = request.getHeader(HttpHeaders.AUTHORIZATION);
      if(StringUtils.isNotEmpty(token)){
          requestTemplate.header(HttpHeaders.AUTHORIZATION,token);
      }
  }
    }
}

五、资源服务器获取认证用户信息

通过以下方式获取用户信息

// 获取从Security上下文中获取认证信息
Authentication authentication =
 SecurityContextHolder.getContext().getAuthentication();
// 获取用户详情,
OAuth2AuthenticationDetails details =
 (OAuth2AuthenticationDetails)authentication.getDetails();

添加加载扩展信息的转换器

1. 需要添加加载扩展信息的转换器才可以获取到用户信息。 在 com.mengxuegu.blog.oauth2.config.JwtTokenStoreConfig 类中创建一个内部类的 DefaultAccessTokenConverter 子类,实现转换逻辑。并添加到 JwtAccessTokenConverter 中

/**
     * 定制 AccessToken 转换器,为额外扩展的用户信息在资源服务器中获取
     */
    public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
        @Override
        public OAuth2Authentication extractAuthentication(Map<String, ?> map) {

            OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
            oAuth2Authentication.setDetails(map);
            return oAuth2Authentication;
        }
    }
package com.jhj.blog.oauth2.config;

import org.apache.commons.io.IOUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
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;

import java.io.IOException;
import java.util.Map;

/**
 * JWT 管理令牌,指定加密的公钥
 *
 * @Auther:
 */
@Configuration
public class JwtTokenStoreConfig {
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 非对称加密,资源服务器使用公钥解密 public.txt
        ClassPathResource resource = new ClassPathResource("public.txt");
        String publicKey = null;
        try {
            publicKey = IOUtils.toString(resource.getInputStream(), "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        converter.setVerifierKey(publicKey);
        /**
         * 定制 AccessToken 转换器添加扩展内容到JWT的转换器中 ++++++++++++++++
         */

        converter.setAccessTokenConverter(new CustomAccessTokenConverter());
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        // Jwt管理令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 定制 AccessToken 转换器,为额外扩展的用户信息在资源服务器中获取
     */
    public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
        @Override
        public OAuth2Authentication extractAuthentication(Map<String, ?> map) {

            OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
            oAuth2Authentication.setDetails(map);
            return oAuth2Authentication;
        }
    }


}

自定义一个工具方法获取当前登录用户信息

public class AuthUtil {
    /**
     * 获取用户信息
     * @return
     */
    public static SysUser getUserInfo() {
        // 获取从Security上下文中获取认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        OAuth2AuthenticationDetails details =
                (OAuth2AuthenticationDetails)authentication.getDetails();
        System.out.println("principal: "+ details.getDecodedDetails());
        Map<String, Object> map = (Map<String, Object>)details.getDecodedDetails();
        Map<String, String> userInfo = (Map<String, String>) map.get("userInfo");
// mobile=16888888888, uid=9, email=mengxuegu888@163.com, nickName=梦学谷, imageUrl=null,  username=admin

        SysUser user = new SysUser();
        String mobile = userInfo.get("mobile");
        user.setId( userInfo.get("uid") );
        user.setUsername( userInfo.get("username") );
        user.setEmail( userInfo.get("email") );
        user.setNickName( userInfo.get("nickName") );
        user.setImageUrl( userInfo.get("imageUrl") );
        return user;
    }


}

通过这种方法获取用户信息 

方法级别权限注解

六、Gateway 统一网关和限流微服务

网关的作用相当于一个过虑器、拦截器,它可以拦截多个服务的请求。使用网关校验用户的身份是否合法。

1. 用户请求某个资源服务前,需要先通过网关访问Oauth2认证授权服务请求一个AccessToken

2. 用户通过认证授权服务得到 AccessToken 后,通过api网关调用其他资源服务A、B、C

3. 资源服务根据AccessToken验证该token的用户请求是否有效

 主要的依赖

<dependencies>
 <!-- gateway 路由网关依赖-->
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-gateway</artifactId>
 </dependency>
 <!-- gateway 结合 Redis 实现限流 -->
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
 </dependency>
 <!-- 解析 jwt -->
 <dependency>
 <groupId>com.nimbusds</groupId>
 <artifactId>nimbus-jose-jwt</artifactId>
 <version>6.0</version>
 </dependency>
 <!-- nacos 客户端 -->
 <dependency>
 <groupId>com.alibaba.cloud</groupId>
 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 </dependency>
 <!-- nacos 配置中心 -->
 <dependency>
 <groupId>com.alibaba.cloud</groupId>
 <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
 </dependency>
 <!--热部署 ctrl+f9-->
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-devtools</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
 </dependency>
 </dependencies>

application.yml如下

server:
  port: 6001

spring:
  redis:
    host: 127.0.0.1
    # Redis服务器地址
    port: 6379
    # Redis服务器连接端口
    password:
  application:
    name: gateway-server # 应用名
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 注册中心地址
    gateway:
      discovery:
        locator:
#          enabled如果为true,则开启以服务名称为目标服务,http://127.0.0.1:6001/article-server/article/api/article/1
           enabled: true
      routes:
        #路由的唯一标识
        - id: blog-article
          #目标服务地址
          #url:http://127.0.0.1:8001 #一般不使用这个
          #目标微服务名称 lb://目标服务名
          uri: lb://article-server
          #断言,路由的判断条件
          predicates:
            #匹配访问的路由,以/article 开头的请求代理到
            - Path=/article/**
            #访问 http://127.0.0.1:6001/article/api/article/1代理到http://127.0.0.1:8001/article/api/article/1
         # filters:
            #代理转发去掉路径,/article/**,会将每个、article这里去掉
            #- stripPrefix=1
          filters:
            - name: RequestRateLimiter
              args: # 限流过滤器的 Bean 名称
                key-resolver: "#{@uriKeyResolver}"
                # 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
                redis-rate-limiter.replenishRate: 2
                # 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻 止所有请求
                redis-rate-limiter.burstCapacity: 4
                  #允许突发4个请求,但是在下一秒中,仅2个请求可用,如果burstCapacity设置为0,则阻止所有请求
        - id: blog-question
          uri: lb://question-server
          predicates:
            #匹配访问的路由,以/article 开头的请求代理到
            - Path=/question/**
          filters:
            - name: RequestRateLimiter
              args: # 限流过滤器的 Bean 名称
                key-resolver: "#{@uriKeyResolver}"
                # 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
                redis-rate-limiter.replenishRate: 2
                # 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻 止所有请求
                redis-rate-limiter.burstCapacity: 4
                #允许突发4个请求,但是在下一秒中,仅2个请求可用,如果burstCapacity设置为0,则阻止所有请求
        - id: blog-system
          uri: lb://system-server
          predicates:
              #匹配访问的路由,以/article 开头的请求代理到
            - Path=/system/**
          filters:
            - name: RequestRateLimiter
              args: # 限流过滤器的 Bean 名称
                key-resolver: "#{@uriKeyResolver}"
                # 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
                redis-rate-limiter.replenishRate: 2
                # 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻 止所有请求
                redis-rate-limiter.burstCapacity: 4
                #允许突发4个请求,但是在下一秒中,仅2个请求可用,如果burstCapacity设置为0,则阻止所有请求
        - id: blog-auth
          uri: lb://auth-server
          predicates:
              #匹配访问的路由,以/auth 开头的请求代理到
            - Path=/auth/**
          filters:
            - name: RequestRateLimiter
              args: # 限流过滤器的 Bean 名称
                key-resolver: "#{@uriKeyResolver}"
                # 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
                redis-rate-limiter.replenishRate: 2
                # 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻 止所有请求
                redis-rate-limiter.burstCapacity: 4
                #允许突发4个请求,但是在下一秒中,仅2个请求可用,如果burstCapacity设置为0,则阻止所有请求

配置Redis地址

spring:
  redis:
    host: 127.0.0.1
    # Redis服务器地址
    port: 6379
    # Redis服务器连接端口
    password:

微服务应用名称及注册中心nacos的地址

application:
    name: gateway-server # 应用名
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 注册中心地址

开启gateway的配置 

 gateway:
      discovery:
        locator:
#          enabled如果为true,则开启以服务名称为目标服务,http://127.0.0.1:6001/article-server/article/api/article/1
           enabled: true

 效果下面相同

http://127.0.0.1:6001/article-server/article/api/article/1

 http://127.0.0.1:8001/article/api/article/1

 由于上面这种带资源服务器应用名称的方式,过于麻烦,所以采用下面这种方式配置

 routes:
        #路由的唯一标识
        - id: blog-article
          #目标服务地址
          #url:http://127.0.0.1:8001 #一般不使用这个
          #目标微服务名称 lb://目标服务名
          uri: lb://article-server
          #断言,路由的判断条件
          predicates:
            #匹配访问的路由,以/article 开头的请求代理到
            - Path=/article/**
            #访问 http://127.0.0.1:6001/article/api/article/1代理到http://127.0.0.1:8001/article/api/article/1
         # filters:
            #代理转发去掉路径,/article/**,会将每个、article这里去掉
            #- stripPrefix=1

 http://127.0.0.1:6001/article/api/article/1

 Gateway 网关限流

需求:限制每个ip地址1秒可发送的多少个请求。如果超过限制的请求返回429错误。

要结合Redis来限流,每次请求url会存放redis,记录访问是否次数过多,到达一定时间自动会从redis删除

主要添加的依赖

<!-- gateway 结合 Redis 实现限流 -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

appliction.yml中添加的依赖如下 

filters:
            - name: RequestRateLimiter
              args: # 限流过滤器的 Bean 名称
                key-resolver: "#{@uriKeyResolver}"
                # 希望允许用户每秒执行多少个请求。令牌桶填充的速率。
                redis-rate-limiter.replenishRate: 2
                # 允许用户在一秒钟内完成的最大请求数。 这是令牌桶可以容纳的令牌数量,将此值设置为零将阻 止所有请求
                redis-rate-limiter.burstCapacity: 4
                  #允许突发4个请求,但是在下一秒中,仅2个请求可用,如果burstCapacity设置为0,则阻止所有请求

创建 application.yml中的设置的bean uriKeyResolver,对路径进行限流

/**
 * 对接口进行限流操作
 */
@Component("uriKeyResolver")
public class UriKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        // 针对微服务的每个请求地址进行限流
        return Mono.just(exchange.getRequest().getURI().getPath());
    }
}

1. 启动redis , redis-server 版本要用 3 以上的版本.

2. 数据在 redis 中存储的时间只有几秒,所以得使用 monitor 指令来动态的观察.

3. 浏览顺频繁发送:http://localhost:6001/article/api/article/1,当每秒达到4次请求后,就会出现如下图,紧 接着请求 每秒只能请求2次了。

自定义认证过滤器转发请求

Gateway 的核心就是过虑器,通过过虑器实现请求过虑,身份校验等。

自定义过虑器需要实现全局过滤器 GlobalFilter 和 Ordered 接口,分别实现接口中的如下方法:

filter :过滤器的业务逻辑。

getOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。

(一)、效验请求头是否带authorization

设置白名单集合,如果不在白名单中,则必须效验请求头是不是带token值,

package com.jhj.blog.filter;

import net.minidev.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Component
public class AuthorizationFilter implements GlobalFilter, Ordered {
    private static Logger logger = LoggerFactory.getLogger(AuthorizationFilter.class);
    /**
     * 白名单:直接放行请求前缀
     */
    private static final String[] white = {"/api/", ""};
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 请求对象
        ServerHttpRequest request = exchange.getRequest();
        // 响应对象
        ServerHttpResponse response = exchange.getResponse();

        // 请求路径
        String path = request.getPath().pathWithinApplication().value();
        logger.info("发送 {} 请求到 {}", request.getMethod(), path);
        // 公开API接口,无需认证
        if( StringUtils.indexOfAny(path, white) != -1 ) {
            // 直接放行
            return chain.filter(exchange);
        }
        // 获取请求头中 key 为 "Authorization" 的值,
        // 获取token时,要带上 Authorization : Basic client_id:client_secret
        // 请求应用接口,要带上 Authorization : Bearer token
        String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        // 如果请求路径中不存在,不转发请求,响应提示
        if (StringUtils.isEmpty(authorization)) {
            // 响应消息内容对象
            JSONObject message = new JSONObject();
            // 响应状态
            message.put("code", 1401);
            // 响应内容
            message.put("message", "缺少身份凭证");
            // 转换响应消息内容对象为字节
            byte[] bits = message.toJSONString().getBytes( StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bits);
            // 设置响应对象状态码 401
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            // 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
            response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF8");
            // 返回响应对象
            return response.writeWith( Mono.just(buffer) );
        }
        logger.info("请求头有 Authorization 放行请求");
        // 如果不为空,就通过,并接收调用目标服务后响应的结果
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

(二)、效验token是否失效

因为我们采用Redis管理jwt的token值,所以我们这里要解析部分jwt,拿到Redis存放的key也就是jti

JWT由三部分组成(Header,Payload,Signature)

Header头部 :用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。 base64enc({ "alg":"HS256","TYPE":"JWT"}) // eyAiYWxnIjoiSFMyNTYiLCJUWVBFIjoiSldUIn0= Payload 载荷:可以把用户名、角色等无关紧要的信息保存到Payload部分。 base64enc({"user":"vichin","pwd":"weichen123"}) // 用户的关键信息 eyJ1c2VyIjoidmljaGluIiwicH Signature(签名): Signature 部分是根据 header+payload+secretKey 进行加密算出来的,如果Payload被篡 改,就可以在解密 Signature 的时候校验是否被篡改。 HMACSHA256(base64enc(header)+","+base64enc(payload), secretKey) Header和Payload部分使用的是Base64编码,几乎等于明文,可直接解析出来 。校验是否被篡改就是通过解密第 3部分 签名 , 解密成功就没有被篡改,解密失败就被篡改。

核心的对token进行解析

           JWSObject jwsObject = JWSObject.parse(token);
            JSONObject jsonObject = jwsObject.getPayload().toJSONObject();
            System.out.println(jsonObject.toJSONString());
            // 查询 redis 是否存在,不存在则过期。
            String jti = jsonObject.get("jti").toString();
            System.out.println(jti);

 Payload 解析并打印出来是这样

 整个的效验token是否失效如下

package com.jhj.blog.filter;

import com.nimbusds.jose.JWSObject;
import net.minidev.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.text.ParseException;

@Component // 一定不要少了
public class AccessTokenFilter  implements GlobalFilter, Ordered {



    private static Logger logger = LoggerFactory.getLogger(AccessTokenFilter.class);
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 请求对象
        ServerHttpRequest request = exchange.getRequest();
        // 响应对象
        ServerHttpResponse response = exchange.getResponse();
        // 获取请求头访问令牌
        String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        String token = StringUtils.substringAfter(authorization, "Bearer ");
        // 如果 token 为 null 可能/api接口不要带token的请求,直接放行
        if(StringUtils.isEmpty(token)) {
            return chain.filter(exchange);
        }
        String message = null;
        try {
            // 解析token中的载荷部分(认证信息),
            // 注意:载荷部分可直接获取,签名部分才要公钥解密去验证是否有效,交给资源服务器验证
            JWSObject jwsObject = JWSObject.parse(token);
            JSONObject jsonObject = jwsObject.getPayload().toJSONObject();
            System.out.println(jsonObject.toJSONString());
            // 查询 redis 是否存在,不存在则过期。
            String jti = jsonObject.get("jti").toString();
            System.out.println(jti);
            Object value = redisTemplate.opsForValue().get(jti);
            if (value == null) {
                logger.info("令牌已过期 {}", token);
                message = "您的身份已过期,请重新认证!";
            }
        } catch (ParseException e) {
            logger.error("解析令牌错误:{} ", token);
            message = "无效令牌!";
        }
        // 响应消息内容对象
        if ( message == null ) {
            // 如果令牌存在redis就通过
            return chain.filter(exchange);
        }
        // 响应提示响应提示
        JSONObject result = new JSONObject();
        // 响应状态
        result.put("code", 1401);
        // 响应内容
        result.put("message", message);
        // 转换响应消息内容对象为字节
        byte[] bits = result.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        // 设置响应对象状态码 401
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        // 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
        // 返回响应对象
        return response.writeWith( Mono.just(buffer) );
    }

    @Override
    public int getOrder() {
        return 10;
    }
}

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值