一、创建认证微服务端
创建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">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:8001/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">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;
}
}