背景
- jwt token的載荷是明文(base64)件相,雖然只是用來傳遞一些非敏感信息,但依舊會讓人感覺有些不適
- jwt token無法主動失效
- 微服務(wù)之間盡量減少耦合度
解決思路
- 由認(rèn)證服務(wù)(iam)產(chǎn)生RedisToken氧苍,該token保存在iam服務(wù)的redis中夜矗,可以主動失效,也可以設(shè)置失效時間
- 采用jwt作為微服務(wù)間驗證的依據(jù)让虐,如果使用RedisToken紊撕,則所有微服務(wù)均需要依賴同一個redis數(shù)據(jù)庫(或集群)
實現(xiàn)方式
- 用戶登陸,由 認(rèn)證服務(wù) 產(chǎn)生RedisToken交給用戶
- 用戶使用RedisToken通過 網(wǎng)關(guān)服務(wù) 訪問 業(yè)務(wù)服務(wù)赡突,在 網(wǎng)關(guān)服務(wù) 中使用RedisToken交換JwtToken(可以包含一些非敏感的當(dāng)前用戶的信息)对扶,再使用JwtToken訪問業(yè)務(wù)服務(wù)
- 業(yè)務(wù)服務(wù)只對JwtToken進(jìn)行驗證,并且可以從jwt payload中解析出當(dāng)前用戶的信息
代碼實現(xiàn)
注冊中心
eureka惭缰、nacos等辩稽,略
認(rèn)證服務(wù)
使用SpringSecurity實現(xiàn)用戶登陸
- 依賴注入
其他如數(shù)據(jù)庫,redis从媚,服務(wù)發(fā)現(xiàn)逞泄,服務(wù)調(diào)用等依賴就不寫了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 設(shè)置用戶密碼加密方式
/**
* 密碼加密方式
*
* @author Jenson
*/
@Component
@Slf4j
public class CustomBCryptPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// 簡單加密,生成一個salt
String salt = BCrypt.gensalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword != null && encodedPassword != null && !encodedPassword.isEmpty()) {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
} else {
log.warn("Empty encoded password");
return false;
}
}
}
- 登錄認(rèn)證過濾器,設(shè)置登錄地址喷众、調(diào)用方式各谚,繼承
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
/**
* 登錄認(rèn)證過濾器
*
* @author Jenson
*/
public class AuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {
/**
* 設(shè)置登錄地址、調(diào)用方式
*/
public AuthenticationLoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 讀取表單提取數(shù)據(jù)
String username = request.getParameter("username");
String password = request.getParameter("password");
// 封裝到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authRequest);
}
}
- 實現(xiàn)
org.springframework.security.core.userdetails.UserDetailsService
驗證登陸接口中用戶傳入的賬號密碼到千,生成org.springframework.security.core.userdetails.UserDetails
/**
* @author Jenson
*/
@Slf4j
@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 使用用戶名查詢數(shù)據(jù)庫(或是緩存中的)中的用戶持久層對象 UserPO(名字隨便了)
UserPO userPo = this.searchUserPoFromDb(username);
if(userPo == null){
// 用戶不存在昌渤,登錄失敗
return null;
}
// 構(gòu)建 org.springframework.security.core.userdetails.User 對象,當(dāng)然最好是繼承它憔四,可以添加一些自己的屬性上去膀息,比如生日年齡性別啥的
User user =new User(userPo.getUsername,
userPo.getPassword,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
return user ;
}
}
- 實現(xiàn) 認(rèn)證成功處理器,
org.springframework.security.web.authentication.AuthenticationSuccessHandler
認(rèn)證成功后了赵,生成token潜支,輸出token
/**
* 認(rèn)證成功處理器
*
* @author Jenson
*/
@Slf4j
@Component
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
// 這是自己寫的redis-token工具對象
@Autowired
private RedisTokenUtils redisTokenUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
User user = (User) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
// 根據(jù)獲取的用戶信息生成token,并將token保存在redis中柿汛,設(shè)置失效時間冗酿,
// 考慮到登陸時,系統(tǒng)中可能有未失效的token络断,為了避免多端登陸互相踢出裁替,可以先嘗試獲取用戶的token,
// 如果存在則刷新緩存時間(token續(xù)期)貌笨,更新token綁定用戶緩存信息弱判,返回老token;如果不存在則沈城新token
UserTokenRel userTokenRel = redisTokenUtils.refreshToken(user);
// 書寫響應(yīng)體
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.OK.value());
// 在響應(yīng)體中寫出token
byte[] body = JSON.toJSONString(userTokenRel.getLoginToken()).getBytes(StandardCharsets.UTF_8);
OutputStream outputStream = response.getOutputStream();
try {
outputStream.write(body);
} finally {
outputStream.flush();
outputStream.close();
}
}
}
- 實現(xiàn) 認(rèn)證失敗處理器锥惋,
org.springframework.security.web.authentication.AuthenticationFailureHandler
/**
* 認(rèn)證失敗處理
*
* @author Jenson
*/
@Slf4j
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
String exceptionMsg = "認(rèn)證失敗原因";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
OutputStream outputStream = response.getOutputStream();
// 自定義的響應(yīng)體
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse("AUTHENTICATION_FAILURE", exceptionMsg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
byte[] body = JSON.toJSONString(responseEntity.getBody()).getBytes(StandardCharsets.UTF_8);
try {
outputStream.write(body);
} finally {
outputStream.flush();
outputStream.close();
}
}
}
- 登錄過濾器的配置
需要使用到上述創(chuàng)建的三個實例
/**
* 登錄過濾器的配置
*
* @author Jenson
*/
@Configuration
public class AuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
/**
* userDetailService
*/
@Qualifier("customUserDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
/**
* 登錄成功處理器
*/
@Autowired
private AuthenticationSuccessHandler loginAuthenticationSuccessHandler;
/**
* 登錄失敗處理器
*/
@Autowired
private AuthenticationFailureHandler loginAuthenticationFailureHandler;
/**
* 加密
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 將登錄接口的過濾器配置到過濾器鏈中
* 1. 配置登錄成功裕循、失敗處理器
* 2. 配置自定義的userDetailService(從數(shù)據(jù)庫中獲取用戶數(shù)據(jù))
* 3. 將自定義的過濾器配置到spring security的過濾器鏈中,配置在UsernamePasswordAuthenticationFilter之前
*
* @param http HttpSecurity
*/
@Override
public void configure(HttpSecurity http) {
AuthenticationLoginFilter filter = new AuthenticationLoginFilter();
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//認(rèn)證成功處理器
filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
//認(rèn)證失敗處理器
filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
//直接使用DaoAuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//設(shè)置userDetailService
provider.setUserDetailsService(userDetailsService);
//設(shè)置加密算法
provider.setPasswordEncoder(passwordEncoder);
http.authenticationProvider(provider);
//將這個過濾器添加到UsernamePasswordAuthenticationFilter之前執(zhí)行
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
- Token校驗過濾器
此處的token净刮,是jwt-token剥哑,在上述“認(rèn)證成功過濾器”中,發(fā)放的是redis-token淹父,但是使用redis-token調(diào)用網(wǎng)關(guān)接口時株婴,網(wǎng)關(guān)會使用redis-token交換jwt-token,所以該認(rèn)證服務(wù)的filter需要認(rèn)證的是jwt-token
/**
* Token校驗過濾器
*
* @author Jenson
*/
@Slf4j
public class TokenAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest;
httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
log.info("---------> Authorization : {}", authorization);
if (StringUtils.hasText(authorization)) {
String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
String token = tokenDetail[1];
if (StringUtils.hasText(token)) {
User user = null;
try {
// JwtUtils 是自定義的jwt-token解析工具
// 解析jwt-token暑认,創(chuàng)建用戶對象
// HttpServletResponseUtils 是自定義的異常打印工具類
User = new User(JwtUtils.verify(token));
} catch (UnauthorizedException e) {
HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
return;
} catch (ForbiddenException e) {
HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
return;
} catch (Exception e) {
HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
return;
}
// 賬號不為空且還沒有認(rèn)證過
if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 認(rèn)證成功困介,設(shè)置當(dāng)前用戶對象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
該過濾器沒有像參考文檔一樣繼承OncePerRequestFilter
,以為我實際使用中遇到了非自定義異常時蘸际,又會過一遍filter才拋出(不知道是不是哪配置錯了)座哩,而這次過filter時filter調(diào)用鏈中沒有了認(rèn)證的filter,就會拋出認(rèn)證失敗的異常粮彤,將原異常覆蓋根穷。
- 接口未認(rèn)證異常
/**
* 用戶未通過認(rèn)證訪問受保護(hù)的資源 401
*
* @author Jenson
*/
@Slf4j
@Component
public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
authException.printStackTrace();
// 自定義異常打印工具
HttpServletResponseUtils.outPrintUnauthorizedException(response);
}
}
- 接口認(rèn)證無權(quán)限異常
/**
* 認(rèn)證成功的用戶訪問受保護(hù)的資源姜骡,但是權(quán)限不夠 403
*
* @author Jenson
*/
@Slf4j
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 自定義異常打印工具
HttpServletResponseUtils.outPrintForbiddenException(response);
}
}
- 接口授權(quán)配置
/**
* @author Jenson
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuthenticationSecurityConfig authenticationSecurityConfig;
@Qualifier("entryPointUauthenticationHandler")
@Autowired
private AuthenticationEntryPoint entryPointUauthenticationHandler;
@Qualifier("requestAccessDeniedHandler")
@Autowired
private AccessDeniedHandler requestAccessDeniedHandler;
/**
* 授權(quán)配置,最高優(yōu)先級
*
* @param http HttpSecurity
* @return SecurityFilterChain
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用表單登錄
.formLogin().disable()
// 應(yīng)用登錄過濾器的配置屿良,配置分離
.apply(authenticationSecurityConfig)
.and()
// 設(shè)置URL的授權(quán)
.authorizeHttpRequests()
// 這里需要將登錄頁面放行,permitAll()表示不再攔截圈澈,/login 登錄的url,/refreshToken刷新token的url
.requestMatchers(
// 登陸接口
"/login",
// token交換接口(redis-token -> jwt-token)
"/token/generate/jwt/{redisToken}"
)
.permitAll()
.anyRequest()
.authenticated()
//處理異常情況:認(rèn)證失敗和權(quán)限不足
.and()
.exceptionHandling()
//認(rèn)證未通過尘惧,不允許訪問異常處理器
.authenticationEntryPoint(entryPointUauthenticationHandler)
//認(rèn)證通過康栈,但是沒權(quán)限處理器
.accessDeniedHandler(requestAccessDeniedHandler)
.and()
//禁用session,JWT校驗不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//將TOKEN校驗過濾器配置到過濾器鏈中喷橙,否則不生效啥么,放到UsernamePasswordAuthenticationFilter之前
.addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 解決跨域問題(其實沒有解決)
.cors()
.and()
// 關(guān)閉csrf
.csrf().disable();
return http.build();
}
}
將登陸接口(/login
)和 token交換接口(/token/generate/jwt/{redisToken}
) 設(shè)置為免登錄。
/token/generate/jwt/{redisToken}
其實是需要認(rèn)證的贰逾,但是filter配置為了認(rèn)證jwt-token悬荣,當(dāng)然也可以在filter中增加一種認(rèn)證方式,但我這里選擇在容器內(nèi)認(rèn)證似踱,所以該接口設(shè)置為免登錄。
- token 交換接口
/token/generate/jwt/{redisToken}
/**
* 生成JWT
*
* @param redisToken redisToken
* @return jwt
*/
@GetMapping("/generate/jwt/{redisToken}")
public WmResponseEntity<String> generateJwt(@PathVariable String redisToken) {
log.info("-----> generateJwt redisToken : {}", redisToken);
// 根據(jù) redisToken 獲取用戶信息 ,redisTokenUtils 為自定義用戶redis-token管理工具
UserTokenRel userTokenRel = redisTokenUtils.getUserTokenRel(redisToken);
if (userTokenRel == null) {
// CommonException 為自定義 RuntimeException
throw new CommonException("NOT_LOGGED_IN", "用戶未登錄");
}
User user = userService.searchUserByUserId(userTokenRel.getUserId());
// 生成 jwt-token稽煤,jwt的payload中可以盡可能裝入除用戶密碼外的用戶信息核芽,
// 在業(yè)務(wù)服務(wù)中進(jìn)行解析,就不需要跨服務(wù)獲取調(diào)用者信息了酵熙,實現(xiàn)了服務(wù)解耦
String jwt = tokenService.generateJwt(user);
// 異步刷新一下token轧简,避免使用中失效
Long userId = user.getUserId();
// 異步刷新token,token自動續(xù)期匾二,防止用戶用著用著突然掉線
CompletableFuture.runAsync(() -> redisTokenUtils.refreshToken(userId), commonExecutor);
return Results.success(jwt);
}
網(wǎng)關(guān)
- 依賴注入
其他工具依賴就省略了
<!-- gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- gateway 路由配置
無論采用靜態(tài)路由配置還是動態(tài)路由配置哮独,不在此處細(xì)說
配置認(rèn)證服務(wù)的路由前綴為/iam
,則登陸接口地址就是/iam/login
spring-gateway+nacos 實現(xiàn)動態(tài)路由配置可以參考以下文章:
https://blog.csdn.net/qq_38374397/article/details/125874882
- 實現(xiàn)
GlobalFilter
察藐,攔截請求進(jìn)行客戶化處理
自定義filter皮璧,在網(wǎng)關(guān)路由前調(diào)用認(rèn)證服務(wù)接口交換token(只能異步調(diào)用),使用jwt調(diào)用服務(wù)
/**
* 網(wǎng)關(guān)請求攔截客戶化處理
*
* @author Jenson
* @version 1.0
*/
@Slf4j
@Component
public class CuxGlobalFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
log.info("-----------> path : {}", path);
HttpHeaders headers = request.getHeaders();
String authorization = headers.getFirst("Authorization");
String tenant = headers.getFirst("Tenant");
if (!"/iam/login".equals(path) && StringUtils.hasText(authorization)) {
// 非 認(rèn)證 服務(wù)分飞,換用 jwt token悴务,此舉是為了在認(rèn)證層面解耦服務(wù)
headers = HttpHeaders.writableHttpHeaders(headers);
String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
String token = tokenDetail[1];
if (!StringUtils.hasText(token)) {
return outUnauthorizedResponse(response);
}
// ------ 獲取Token start -----
// 在此處,異步 feign調(diào)用 交換 token 接口譬猫,獲得新jwt-token
CompletableFuture<ResponseEntity<String>> newJwtFuture = CompletableFuture.supplyAsync(() -> tokenService.generateJwt(token));
String jwt;
try {
ResponseEntity<String> responseEntity = newJwtFuture.get(1, TimeUnit.SECONDS);
// FeignRspEntityParseUtils 自定義接口響應(yīng)結(jié)果解析工具
jwt = FeignRspEntityParseUtils.parse(responseEntity, String.class);
} catch (CommonException e) {
if ("NOT_LOGGED_IN".equals(e.getCode())) {
return outUnauthorizedResponse(response);
}
return outCommonExceptionResponse(response, e.getCode(), e.getMsg());
} catch (Exception e) {
log.info("----> 換取 jwt 失敗 , {}", e);
return outCommonExceptionResponse(response, "SWITCH_JWT_ERROR", "換取 jwt 失敗 ,請聯(lián)系管理員檢查認(rèn)證服務(wù)");
}
log.info("-----> jwt : {}", jwt);
// ---- 在這里獲取Token end -----
headers.setBearerAuth(jwt);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
/**
* 輸出未認(rèn)證響應(yīng)
*
* @param response 響應(yīng)體
* @return 未認(rèn)證
* @throws JsonProcessingException json解析異常
*/
private Mono<Void> outUnauthorizedResponse(ServerHttpResponse response) throws JsonProcessingException {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
DataBufferFactory bufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse("UNAUTHORIZED", "Unauthorized !"), new HttpHeaders(), HttpStatus.UNAUTHORIZED);
DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
return response.writeWith(Mono.fromSupplier(() -> wrap));
}
/**
* 輸出通用異常響應(yīng)信息
*
* @param response 響應(yīng)體
* @return 未認(rèn)證
* @throws JsonProcessingException json解析異常
*/
private Mono<Void> outCommonExceptionResponse(ServerHttpResponse response, String code, String msg) throws JsonProcessingException {
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
DataBufferFactory bufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse(code, msg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
return response.writeWith(Mono.fromSupplier(() -> wrap));
}
}
- 自定義異常處理(非必須)
將網(wǎng)關(guān)拋的錯改造為自定義的異常格式讯檐,方便前端處理
/**
* 自定義異常處理
*
* @author Jenson
* @version 1.0
*/
@Slf4j
@Configuration
@Order(-1)
public class CuxWebExceptionHandler implements WebExceptionHandler {
@SneakyThrows
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
} else if (ex instanceof ResponseStatusException rspEx) {
HttpStatusCode httpStatusCode = rspEx.getStatusCode();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(httpStatusCode);
DataBufferFactory bufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse(((HttpStatus) httpStatusCode).name(), rspEx.getReason()),
rspEx.getHeaders(), httpStatusCode.value());
DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
return response.writeWith(Mono.fromSupplier(() -> wrap));
} else {
return Mono.error(ex);
}
}
}
- 解決跨域
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* 跨域
*
* @author Jenson
* @version 1.0
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允許的請求頭
config.addAllowedMethod("*");
// 允許的請求源 (如:http://localhost:8080)
config.addAllowedOrigin("*");
// 允許的請求方法 ==> GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
config.addAllowedHeader("*");
// URL 映射 (如: /admin/**)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
業(yè)務(wù)服務(wù)
業(yè)務(wù)服務(wù)的認(rèn)證邏輯都是統(tǒng)一的,所以采用依賴starter組件的方式染服,就可以快速為每一個業(yè)務(wù)服務(wù)增加接口認(rèn)證
- 構(gòu)建
cux-start-core
工程
- pom.xml 依賴注入
......
<groupId>com.cux</groupId>
<artifactId>cux-starter-core</artifactId>
<version>1.0-SNAPSHOT</version>
<name>cux-starter-core</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
......
- 接口認(rèn)證filter(jwt)
此處代碼邏輯和認(rèn)證服務(wù)是一致的别洪,其實可以考慮將此核心依賴包運(yùn)用于認(rèn)證服務(wù)
/**
* Token校驗過濾器
*
* @author Jenson
*/
@Slf4j
public class TokenAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest;
httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
log.info("---------> Authorization : {}", authorization);
if (StringUtils.hasText(authorization)) {
String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
String token = tokenDetail[1];
if (StringUtils.hasText(token)) {
User user = null;
try {
// JwtUtils 是自定義的jwt-token解析工具
// 解析jwt-token,創(chuàng)建用戶對象
// HttpServletResponseUtils 是自定義的異常打印工具類
User = new User(JwtUtils.verify(token));
} catch (UnauthorizedException e) {
HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
return;
} catch (ForbiddenException e) {
HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
return;
} catch (Exception e) {
HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
return;
}
// 賬號不為空且還沒有認(rèn)證過
if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 認(rèn)證成功柳刮,設(shè)置當(dāng)前用戶對象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
- 接口未認(rèn)證異常
此處代碼邏輯和認(rèn)證服務(wù)是一致的挖垛,其實可以考慮將此核心依賴包運(yùn)用于認(rèn)證服務(wù)
/**
* 用戶未通過認(rèn)證訪問受保護(hù)的資源 401
*
* @author Jenson
*/
@Slf4j
@Component
public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
authException.printStackTrace();
// 自定義異常打印工具
HttpServletResponseUtils.outPrintUnauthorizedException(response);
}
}
- 接口認(rèn)證無權(quán)限異常
此處代碼邏輯和認(rèn)證服務(wù)是一致的痒钝,其實可以考慮將此核心依賴包運(yùn)用于認(rèn)證服務(wù)
/**
* 認(rèn)證成功的用戶訪問受保護(hù)的資源,但是權(quán)限不夠 403
*
* @author Jenson
*/
@Slf4j
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 自定義異常打印工具
HttpServletResponseUtils.outPrintForbiddenException(response);
}
}
- 認(rèn)證配置
/**
* @author Jenson
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private PermitRequestMatchers permitRequestMatchers;
/**
* 授權(quán)配置晕换,最高優(yōu)先級
*
* @param http HttpSecurity
* @return SecurityFilterChain
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用表單登錄
.formLogin().disable()
// 設(shè)置URL的授權(quán)
.authorizeHttpRequests()
// 需要放行的url,動態(tài)獲取免登錄接口
.requestMatchers(permitRequestMatchers.requestMatchersToArray())
.permitAll()
.anyRequest()
.authenticated()
//處理異常情況:認(rèn)證失敗和權(quán)限不足
.and()
.exceptionHandling()
//認(rèn)證未通過午乓,不允許訪問異常處理器
.authenticationEntryPoint(new EntryPointUauthenticationHandler())
//認(rèn)證通過,但是沒權(quán)限處理器
.accessDeniedHandler(new RequestAccessDeniedHandler())
.and()
//禁用session闸准,JWT校驗不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//將TOKEN校驗過濾器配置到過濾器鏈中益愈,否則不生效,放到UsernamePasswordAuthenticationFilter之前
.addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 解決跨域問題
.cors()
.and()
// 關(guān)閉csrf
.csrf().disable();
return http.build();
}
}
- 服務(wù)間調(diào)用夷家,token傳遞(
openfeign
)
微服務(wù)間feign調(diào)用是不走網(wǎng)關(guān)的蒸其,為了互相傳遞當(dāng)前調(diào)用接口的jwt-token,需要配置feign库快,token的傳遞可以進(jìn)行公共配置
- feign 依賴
<!--服務(wù)調(diào)用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>4.0.1</version>
</dependency>
- Feign 請求攔截器
/**
* Feign 請求攔截器
* <p>
* 請求帶上token
*
* @author Jenson
*/
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String authorization = request.getHeader("Authorization");
requestTemplate.header("Authorization", authorization);
}
}
}
/**
* @author Jenson
*/
@Configuration
public class FeignRequestInterceptorConfig {
@Bean
public RequestInterceptor createFeignRequestInterceptor() {
return new FeignRequestInterceptor();
}
}
- spring.factories 配置文件
定義需要裝載的配置類
- 打包摸袁,安裝依賴
本地安裝:mvn install
- 將該核心依賴運(yùn)用于業(yè)務(wù)服務(wù),即可完成對業(yè)務(wù)服務(wù)的接口權(quán)限控制
業(yè)務(wù)服務(wù)與認(rèn)證服務(wù)义屏、網(wǎng)關(guān)需要在同一個注冊中心下靠汁;
網(wǎng)關(guān)需要配置對應(yīng)業(yè)務(wù)服務(wù)的路由;
此時闽铐,直接訪問業(yè)務(wù)服務(wù)的接口需要jwt-token蝶怔,通過網(wǎng)關(guān)訪問,需要redis-token兄墅;
只需將網(wǎng)關(guān)暴露到外部而不需要暴露業(yè)務(wù)服務(wù)
其他補(bǔ)充
- jwt 生成 和 解析
- pom.xml 依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
- 生成jwt
/**
* 生成jwt,設(shè)置超時時間
*
* @param payload 載荷
* @return jwt
*/
public static String generate(Map<String, String> payload) {
//過期時間
Date expireDate = new Date(System.currentTimeMillis() + DEFAULT_JWT_TTL);
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
JWTCreator.Builder jwtBuilder = JWT.create()
// 添加頭部
.withHeader(map)
//超時設(shè)置,設(shè)置過期的日期
.withExpiresAt(expireDate)
//簽發(fā)時間
.withIssuedAt(new Date());
// 構(gòu)建 jwt 載荷
payload.forEach(jwtBuilder::withClaim);
// 簽名踢星,返回
return jwtBuilder.sign(Algorithm.HMAC256(DEFAULT_JWT_SECRET));
}
- 解析 jwt
/**
* 校驗token并解析token
*
* @return 從token中解析出的載荷
*/
public static Map<String, Claim> verify(String token) {
if ("token_absent".equals(token)) {
// redis token 不存在
throw new UnauthorizedException();
}
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(DEFAULT_JWT_SECRET)).build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaims();
} catch (TokenExpiredException e) {
log.error("jwt token已過期");
throw new UnauthorizedException();
} catch (JWTVerificationException e) {
log.error("jwt token不存在或不正確");
throw new ForbiddenException();
}
}