日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

SpringCould整合oauth2

發(fā)布時(shí)間:2023/12/8 javascript 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SpringCould整合oauth2 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、創(chuàng)建認(rèn)證微服務(wù)端

創(chuàng)建mengxuegu-blog-oauth2微服務(wù)工程,做認(rèn)證使用

認(rèn)證服務(wù)器表結(jié)構(gòu)默認(rèn)如下

?最主要表就是這個(gè)oauth_client_details

?生成 client_sercet密碼

@RunWith(SpringRunner.class) @SpringBootTest public class TestAuthApplication {@Testpublic 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>

因?yàn)榈卿浺{(diào)用其他微服務(wù)模塊的接口,所以先封裝接口

feign封裝的接口

1、 findUserByUsername (通過用戶名查詢用戶信息)

2、findMenuByUserId(通過用戶的ID查詢用戶所有權(quán)限)

@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查詢擁有權(quán)限")@GetMapping("/api/feign/menu/{userId}")List<SysMenu> findMenuByUserId(@PathVariable("userId") String userId);

(1)、實(shí)現(xiàn) UserDetailsService

邏輯 1. 因?yàn)?UserDetailsService 接口中有一個(gè) UserDetails loadUserByUsername(String username) 抽象方法, 它的返回值 UserDetails 接口,我們要創(chuàng)建一個(gè) JwtUser 類實(shí)現(xiàn)這個(gè)接口。

注意:isAccountNonExpired 聲明了 boolean 類型,但是在構(gòu)造器是 Integer 類型接收, 原因是 數(shù)據(jù)庫 sys_user 表中存儲的是整型,所以我們?nèi)缓筠D(zhuǎn) boolean,即 : this.isAccountNonExpired = isAccountNonExpired == 1 ? true: false;

@JSONField(serialize = false) // 忽略轉(zhuǎn)json ,因?yàn)?后面我們要將這個(gè)類對象轉(zhuǎn)成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) // 忽略轉(zhuǎn)json@ApiModelProperty(value = "密碼,加密存儲, admin/1234")private String password;@ApiModelProperty(value = "昵稱")private String nickName;@ApiModelProperty(value = "頭像url")private String imageUrl;@ApiModelProperty(value = "注冊手機(jī)號")private String mobile;@ApiModelProperty(value = "注冊郵箱")private String email;// 1 true 0 false@JSONField(serialize = false) // 忽略轉(zhuǎn)json@ApiModelProperty(value = "帳戶是否過期(1 未過期,0已過期)")private boolean isAccountNonExpired; // 不要寫小寫 boolean@JSONField(serialize = false) // 忽略轉(zhuǎn)json@ApiModelProperty(value = "帳戶是否被鎖定(1 未過期,0已過期)")private boolean isAccountNonLocked;@JSONField(serialize = false) // 忽略轉(zhuǎn)json@ApiModelProperty(value = "密碼是否過期(1 未過期,0已過期)")private boolean isCredentialsNonExpired;@JSONField(serialize = false) // 忽略轉(zhuǎn)json@ApiModelProperty(value = "帳戶是否可用(1 可用,0 刪除用戶)")private boolean isEnabled;/*** 封裝用戶擁有的菜單權(quán)限標(biāo)識*/@JSONField(serialize = false) // 忽略轉(zhuǎn)jsonprivate List<GrantedAuthority> authorities;// isAccountNonExpired 是 Integer 類型接收,然后轉(zhuǎn) booleanpublic 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)、創(chuàng)建UserDetailsServiceImpl 實(shí)現(xiàn) UserDetailsService接口。

調(diào)用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 // 檢查啟動類注解 @EnableFeignClientsprivate IFeignSystemController feignSystemController;@Overridepublic 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("用戶名或密碼錯(cuò)誤");}// 3. 通過用戶id去查詢數(shù)據(jù)庫的擁有的權(quán)限信息List<SysMenu> menuList =feignSystemController.findMenuByUserId(sysUser.getId());// 4. 封裝權(quán)限信息(權(quán)限標(biāo)識符code)List<GrantedAuthority> authorities = null;if(CollectionUtils.isNotEmpty(menuList)) {authorities = new ArrayList<>();for(SysMenu menu: menuList) {// 權(quán)限標(biāo)識String code = menu.getCode(); // 將權(quán)限標(biāo)識封裝起來比如 article:delete 文章刪除權(quán)限authorities.add(new SimpleGrantedAuthority(code));}}// 5. 構(gòu)建UserDetails接口的實(shí)現(xiàn)類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 //標(biāo)注配置類 public class PasswordEncoderConfig {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();} }

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

JSON Web Token(JWT)是一個(gè)開放的行業(yè)標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊且獨(dú)立的方式,用于在各方之 間作為JSON對象安全地傳輸信息。此信息可以通過數(shù)字簽名進(jìn)行驗(yàn)證和信任。JWT可以使用密碼(使用HMAC算 法)或使用RSA或ECDSA的公鑰/私鑰對進(jìn)行簽名 ,防止被篡改。

JWT令牌生成采用非對稱加密算法.

別名為 oauth2,秘鑰算法為 RSA,秘鑰口令為 oauth2,秘鑰庫(文件)名稱為 oauth2.jks,秘鑰庫(文 件)口令為 oauth2。輸入命令回車后,后面還問題需要回答,最后輸入 y 表示確定 :

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

將生成的 oauth2.jks 文件 拷貝到認(rèn)證微服務(wù)服務(wù)器 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 {@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();// 采用非對稱加密jwt// 第1個(gè)參數(shù)就是密鑰證書文件,第2個(gè)參數(shù) 密鑰庫口令, 私鑰進(jìn)行簽名KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());converter.setKeyPair(factory.getKeyPair("oauth2"));return converter;}@Beanpublic TokenStore tokenStore() {// Jwt管理令牌return new JwtTokenStore(jwtAccessTokenConverter());}}

? (5)、擴(kuò)展認(rèn)證的響應(yīng)數(shù)據(jù)(將用戶信息userinfo響應(yīng)給前端)

@Component // 不要少了 public class JwtTokenEnhancer implements TokenEnhancer {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { // 擴(kuò)展令牌內(nèi)容JwtUser user = (JwtUser) oAuth2Authentication.getPrincipal();Map<String, Object> map = new LinkedHashMap<>();map.put("userInfo", JSON.toJSON(user));//設(shè)置附加信息((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);return oAuth2AccessToken;} }

(6)、加載中文響應(yīng)信息

? ? ?全局搜索messages_zh_CN,找到springscurity的jar包解壓,拿到messages_zh_CN.properties文件,放到resource目錄下面,因?yàn)榈讓訒由蟨roperties,所以不加上后綴。

@Configuration public class ReloadMessageConfig {@Bean // 加載中文的認(rèn)證提示信息public ReloadableResourceBundleMessageSource messageSource() {ReloadableResourceBundleMessageSource messageSource = newReloadableResourceBundleMessageSource();//.properties 不要加到后面 // messageSource.setBasename("classpath:org/springframework/security/messages_zh_CN");messageSource.setBasename("classpath:messages_zh_CN");//不要.propertiesreturn messageSource;} }

(7)、創(chuàng)建認(rèn)證服務(wù)器配置類

1、配置數(shù)據(jù)源,2、配置密碼模式,3配置擴(kuò)展響應(yīng)數(shù)據(jù)等

@Configuration @EnableAuthorizationServer // 開啟了認(rèn)證服務(wù)器 public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowired // 1. 數(shù)據(jù)源private DataSource dataSource;@Bean // 1. 客戶端使用 jdbc 管理public ClientDetailsService jdbcClientDetailsService() {return new JdbcClientDetailsService(dataSource);}/*** 1. 配置被允許訪問此認(rèn)證服務(wù)器的客戶端信息: 數(shù)據(jù)庫方式* 如:門戶客戶端,后臺客戶端*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {// jdbc 管理客戶端clients.withClientDetails(jdbcClientDetailsService());}// 2. 在 SpringSecurityConfig 中添加到容器了, 密碼模式需要@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UserDetailsService userDetailsService;@Autowired // token管理方式,引用 JwtTokenStoreConfig 配置的private TokenStore tokenStore;@Autowired // jwt 轉(zhuǎn)換器private JwtAccessTokenConverter jwtAccessTokenConverter;/*** 注入擴(kuò)展器*/@Autowiredprivate TokenEnhancer jwtTokenEnhancer;/*** 關(guān)于認(rèn)證服務(wù)器端點(diǎn)配置** @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager);// 刷新令牌時(shí)需要使用endpoints.userDetailsService(userDetailsService);// 令牌的管理方式endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);// 添加擴(kuò)展器 +++++++ ++++++++++++++++++++++++++++TokenEnhancerChain enhancerChain = new TokenEnhancerChain();List<TokenEnhancer> enhancerList = new ArrayList<>();enhancerList.add(jwtTokenEnhancer);enhancerList.add(jwtAccessTokenConverter);enhancerChain.setTokenEnhancers(enhancerList);endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {// /oauth/check_token 解析令牌,默認(rèn)情況 下拒絕訪問security.checkTokenAccess("permitAll()");}}

1、密碼模式響應(yīng)用戶信息

localhost:7001/auth/oauth/token

密碼模式響應(yīng)用戶信息?

?通過刷新令牌獲取用戶信息

(8)、封裝接口放回restful風(fēng)格接口

自定義返回值,遵循其他接口一樣的restful風(fēng)格

1、創(chuàng)建service層,

通過 loadBalancerClient 調(diào)用調(diào)用認(rèn)證中心的接口,拼接成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 {@AutowiredLoadBalancerClient loadBalancerClient;//開啟負(fù)載均衡public Result refreshToken(String header, String refreshToken) throws HttpProcessException {ServiceInstance serviceInstance = loadBalancerClient.choose("auth-server");if (serviceInstance == null) {return Result.error("未找到有效認(rèn)證服務(wù)器");}// 請求刷新令牌 url,拿到分布式微服務(wù)的基礎(chǔ)路徑System.out.println("serviceInstance.getUri():"+serviceInstance.getUri().toString());String refreshTokenUrl = serviceInstance.getUri().toString() + "/auth/oauth/token";// 封裝刷新令牌請求參數(shù)HashMap<String , Object> map = new HashMap<>(2);map.put("grant_type", "refresh_token");map.put("refresh_token",refreshToken);// 構(gòu)建配置請求參數(shù)(網(wǎng)址、請求參數(shù)、編碼、client)Header[] headers = HttpHeader.custom() // 自定義請求.contentType(HttpHeader.Headers.APP_FORM_URLENCODED) // 數(shù)據(jù)類型.authorization(header) // 認(rèn)證請求頭.build();HttpConfig config = HttpConfig.custom().headers(headers).url(refreshTokenUrl).map(map);// 發(fā)送請求, 響應(yīng)令牌String token = HttpClientUtil.post(config);JSONObject jsonToken = JSON.parseObject(token);if(StringUtils.isNotEmpty(jsonToken.getString("error")) ) {return Result.build(ResultEnum.TOKEN_PAST);}// 響應(yīng)新令牌對象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 {@Autowiredprivate AuthService authService;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate ClientDetailsService clientDetailsService;private static final String HEADER_TYPE = "Basic ";@GetMapping("/user/refreshToken")//相當(dāng)于問號傳參的那樣HttpServletRequestpublic 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解碼,那取到授權(quán)表的用戶名和密碼String[] tokens = RequestUtil.extractAndDecodeHeader(header);assert tokens.length == 2;/*** assert(斷言的用法)* if(假設(shè)成立)* {* 程序正常運(yùn)行;* }* else* {* 報(bào)錯(cuò)&&終止程序!(避免由程序運(yùn)行引起更大的錯(cuò)誤)* }*/String clientId = tokens[0];String clientSecret = tokens[1]; // springCloud自定義的方法用于判斷,通過ID來查尋一條記錄ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);if (clientDetails == null) {throw new UnapprovedClientAuthenticationException("clientId對應(yīng)的配置信息不存在:"+ clientId);}//拿到這一條中存儲的密碼String clientSecret1 = clientDetails.getClientSecret();//通過加密器match方法自定義效驗(yàn),返回的是布爾值if (!passwordEncoder.matches(clientSecret, clientSecret1)) {throw new UnapprovedClientAuthenticationException("無效clientSecret");}/***** 前面終于效驗(yàn)完了請求頭和刷新碼refresh_token* 下面調(diào)用service封裝的調(diào)用方法,放回reful風(fēng)格驗(yàn)證信息*/// 獲取刷新令牌return authService.refreshToken(header, refreshToken);} catch (Exception e) {return Result.error("令牌刷新失敗" + e.getMessage());}} }

3、進(jìn)行關(guān)閉 csrf 攻擊,不然調(diào)用不到接口

/*** 在安全配置類 com.mengxuegu.blog.oauth2.config.SpringSecurityConfig 覆蓋#configure(HttpSecurity)* 進(jìn)行關(guān)閉 csrf 攻擊,不然調(diào)用不到接口* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();}

4、調(diào)用接口

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

二、Redis管理JWT令牌-登錄與退出

Redis 存儲有效令牌:

1、生成Jwt 訪問令牌的時(shí)候,將 Jwt Token 存入redis中

2、擴(kuò)展Jwt的驗(yàn)證功能,驗(yàn)證redis中是否存在數(shù)據(jù),如果存在則token有效,否則無效

3、退出系統(tǒng)時(shí)將Redis中的數(shù)據(jù)刪除。

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服務(wù)器地址port: 6379 # Redis服務(wù)器連接端口password: # Redis服務(wù)器連接密碼(默認(rèn)為空),redis 不需要用戶名的

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

@Beanpublic TokenStore tokenStore() {// Jwt管理令牌,//將jti作為key值,存儲token信息return new JwtTokenStore(jwtAccessTokenConverter()) {@Overridepublic void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {//判斷map中是否含有jtiif (token.getAdditionalInformation().containsKey("jti")) {//token值String value = token.getValue();//超時(shí)時(shí)間int expiresIn = token.getExpiresIn();String jti = token.getAdditionalInformation().get("jti").toString(); // redis設(shè)置key value ,超時(shí)時(shí)間,時(shí)間單位秒redisTemplate.opsForValue().set(jti, value, expiresIn, TimeUnit.SECONDS);}super.storeAccessToken(token, authentication);}@Overridepublic void removeAccessToken(OAuth2AccessToken token) {if (token.getAdditionalInformation().containsKey("jti")) {// 通過 Jwt 的唯一標(biāo)識 jti 為 Key 刪除 redis 中數(shù)據(jù)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 {// 關(guān)閉csrf攻擊http.formLogin() // ++.and().csrf().disable(); }

(1)、成功處理器獲取 token 值響應(yīng)

創(chuàng)建 com.mengxuegu.blog.oauth2.CustomAuthenticationSuccessHandler 注意不要放到與 SpringSecurityConfig 同級包,不然功能都會失效,要放到父包上。

@Component("customAuthenticationSuccessHandler") public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate ClientDetailsService clientDetailsService;private static final String HEADER_TYPE = "Basic ";@Resourceprivate AuthorizationServerTokenServices authorizationServerTokenServices;@Resourceprivate ObjectMapper objectMapper;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException {/*** 開始效驗(yàn)請求頭*/String header = request.getHeader(HttpHeaders.AUTHORIZATION);//判斷請求頭中的信息if (header == null || !header.startsWith(HEADER_TYPE)) {throw new UnapprovedClientAuthenticationException("請求頭中無client信息");}/*** 開始效驗(yàn)請求的信息,并與數(shù)據(jù)庫中比對*///通過自定義的base64解碼,那取到授權(quán)表的用戶名和密碼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對應(yīng)的配置信息不存在:"+ clientId);}//拿到這一條中存儲的密碼String clientSecret1 = clientDetails.getClientSecret();//通過加密器match方法自定義效驗(yàn),返回的是布爾值if (!passwordEncoder.matches(clientSecret, clientSecret1)) {throw new UnapprovedClientAuthenticationException("無效clientSecret");}/*** 結(jié)束效驗(yàn)信息*/Result result = null;try {// 構(gòu)建 tokenRequest 和 oAuth2Request 組合成 oAuth2Authentication 去獲取 accessTokenTokenRequest tokenRequest =new TokenRequest(MapUtils.EMPTY_MAP, clientId,clientDetails.getScope(), "custom");OAuth2Request oAuth2Request =tokenRequest.createOAuth2Request(clientDetails);OAuth2Authentication oAuth2Authentication =new OAuth2Authentication(oAuth2Request, authentication);// 獲取 accessTokenOAuth2AccessToken token =authorizationServerTokenServices.createAccessToken(oAuth2Authentication);result = Result.ok(token);} catch (Exception e) {// 認(rèn)證失敗result = Result.build(ResultEnum.AUTH_FAIL.getCode(), e.getMessage());}// 響應(yīng)結(jié)果response.setContentType("application/json;charset=UTF-8");PrintWriter writer = response.getWriter();writer.write(objectMapper.writeValueAsString(result));} }

(2)、登錄失敗處理器

@Component("customAuthenticationFailureHandler") public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { // 響應(yīng)錯(cuò)誤信息:json格式response.setContentType("application/json;charset=UTF-8");String result = objectMapper.writeValueAsString(Result.error(e.getMessage()));response.getWriter().write( result );} }

(3)、注入失敗與成功處理器

@Autowiredprivate AuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()// 成功處理器.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and().csrf().disable();}

2、測試登錄

訪問 post 請求 http://localhost:7001/auth/login 登錄,添加 Basic 請求頭,和用戶名密碼. 登錄后,查看 redis 中是否有數(shù)據(jù)。

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 {@AutowiredTokenStore tokenStore;@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException { // 獲取 access_tokenString accessToken = request.getParameter("accessToken");if (StringUtils.isNotBlank(accessToken)) {// 轉(zhuǎn)換token對象OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);if (oAuth2AccessToken != null) {// 刪除redis的訪問令牌tokenStore.removeAccessToken(oAuth2AccessToken);}}// 退出成功, 響應(yīng)結(jié)果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 {// 關(guān)閉csrf攻擊http.formLogin()// 成功處理器.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and().logout() // 1. 退出成功處理器 +++++++.logoutSuccessHandler(logoutSuccessHandler).and().csrf().disable();}

三、資源服務(wù)器安全配置

1、創(chuàng)建公鑰

根據(jù)密鑰證書獲取公鑰 在請求資源服務(wù)器(文章、問答、系統(tǒng)微服務(wù))接口時(shí),都必須要求在請求頭帶上jwt令牌來訪問服務(wù)接口,而認(rèn) 證服務(wù)器生成的jwt令牌是通過非對稱私鑰進(jìn)行加密了,資源服務(wù)收到請求后,要解析出jwt令牌就需要公鑰進(jìn)行解 析出來。所以下面要通過 密鑰證書獲取公鑰,放到資源服務(wù)器中,這樣資源服務(wù)器可以直接解析出有效信息。

1、安裝 OpenSSL

2、配置 OpenSSL 的環(huán)境變量,即你所安裝的目錄\bin,如: D:\workInstall\OpenSSL-Win64\bin

1、重新打開 CMD 命令行窗口, 進(jìn)入 oauth2.jks 文件所在目錄執(zhí)行如下命令

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

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

生成一個(gè)公鑰,拷貝公鑰,復(fù)制成一個(gè)public.txt文件,放在資源服務(wù)器對token解密

2. 復(fù)制打印出來的公鑰,?

注意:-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----必須要帶上 。

3. 在資源服務(wù)器的 resources 文件夾下面,新建一個(gè) public.txt 文件,將公鑰粘貼進(jìn)去保存

因?yàn)楫?dāng)前的資源服務(wù)的所有接口,

不管是有身份還是沒有身份的用戶都可以訪問到, 我們應(yīng)該讓請求在它的請求頭中帶著 有效 token 過來,才允許訪問到對應(yīng)有權(quán)限的接口。

2、添加oAuth2依賴

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

1. 添加依賴后,重啟服務(wù),默認(rèn)會攔截所有請求。

2. 訪問 http://localhost:8001/article/swagger-ui.html 接口文檔,會被攔截要求登錄,所以無法在這里測試接 口。

3. 簡單測一個(gè) get 請求,在瀏覽器直接訪問 get 請求的 http://localhost:8001/article/label/1 查詢標(biāo)簽id=1的標(biāo) 簽信息,會跳轉(zhuǎn)到一個(gè) Spring Security Oauth2 自帶的一個(gè)登錄頁面,要求你先登錄通過認(rèn)證,已經(jīng)不能直 接訪問。

?3、創(chuàng)建 Jwt 管理令牌配置類,采用公鑰進(jìn)行解密

@Configuration public class JwtTokenStoreConfig {@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();// 非對稱加密,資源服務(wù)器使用公鑰解密 public.txtClassPathResource 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 轉(zhuǎn)換器添加擴(kuò)展內(nèi)容到JWT的轉(zhuǎn)換器中 ++++++++++++++++*/converter.setAccessTokenConverter(new CustomAccessTokenConverter());return converter;}@Beanpublic TokenStore tokenStore() {// Jwt管理令牌return new JwtTokenStore(jwtAccessTokenConverter());} }

4、配置資源服務(wù)器

.1、 創(chuàng)建?com.mengxuegu.blog.oauth2.config.ResourceServerConfig 類,然后繼承 ResourceServerConfigurerAdapter 資源服務(wù)器配置適配器

2. 在類上加上以下注解: @Configuration @EnbleResourceServer :標(biāo)識為資源服務(wù)器,所有發(fā)往這個(gè)服務(wù)的請求,都會去請求頭里找 token, 找不到或者通過認(rèn)證服務(wù)器驗(yàn)證不合法,則不允許訪問。 @EnableGlobalMethodSecurity(prePostEnabled = true) :開啟方法級權(quán)限控制

3. 重寫資源服務(wù)器相關(guān)配置方法 configure(ResourceServerSecurityConfigurer resources) 指定 Jwt 令牌管理方式

4. 重寫 configure(HttpSecurity http) 進(jìn)行權(quán)限規(guī)則配置,指定哪些請求接口需要認(rèn)證后才可訪問 ,哪些請 求接口不需要認(rèn)證就可以訪問。放行 swagger-ui.html 接口文檔請求。

@Configuration //標(biāo)識為配置類 @EnableResourceServer // 標(biāo)識為資源服務(wù)器,請求服務(wù)中的資源,就要帶著token過來,找不到token或token是無效訪問不了資源 @EnableGlobalMethodSecurity(prePostEnabled = true) // 開啟方法級別權(quán)限控 public class ResourceServerConfig extends ResourceServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.tokenStore(tokenStore); // JWT管理令牌}@Overridepublic void configure(HttpSecurity http) throws Exception {http.sessionManagement()// 不使用也不會創(chuàng)建HttpSession實(shí)例,因?yàn)槲覀兪褂?token 方式.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests() // 授權(quán)規(guī)則配置// 放行 swagger-ui 相關(guān)請求.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')")// 其他所有請求都要先通過認(rèn)證.anyRequest().authenticated();} }

四、Feign 請求攔截器

微服務(wù)加上安全配置后,微服務(wù)之間使用 Feign 進(jìn)行遠(yuǎn)程調(diào)用也需要攜帶 JWT 令牌,通過 Feign 攔截器實(shí)現(xiàn)攜帶 JWT 遠(yuǎn)程調(diào)用。

@Component public class FeignRequestInterceptor implements RequestInterceptor {@Overridepublic 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);}}} }

五、資源服務(wù)器獲取認(rèn)證用戶信息

通過以下方式獲取用戶信息

// 獲取從Security上下文中獲取認(rèn)證信息 Authentication authentication =SecurityContextHolder.getContext().getAuthentication(); // 獲取用戶詳情, OAuth2AuthenticationDetails details =(OAuth2AuthenticationDetails)authentication.getDetails();

添加加載擴(kuò)展信息的轉(zhuǎn)換器

1. 需要添加加載擴(kuò)展信息的轉(zhuǎn)換器才可以獲取到用戶信息。 在 com.mengxuegu.blog.oauth2.config.JwtTokenStoreConfig 類中創(chuàng)建一個(gè)內(nèi)部類的 DefaultAccessTokenConverter 子類,實(shí)現(xiàn)轉(zhuǎn)換邏輯。并添加到 JwtAccessTokenConverter 中

/*** 定制 AccessToken 轉(zhuǎn)換器,為額外擴(kuò)展的用戶信息在資源服務(wù)器中獲取*/public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {@Overridepublic 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 {@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();// 非對稱加密,資源服務(wù)器使用公鑰解密 public.txtClassPathResource 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 轉(zhuǎn)換器添加擴(kuò)展內(nèi)容到JWT的轉(zhuǎn)換器中 ++++++++++++++++*/converter.setAccessTokenConverter(new CustomAccessTokenConverter());return converter;}@Beanpublic TokenStore tokenStore() {// Jwt管理令牌return new JwtTokenStore(jwtAccessTokenConverter());}/*** 定制 AccessToken 轉(zhuǎn)換器,為額外擴(kuò)展的用戶信息在資源服務(wù)器中獲取*/public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {@Overridepublic OAuth2Authentication extractAuthentication(Map<String, ?> map) {OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);oAuth2Authentication.setDetails(map);return oAuth2Authentication;}}}

自定義一個(gè)工具方法獲取當(dāng)前登錄用戶信息

public class AuthUtil {/*** 獲取用戶信息* @return*/public static SysUser getUserInfo() {// 獲取從Security上下文中獲取認(rèn)證信息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=夢學(xué)谷, imageUrl=null, username=adminSysUser 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;}}

通過這種方法獲取用戶信息?

方法級別權(quán)限注解

六、Gateway 統(tǒng)一網(wǎng)關(guān)和限流微服務(wù)

網(wǎng)關(guān)的作用相當(dāng)于一個(gè)過慮器、攔截器,它可以攔截多個(gè)服務(wù)的請求。使用網(wǎng)關(guān)校驗(yàn)用戶的身份是否合法。

1. 用戶請求某個(gè)資源服務(wù)前,需要先通過網(wǎng)關(guān)訪問Oauth2認(rèn)證授權(quán)服務(wù)請求一個(gè)AccessToken

2. 用戶通過認(rèn)證授權(quán)服務(wù)得到 AccessToken 后,通過api網(wǎng)關(guān)調(diào)用其他資源服務(wù)A、B、C

3. 資源服務(wù)根據(jù)AccessToken驗(yàn)證該token的用戶請求是否有效

?主要的依賴

<dependencies><!-- gateway 路由網(wǎng)關(guān)依賴--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!-- gateway 結(jié)合 Redis 實(shí)現(xiàn)限流 --><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: 6001spring:redis:host: 127.0.0.1# Redis服務(wù)器地址port: 6379# Redis服務(wù)器連接端口password:application:name: gateway-server # 應(yīng)用名cloud:nacos:discovery:server-addr: 127.0.0.1:8848 # 注冊中心地址gateway:discovery:locator: # enabled如果為true,則開啟以服務(wù)名稱為目標(biāo)服務(wù),http://127.0.0.1:6001/article-server/article/api/article/1enabled: trueroutes:#路由的唯一標(biāo)識- id: blog-article#目標(biāo)服務(wù)地址#url:http://127.0.0.1:8001 #一般不使用這個(gè)#目標(biāo)微服務(wù)名稱 lb://目標(biāo)服務(wù)名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:#代理轉(zhuǎn)發(fā)去掉路徑,/article/**,會將每個(gè)、article這里去掉#- stripPrefix=1filters:- name: RequestRateLimiterargs: # 限流過濾器的 Bean 名稱key-resolver: "#{@uriKeyResolver}"# 希望允許用戶每秒執(zhí)行多少個(gè)請求。令牌桶填充的速率。redis-rate-limiter.replenishRate: 2# 允許用戶在一秒鐘內(nèi)完成的最大請求數(shù)。 這是令牌桶可以容納的令牌數(shù)量,將此值設(shè)置為零將阻 止所有請求redis-rate-limiter.burstCapacity: 4#允許突發(fā)4個(gè)請求,但是在下一秒中,僅2個(gè)請求可用,如果burstCapacity設(shè)置為0,則阻止所有請求- id: blog-questionuri: lb://question-serverpredicates:#匹配訪問的路由,以/article 開頭的請求代理到- Path=/question/**filters:- name: RequestRateLimiterargs: # 限流過濾器的 Bean 名稱key-resolver: "#{@uriKeyResolver}"# 希望允許用戶每秒執(zhí)行多少個(gè)請求。令牌桶填充的速率。redis-rate-limiter.replenishRate: 2# 允許用戶在一秒鐘內(nèi)完成的最大請求數(shù)。 這是令牌桶可以容納的令牌數(shù)量,將此值設(shè)置為零將阻 止所有請求redis-rate-limiter.burstCapacity: 4#允許突發(fā)4個(gè)請求,但是在下一秒中,僅2個(gè)請求可用,如果burstCapacity設(shè)置為0,則阻止所有請求- id: blog-systemuri: lb://system-serverpredicates:#匹配訪問的路由,以/article 開頭的請求代理到- Path=/system/**filters:- name: RequestRateLimiterargs: # 限流過濾器的 Bean 名稱key-resolver: "#{@uriKeyResolver}"# 希望允許用戶每秒執(zhí)行多少個(gè)請求。令牌桶填充的速率。redis-rate-limiter.replenishRate: 2# 允許用戶在一秒鐘內(nèi)完成的最大請求數(shù)。 這是令牌桶可以容納的令牌數(shù)量,將此值設(shè)置為零將阻 止所有請求redis-rate-limiter.burstCapacity: 4#允許突發(fā)4個(gè)請求,但是在下一秒中,僅2個(gè)請求可用,如果burstCapacity設(shè)置為0,則阻止所有請求- id: blog-authuri: lb://auth-serverpredicates:#匹配訪問的路由,以/auth 開頭的請求代理到- Path=/auth/**filters:- name: RequestRateLimiterargs: # 限流過濾器的 Bean 名稱key-resolver: "#{@uriKeyResolver}"# 希望允許用戶每秒執(zhí)行多少個(gè)請求。令牌桶填充的速率。redis-rate-limiter.replenishRate: 2# 允許用戶在一秒鐘內(nèi)完成的最大請求數(shù)。 這是令牌桶可以容納的令牌數(shù)量,將此值設(shè)置為零將阻 止所有請求redis-rate-limiter.burstCapacity: 4#允許突發(fā)4個(gè)請求,但是在下一秒中,僅2個(gè)請求可用,如果burstCapacity設(shè)置為0,則阻止所有請求

配置Redis地址

spring:redis:host: 127.0.0.1# Redis服務(wù)器地址port: 6379# Redis服務(wù)器連接端口password:

微服務(wù)應(yīng)用名稱及注冊中心nacos的地址

application:name: gateway-server # 應(yīng)用名cloud:nacos:discovery:server-addr: 127.0.0.1:8848 # 注冊中心地址

開啟gateway的配置?

gateway:discovery:locator: # enabled如果為true,則開啟以服務(wù)名稱為目標(biāo)服務(wù),http://127.0.0.1:6001/article-server/article/api/article/1enabled: true

?效果下面相同

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

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

?由于上面這種帶資源服務(wù)器應(yīng)用名稱的方式,過于麻煩,所以采用下面這種方式配置

routes:#路由的唯一標(biāo)識- id: blog-article#目標(biāo)服務(wù)地址#url:http://127.0.0.1:8001 #一般不使用這個(gè)#目標(biāo)微服務(wù)名稱 lb://目標(biāo)服務(wù)名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:#代理轉(zhuǎn)發(fā)去掉路徑,/article/**,會將每個(gè)、article這里去掉#- stripPrefix=1

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

?Gateway 網(wǎng)關(guān)限流

需求:限制每個(gè)ip地址1秒可發(fā)送的多少個(gè)請求。如果超過限制的請求返回429錯(cuò)誤。

要結(jié)合Redis來限流,每次請求url會存放redis,記錄訪問是否次數(shù)過多,到達(dá)一定時(shí)間自動會從redis刪除

主要添加的依賴

<!-- gateway 結(jié)合 Redis 實(shí)現(xiàn)限流 --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>

appliction.yml中添加的依賴如下?

filters:- name: RequestRateLimiterargs: # 限流過濾器的 Bean 名稱key-resolver: "#{@uriKeyResolver}"# 希望允許用戶每秒執(zhí)行多少個(gè)請求。令牌桶填充的速率。redis-rate-limiter.replenishRate: 2# 允許用戶在一秒鐘內(nèi)完成的最大請求數(shù)。 這是令牌桶可以容納的令牌數(shù)量,將此值設(shè)置為零將阻 止所有請求redis-rate-limiter.burstCapacity: 4#允許突發(fā)4個(gè)請求,但是在下一秒中,僅2個(gè)請求可用,如果burstCapacity設(shè)置為0,則阻止所有請求

創(chuàng)建 application.yml中的設(shè)置的bean?uriKeyResolver,對路徑進(jìn)行限流

/*** 對接口進(jìn)行限流操作*/ @Component("uriKeyResolver") public class UriKeyResolver implements KeyResolver {@Overridepublic Mono<String> resolve(ServerWebExchange exchange) {// 針對微服務(wù)的每個(gè)請求地址進(jìn)行限流return Mono.just(exchange.getRequest().getURI().getPath());} }

1. 啟動redis , redis-server 版本要用 3 以上的版本.

2. 數(shù)據(jù)在 redis 中存儲的時(shí)間只有幾秒,所以得使用 monitor 指令來動態(tài)的觀察.

3. 瀏覽順頻繁發(fā)送:http://localhost:6001/article/api/article/1,當(dāng)每秒達(dá)到4次請求后,就會出現(xiàn)如下圖,緊 接著請求 每秒只能請求2次了。

自定義認(rèn)證過濾器轉(zhuǎn)發(fā)請求

Gateway 的核心就是過慮器,通過過慮器實(shí)現(xiàn)請求過慮,身份校驗(yàn)等。

自定義過慮器需要實(shí)現(xiàn)全局過濾器 GlobalFilter 和 Ordered 接口,分別實(shí)現(xiàn)接口中的如下方法:

filter :過濾器的業(yè)務(wù)邏輯。

getOrder:此方法返回整型數(shù)值,通過此數(shù)值來定義過濾器的執(zhí)行順序,數(shù)字越小優(yōu)先級越高。

(一)、效驗(yàn)請求頭是否帶authorization

設(shè)置白名單集合,如果不在白名單中,則必須效驗(yàn)請求頭是不是帶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/", ""};@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 請求對象ServerHttpRequest request = exchange.getRequest();// 響應(yīng)對象ServerHttpResponse response = exchange.getResponse();// 請求路徑String path = request.getPath().pathWithinApplication().value();logger.info("發(fā)送 {} 請求到 {}", request.getMethod(), path);// 公開API接口,無需認(rèn)證if( StringUtils.indexOfAny(path, white) != -1 ) {// 直接放行return chain.filter(exchange);}// 獲取請求頭中 key 為 "Authorization" 的值,// 獲取token時(shí),要帶上 Authorization : Basic client_id:client_secret// 請求應(yīng)用接口,要帶上 Authorization : Bearer tokenString authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);// 如果請求路徑中不存在,不轉(zhuǎn)發(fā)請求,響應(yīng)提示if (StringUtils.isEmpty(authorization)) {// 響應(yīng)消息內(nèi)容對象JSONObject message = new JSONObject();// 響應(yīng)狀態(tài)message.put("code", 1401);// 響應(yīng)內(nèi)容message.put("message", "缺少身份憑證");// 轉(zhuǎn)換響應(yīng)消息內(nèi)容對象為字節(jié)byte[] bits = message.toJSONString().getBytes( StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);// 設(shè)置響應(yīng)對象狀態(tài)碼 401response.setStatusCode(HttpStatus.UNAUTHORIZED);// 設(shè)置響應(yīng)對象內(nèi)容并且指定編碼,否則在瀏覽器中會中文亂碼response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF8");// 返回響應(yīng)對象return response.writeWith( Mono.just(buffer) );}logger.info("請求頭有 Authorization 放行請求");// 如果不為空,就通過,并接收調(diào)用目標(biāo)服務(wù)后響應(yīng)的結(jié)果return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;} }

(二)、效驗(yàn)token是否失效

因?yàn)槲覀儾捎肦edis管理jwt的token值,所以我們這里要解析部分jwt,拿到Redis存放的key也就是jti

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

Header頭部 :用于描述關(guān)于該JWT的最基本的信息,例如其類型以及簽名所用的算法等。 base64enc({ "alg":"HS256","TYPE":"JWT"}) // eyAiYWxnIjoiSFMyNTYiLCJUWVBFIjoiSldUIn0= Payload 載荷:可以把用戶名、角色等無關(guān)緊要的信息保存到Payload部分。 base64enc({"user":"vichin","pwd":"weichen123"}) // 用戶的關(guān)鍵信息 eyJ1c2VyIjoidmljaGluIiwicH Signature(簽名): Signature 部分是根據(jù) header+payload+secretKey 進(jìn)行加密算出來的,如果Payload被篡 改,就可以在解密 Signature 的時(shí)候校驗(yàn)是否被篡改。 HMACSHA256(base64enc(header)+","+base64enc(payload), secretKey) Header和Payload部分使用的是Base64編碼,幾乎等于明文,可直接解析出來 。校驗(yàn)是否被篡改就是通過解密第 3部分 簽名 , 解密成功就沒有被篡改,解密失敗就被篡改。

核心的對token進(jìn)行解析

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 解析并打印出來是這樣

?整個(gè)的效驗(yàn)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);@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 請求對象ServerHttpRequest request = exchange.getRequest();// 響應(yīng)對象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中的載荷部分(認(rèn)證信息),// 注意:載荷部分可直接獲取,簽名部分才要公鑰解密去驗(yàn)證是否有效,交給資源服務(wù)器驗(yàn)證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 = "您的身份已過期,請重新認(rèn)證!";}} catch (ParseException e) {logger.error("解析令牌錯(cuò)誤:{} ", token);message = "無效令牌!";}// 響應(yīng)消息內(nèi)容對象if ( message == null ) {// 如果令牌存在redis就通過return chain.filter(exchange);}// 響應(yīng)提示響應(yīng)提示JSONObject result = new JSONObject();// 響應(yīng)狀態(tài)result.put("code", 1401);// 響應(yīng)內(nèi)容result.put("message", message);// 轉(zhuǎn)換響應(yīng)消息內(nèi)容對象為字節(jié)byte[] bits = result.toJSONString().getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);// 設(shè)置響應(yīng)對象狀態(tài)碼 401response.setStatusCode(HttpStatus.UNAUTHORIZED);// 設(shè)置響應(yīng)對象內(nèi)容并且指定編碼,否則在瀏覽器中會中文亂碼response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");// 返回響應(yīng)對象return response.writeWith( Mono.just(buffer) );}@Overridepublic int getOrder() {return 10;} }

總結(jié)

以上是生活随笔為你收集整理的SpringCould整合oauth2的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。